Транспорт бэкенда
Сериализация: 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 передаёт **байты** - последовательность нулей и единиц. Но в коде работают с объектами, структурами, классами. Как объект `{ 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-валидацию.
| Характеристика | JSON | XML |
|---|---|---|
| Читаемость | Высокая | Средняя (многословный) |
| Размер данных | Компактнее (~30% меньше XML) | Больше (теги открытия/закрытия) |
| Типы данных | string, number, boolean, null, array, object | Всё - строки (типы через XSD) |
| Схема валидации | JSON Schema (опционально) | XSD, DTD (зрелая экосистема) |
| Комментарии | Не поддерживает | Поддерживает <!-- ... --> |
| Парсинг | Быстрый (встроен в браузер/JS) | Медленнее (DOM/SAX парсеры) |
| Использование в 2025 | REST API, конфиги, NoSQL | SOAP, 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. Имена полей сохраняются, но кодируются эффективнее.
| Критерий | Protobuf | Avro | MessagePack |
|---|---|---|---|
| Схема | Обязательна (.proto) | Встроена в файл/Registry | Не нужна (schemaless) |
| Кодирование полей | По номерам (1, 2, 3) | По порядку в схеме | По именам (как JSON) |
| Размер данных | Минимальный | Компактный | Средний (имена сохранены) |
| Экосистема | gRPC, Google Cloud | Kafka, Hadoop, Spark | Redis, 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;` чтобы компилятор не дал его использовать повторно.
| Действие | Protobuf | Avro | JSON |
|---|---|---|---|
| Добавить поле | Безопасно (новый 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