Транспорт бэкенда

Сериализация: JSON, Protobuf, Avro

Google обрабатывает 10+ миллиардов внутренних RPC-вызовов в секунду. Если бы каждое сообщение было в JSON, трафик между серверами вырос бы в 5-10 раз - это петабайты лишних данных ежедневно. Именно поэтому в 2001 году Google создал Protocol Buffers, который сегодня используют Uber, Netflix, Spotify и тысячи других компаний.

  • **Kafka + Avro** в LinkedIn обрабатывает 7+ триллионов сообщений в день - Schema Registry гарантирует, что 2000+ сервисов могут обновляться независимо без поломки формата данных
  • **Discord** перешёл с JSON на MessagePack для WebSocket-сообщений и сократил трафик на 30-40%, что критично при 200+ миллионах пользователей
  • **gRPC + Protobuf** в Dropbox сократил размер межсервисных сообщений в 8 раз по сравнению с JSON REST API

Protobuf: от внутреннего инструмента Google до индустриального стандарта

В 2001 году Google столкнулся с проблемой: сотни сервисов обменивались данными через простой текст. Скорость сериализации и размер данных стали bottleneck. Команда разработала Protocol Buffers - бинарный формат с генерацией кода. В 2008 году Google опубликовал исходный код. Появился gRPC (2015) - высокопроизводительный RPC-фреймворк на основе Protobuf. Сегодня Protobuf используется в Kubernetes, Envoy, Cloud Spanner. Параллельно в экосистеме Hadoop появился Apache Avro (2009) - Doug Cutting решал ту же задачу для Big Data, но с акцентом на self-describing формат для долговременного хранения.

Предварительные знания

  • TCP/IP and OSI: The Foundation of Everything

Зачем нужна сериализация

TCP передаёт **байты** - последовательность нулей и единиц. Но в коде работают с объектами, структурами, классами. Как объект `{ name: "Alice", age: 30 }` из памяти одного сервера попадает в память другого?

**Сериализация** - процесс преобразования структуры данных в формат, который можно передать по сети или сохранить на диск. **Десериализация** - обратный процесс: из байтов/текста обратно в объект в памяти.

Сериализация решает три проблемы: **1)** преобразование из внутреннего представления памяти в переносимый формат, **2)** совместимость между языками (JS-объект → JSON → Python dict), **3)** компактность для передачи по сети.

Объект в памяти - это указатели, виртуальные таблицы методов, padding для выравнивания. Передать raw-память между двумя сервисами невозможно, даже если они на одном языке: разные версии runtime, разная архитектура процессора (little-endian vs big-endian).

Почему нельзя просто отправить raw-память объекта с одного сервера на другой?

JSON и XML: текстовые форматы

**JSON (JavaScript Object Notation)** - де-факто стандарт для веб-API. Придуман Дугласом Крокфордом в начале 2000-х как лёгкая альтернатива XML. Человеко-читаемый, поддерживается всеми языками, минимальный синтаксис.

**XML (eXtensible Markup Language)** - предшественник JSON, до сих пор используется в SOAP, конфигурациях (Maven, Android), корпоративных системах. Более многословный, но поддерживает атрибуты, namespaces и XSD-валидацию.

ХарактеристикаJSONXML
ЧитаемостьВысокаяСредняя (многословный)
Размер данныхКомпактнее (~30% меньше XML)Больше (теги открытия/закрытия)
Типы данныхstring, number, boolean, null, array, objectВсё - строки (типы через XSD)
Схема валидацииJSON Schema (опционально)XSD, DTD (зрелая экосистема)
КомментарииНе поддерживаетПоддерживает <!-- ... -->
ПарсингБыстрый (встроен в браузер/JS)Медленнее (DOM/SAX парсеры)
Использование в 2025REST API, конфиги, NoSQLSOAP, Enterprise, Android, SVG

JSON победил XML в вебе не потому что лучше, а потому что проще. `JSON.parse()` встроен в каждый браузер. Но XML выразительнее: namespaces предотвращают коллизии имён, XSLT трансформирует документы, XPath - выразительный query-язык.

Главный недостаток текстовых форматов - **размер и скорость**. Строка `"age": 30` занимает 8 байт, хотя число 30 - это 1 байт. Имена полей повторяются в каждом объекте массива. Для 1 000 запросов в секунду приемлемо. Для 1 000 000 - уже нет.

Что является главным преимуществом JSON перед бинарными форматами?

Protocol Buffers: бинарная эффективность

**Protocol Buffers (Protobuf)** - бинарный формат сериализации от Google. Создан в 2001 году для внутренней коммуникации между сервисами Google, открыт в 2008. Сегодня - основа gRPC и стандарт де-факто для высоконагруженных бэкендов.

Ключевая идея Protobuf: **schema-first**. Сначала описывается структура данных в `.proto`-файле, затем компилятор генерирует код для сериализации/десериализации на любом языке. Никакого ручного парсинга.

**Field numbers** (1, 2, 3...) - не индексы, а идентификаторы. Они кодируются в бинарные данные вместо имён полей. Вместо `"name":` (6 байт) Protobuf пишет один байт с номером поля и типом. Это даёт колоссальную экономию при тысячах сообщений.

Protobuf использует **varint-кодирование** для чисел: маленькие числа занимают меньше байт. Число 1 - один байт, число 300 - два байта, число 1 000 000 - три байта. JSON всегда кодирует числа как текст: «1» = 1 байт, «300» = 3 байта, но «1000000» = 7 байт против 3.

Protobuf - не серебряная пуля. Бинарные данные нечитаемы без .proto-файла. `curl` покажет мусор, а не читаемый JSON. Отладка сложнее. Зато скорость сериализации - в 5-10 раз быстрее JSON, а размер - в 3-10 раз меньше.

Зачем Protobuf использует field numbers (1, 2, 3) вместо имён полей?

Avro и MessagePack: другие бинарные форматы

Protobuf - не единственный бинарный формат. Два других заслуживают внимания: **Apache Avro** (мир Big Data и Kafka) и **MessagePack** (быстрый бинарный JSON).

**Apache Avro** - формат, созданный Дагом Каттингом (автором Hadoop) для экосистемы Apache. Главное отличие от Protobuf: **схема передаётся вместе с данными**. Файл Avro содержит заголовок со схемой, а затем блоки данных. Читатель может декодировать данные без отдельного .proto-файла.

**Kafka + Avro** - золотой стандарт для event streaming. Schema Registry (Confluent) хранит все версии Avro-схем. Producer публикует событие, указывая schema id. Consumer автоматически получает нужную схему из Registry и декодирует сообщение.

**MessagePack** - бинарный формат, позиционируемый как «JSON, только быстрее и компактнее». В отличие от Protobuf и Avro, MessagePack **не требует схемы** - это schemaless формат, как JSON. Имена полей сохраняются, но кодируются эффективнее.

КритерийProtobufAvroMessagePack
СхемаОбязательна (.proto)Встроена в файл/RegistryНе нужна (schemaless)
Кодирование полейПо номерам (1, 2, 3)По порядку в схемеПо именам (как JSON)
Размер данныхМинимальныйКомпактныйСредний (имена сохранены)
ЭкосистемаgRPC, Google CloudKafka, Hadoop, SparkRedis, MessagePack-RPC
Код-генерацияДа (protoc)Да (avro-tools)Нет (schemaless)
Простота внедренияСредняя (нужен компилятор)Средняя (нужен Registry)Высокая (drop-in JSON)

Выбор формата зависит от контекста. **Protobuf** - для gRPC и строго типизированных API. **Avro** - для Kafka и Big Data. **MessagePack** - когда нужен быстрый JSON без изменения архитектуры.

Чем Avro принципиально отличается от Protobuf в подходе к схемам?

Эволюция схем: не ломай production

Схемы меняются. Добавлено поле `phone` в `User`. Сервис A уже обновлён и отправляет `phone`. Сервис B ещё нет - он не знает про `phone`. Что произойдёт?

**Schema evolution** - способность формата корректно работать, когда producer и consumer используют **разные версии** схемы. Три вида совместимости:

Protobuf изначально спроектирован для schema evolution. **Field numbers** - ключ к этому. При добавлении нового поля присваивается **новый номер**. Старый код не знает про номер 6 - просто пропустит. Новый код не нашёл номер 6 в старых данных - использует default.

**Золотое правило Protobuf: никогда не переиспользуй field numbers.** Если удалено поле `phone = 4`, то номер 4 навсегда «занят». Используй `reserved 4;` чтобы компилятор не дал его использовать повторно.

ДействиеProtobufAvroJSON
Добавить полеБезопасно (новый field number)Безопасно (default value)Безопасно (новый ключ)
Удалить полеБезопасно (reserved)Безопасно (игнорируется)Опасно (код может упасть)
Изменить типЛомает совместимостьЛомает совместимостьТихо сломается в runtime
ПереименоватьБезопасно (wire = номер)Ломает (wire = имя)Ломает (wire = имя)
ВалидацияCompile-time (protoc)Registry (runtime)Нет (только runtime)

JSON не имеет встроенного механизма schema evolution. Если сервис A добавил поле, сервис B может упасть с ошибкой - или молча проигнорировать. Нет гарантий. Для критичных систем с десятками сервисов и независимыми деплоями Protobuf и Avro кратно безопаснее.

Объект `{ name: "Alice", age: 30 }` из начала урока. Теперь известен весь путь: объект → сериализация (JSON/Protobuf/Avro) → байты → TCP → сеть → TCP → десериализация → объект на другом сервисе. И ясно, как этот процесс остаётся работоспособным, когда схема данных эволюционирует.

JSON достаточно для любых задач - зачем усложнять бинарными форматами?

JSON идеален для публичных API и отладки, но для high-throughput внутренней коммуникации бинарные форматы дают 3-10x экономию на размере и 5-10x на скорости парсинга

При 100 запросах в секунду разница между JSON и Protobuf незаметна. При 100 000 запросов/сек каждый лишний байт = гигабайты трафика в день. Google внутри использует Protobuf для всех сервисов, потому что при их масштабе JSON создавал бы терабайты лишнего трафика ежедневно. Второй аргумент: бинарные форматы со схемами ловят ошибки на этапе компиляции, а не в runtime на production.

Ключевые идеи

  • **Сериализация** преобразует объект в памяти в формат, передаваемый по сети. Десериализация - обратный процесс
  • **JSON** - текстовый, человеко-читаемый, универсальный. Идеален для публичных API и отладки
  • **Protobuf** - бинарный, schema-first, компактный (3-10x меньше JSON). Стандарт для gRPC и внутренних API
  • **Avro** - бинарный с встроенной схемой. Стандарт для Kafka и Big Data. **MessagePack** - бинарный JSON без схемы
  • **Schema evolution** - критична для микросервисов с независимым деплоем. Protobuf field numbers и Avro Schema Registry решают эту задачу

Связанные темы

Сериализация - мост между данными в коде и данными на проводе:

  • TCP/IP и OSI — Сериализованные байты передаются через TCP/UDP на транспортном уровне
  • gRPC — gRPC использует Protobuf как формат сериализации по умолчанию
  • Kafka — Kafka чаще всего использует Avro + Schema Registry для сообщений

Вопросы для размышления

  • Проектируется API для мобильного приложения с миллионами пользователей. Трафик дорогой (мобильные данные). Какой формат сериализации выбрать для ответов API и почему?
  • Почему Kafka использует Avro, а не Protobuf, хотя Protobuf компактнее? Подумать про хранение сообщений на месяцы/годы.
  • 50 микросервисов на 5 разных языках. Как schema evolution в Protobuf помогает деплоить их независимо друг от друга?

Связанные уроки

  • it-03 — Сжатие данных - та же задача компактности, другой подход
  • bd-01 — Форматы хранения в БД применяют те же принципы бинарного кодирования
  • se-03 — API design - выбор формата сериализации для публичных контрактов
  • sd-03-scalability — Выбор формата влияет на throughput при горизонтальном масштабировании
  • stream-01 — Kafka использует Avro + Schema Registry для event streaming
  • comp-31-bytecode
Сериализация: JSON, Protobuf, Avro

0

1

Войти

В Protobuf удалено поле `phone = 4` из схемы. Что нужно сделать, чтобы не сломать совместимость?