Мы работали с крупным производственным предприятием с , и Redis действовал как основное хранилище для пользовательского состояния. Каждая запись в Redis была Он выглядел чистым и удобным — пока не начал болеть. 10 million monthly active users JSON-serialized Pydantic model По мере того, как мы росли, наш кластер расширялся до Объекты JSON надувались далеко за пределами размера фактических данных, и мы буквально платили за это. - в облачных счетах, потерянной оперативной памяти и ухудшенной производительности. five Redis nodes Воздух В какой-то момент я рассчитал соотношение реальной полезной нагрузки к общему объему хранения, и результат сделал очевидным, что мы не могли продолжать так: 14,000 bytes per user in JSON → 2,000 bytes in a binary format А Только из-за серийного формата. 7× difference. Именно тогда я построил то, что в конце концов стало - компактный бинарный кодировщик / декодер для моделей Pydantic.И ниже рассказывается история о том, как я попал туда, что не сработало, и почему окончательный подход сделал Redis (и наши кошельки) намного счастливее. PyByntic Почему JSON стал проблемой JSON отлично подходит как универсальный формат обмена, но внутри кеша низкого уровня он превращается в : memory-hungry monster Он хранит имена полей в полном объеме она хранит типы косвенно как строки Он дублирует структуру снова и снова Не оптимизирован для бинарных данных он надувает использование оперативной памяти до 3–10× размера реальной полезной нагрузки Когда вы держите Это уже не какое-то академическое неэффективность – это В масштабе JSON перестает быть безобидным удобством и становится молчаливым налогом на память. tens of millions of objects in Redis real bill and an extra server in the cluster Какие альтернативы существуют (и почему они не работают) Я прошел через очевидных кандидатов: Format Why It Failed in Our Case Protobuf Too much ceremony: separate schemas, code generation, extra tooling, and a lot of friction for simple models MessagePack More compact than JSON, but still not enough – and integrating it cleanly with Pydantic was far from seamless BSON Smaller than JSON, but the Pydantic integration story was still clumsy and not worth the hassle Protobuf Слишком много церемонии: отдельные схемы, генерация кода, дополнительные инструменты и много трения для простых моделей MessagePack Более компактный, чем JSON, но все еще недостаточно — и интегрировать его чисто с Pydantic было далеко не безупречно BSON Меньше, чем JSON, но история интеграции Pydantic все еще была неуклюжей и не стоила хлопот Все эти форматы хороши в целом, но для конкретного сценария Они чувствовали, что Тяжелый, шумный и с практически никаким реальным облегчением в использовании памяти. “Pydantic + Redis as a state store” using a sledgehammer to crack a nut Мне нужно решение, которое: перейти в существующую кодовую базу всего с несколькими строками радикальное снижение использования памяти избегать дополнительных DSL, схем или генерации кода работа напрямую с моделями Pydantic, не нарушая экосистемы Что я построил Так что я закончил писать минималистический бинарный формат с легким кодировщиком / декодером на верхней части отмеченных моделей Pydantic. Была рождена PyByntic Его API намеренно спроектирован так, что вы можете пропустить его практически без трения — в большинстве случаев вы просто заменяете звонки, такие как: model.serialize() # replaces .model_dump_json() Model.deserialize(bytes) # replaces .model_validate_json() Пример использования : from pybyntic import AnnotatedBaseModel from pybyntic.types import UInt32, String, Bool from typing import Annotated class User(AnnotatedBaseModel): user_id: Annotated[int, UInt32] username: Annotated[str, String] is_active: Annotated[bool, Bool] data = User( user_id=123, username="alice", is_active=True ) raw = data.serialize() obj = User.deserialize(raw) В качестве опции вы также можете предоставить пользовательскую функцию сжатия: import zlib serialized = user.serialize(encoder=zlib.compress) deserialized_user = User.deserialize(serialized, decoder=zlib.decompress) сравнение Для справедливого сравнения, я создал на основе наших реальных моделей производства. Каждый пользовательский объект содержал смесь полей – , , , , , , , и Кроме того, каждый пользователь имел таких ролей и разрешений, а в некоторых случаях могут быть Другими словами, это был не синтетический пример игрушки — это был реалистичный набор данных с глубоко укоренившимися структурами и широким спектром типов полей. 2 million user records UInt16 UInt32 Int32 Int64 Bool Float32 String DateTime32 nested objects hundreds of permissions per user График показывает, сколько памяти потребляет Redis при хранении используя различные форматы сериализации. JSON используется в качестве базовой линии приблизительно PyByntic оказался самым компактным вариантом — просто , что касается Protobuf и MessagePack также предлагают заметное улучшение по сравнению с JSON, но в абсолютных цифрах они все еще далеко отстают от PyByntic. 2,000,000 user objects 35.1 GB ~4.6 GB (13.3% of JSON) 7.5× smaller Давайте сравним, что это означает для вашего облачного счета: Format Price of Redis on GCP JSON $876/month PyByntic $118/month MessagePack $380/month BSON $522/month Protobuf $187/month JSON $876 в месяц PyByntic $118/month Пакет сообщений $380 в месяц Бсон $522 в месяц Протоколы $187 в месяц Этот расчет основан на хранении 2 000 000 пользовательских объектов с использованием с Google Cloud Platform. Экономия значительна – и она расширяется по мере роста нагрузки. Отзывы о Redis Cluster Откуда берутся пространственные сбережения? Огромные сбережения памяти исходят из двух простых фактов: , и В JSON типичное время датирования хранится в виде строки, подобной - это это Так как каждый персонаж ASCII является Стоимость единого временного знака В бинарных, а принимает только , делая его с нулевым форматированием сверху. binary data doesn’t need a text format it doesn’t repeat structure on every object "1970-01-01T00:00:01.000000" 26 characters 1 byte = 8 bits 208 bits DateTime32 32 bits 6.5× smaller То же самое относится и к цифрам. (на В JSON принимает , в то время как бинарное представление является фиксированным И, наконец, JSON продолжает Бинарный формат не нуждается в этом — схема известна заранее, поэтому нет структурного налога на каждый запись. 18446744073709551615 2^64−1 20 characters = 160 bits 64 bits repeating field names for every single object Эти три эффекта – - это именно то, откуда происходит уменьшение размера. no strings, no repetition, and no formatting overhead Заключение Если вы используете Pydantic и состояние хранения в Redis, то JSON - это роскошь, которую вы платите Бинарный формат, который остается совместимым с вашими существующими моделями, является просто более рациональным выбором. RAM tax Для нас , Именно это и стало возможным — а Это не сломало ничего, а устранило целый класс проблем и ненужных перегрузок. PyByntic Логическая оптимизация Репозитории GitHub: https://github.com/sijokun/PyByntic