Сообщение должно пережить новую версию
- 01ПроизводительСмысл и схема записи
- 02ФорматПредставление данных
- 03ПотребительПоддерживаемые версии
Брокер сообщений и HTTP-сервер передают байты. Смысл этим байтам задаёт контракт между производителем и потребителем: какие поля существуют, что они означают и как меняются со временем.
Поэтому формат обмена — не соревнование «текст против бинарника». В реальной интеграции важнее четыре вопроса:
- Как описана схема?
- Какие изменения совместимы со старыми клиентами и данными?
- Чем диагностировать сообщение в production?
- Даёт ли другой формат измеримый выигрыш на нашем payload и стеке?
Краткая карта форматов
| Формат | Схема | Сильная сторона | Основная цена |
|---|---|---|---|
| JSON | Опционально: JSON Schema, OpenAPI | Простые инструменты и читаемость | Контракт легко оставить только «в договорённостях» |
| XML | Опционально: XSD | Зрелые схемы, пространства имён, enterprise-стек | Многословность и сложность инструментов |
| YAML | Зависит от задачи | Ручное описание конфигураций и спецификаций | Неоднозначности парсеров и неудобство wire-формата |
| Protobuf | .proto, номера полей, генерация кода | Компактный контракт для типизированных клиентов | Кодогенерация и дисциплина совместимости |
| Avro | Writer schema + reader schema | Разрешение версий схем в потоках и файлах | Без схемы бинарные данные не прочитать |
| MessagePack | Встроенной схемы нет | Бинарная модель, близкая к JSON | Эволюцию контракта нужно организовать отдельно |
JSON не обязан быть бесконтрольным, а бинарный формат не делает данные корректными автоматически. Схема проверяет структуру и типы, но бизнес-инварианты всё равно остаются в приложении.
JSON, XML и YAML: разные роли
JSON остаётся удобным выбором для публичных и внутренних HTTP API. Его легко посмотреть в логах, воспроизвести через стандартные инструменты и поддержать почти на любом языке. Но для долгоживущего контракта полезны OpenAPI или JSON Schema, проверки совместимости и правило обработки неизвестных полей.
XML нужен не только SOAP. XSD может описывать структуру, типы и ограничения, а namespaces помогают соединять словари. При этом XML сам по себе не гарантирует валидацию: получатель должен действительно применить схему и корректно обрабатывать её версии.
YAML я оставляю для конфигураций и человекоориентированных спецификаций, например OpenAPI. В качестве транспортного формата он редко даёт преимущество: разные версии и настройки парсеров могут по-разному интерпретировать скаляры, а отступы повышают цену случайной ошибки.
Бинарный формат не равен «быстрее»
Protobuf, Avro и MessagePack часто дают меньший payload и более дешёвый разбор, но итог зависит от данных, библиотек, языка, компрессии и дальнейшей обработки. На небольшом HTTP-ответе сеть и бизнес-логика могут стоить намного больше сериализации.
Поэтому я не принимаю решение по чужому бенчмарку. Беру реальные сообщения и измеряю:
- размер без компрессии и с той компрессией, которая будет в production;
- CPU и память на сериализацию и чтение;
- p95/p99 всего запроса или pipeline, а не отдельной функции;
- время на генерацию клиентов, диагностику и обновление схем;
- поведение старого потребителя при новом сообщении.
Последний пункт обычно важнее первых четырёх.
Protobuf: контракт держится на номерах полей
Простая схема выглядит так:
syntax = "proto3";
message Message {
string sender = 1;
string text = 2;
}
В бинарном представлении идентификатором поля служит номер, а не имя. Из этого следуют основные правила:
- новое поле с новым номером обычно совместимо на wire-уровне;
- номер существующего поля нельзя менять;
- удалённый номер нельзя отдавать другому смыслу — его нужно пометить
reserved; - совместимость байтов не гарантирует совместимость сгенерированного кода и бизнес-семантики;
- изменения типов имеют отдельные правила и не всегда безопасны.
Protobuf хорошо подходит для gRPC и внутренних типизированных контрактов. Его сила не только в размере, а в том, что схема становится версионируемым исходным кодом.
Avro: writer schema встречается с reader schema
Avro часто используют в потоковых и пакетных конвейерах. Бинарные данные читаются с учётом схемы, которой они были записаны, и схемы, которую ожидает потребитель.
{
"type": "record",
"name": "Message",
"fields": [
{ "name": "sender", "type": "string" },
{ "name": "text", "type": "string" }
]
}
Но «Avro поддерживает эволюцию» не означает, что структуру можно менять произвольно. Например, если в схему читателя добавили поле, он сможет прочитать старую запись, когда для отсутствующего в ней поля задано корректное значение по умолчанию. Переименование, изменение enum или несовместимый тип требуют отдельной стратегии.
В Kafka-проектах схему часто хранят в registry и проверяют политику совместимости при публикации. Сам Kafka формат payload не определяет: JSON, Avro и Protobuf для него остаются массивами байтов.
MessagePack: компактная модель без встроенного контракта
MessagePack кодирует числа, строки, массивы, maps и бинарные значения. Для системы, где уже есть JSON-подобные структуры, переход может быть технически простым.
Но встроенного механизма эволюции схем у MessagePack нет. Если map содержит строковые имена полей, они продолжают передаваться в каждом сообщении. Если заменить их позиционным массивом ради компактности, контракт станет менее прозрачным и более хрупким.
MessagePack имеет смысл после измерения, когда JSON-модель подходит, а размер или стоимость разбора действительно стали ограничением.
Что означает совместимость
Термины полезно зафиксировать в команде:
- backward compatibility: новый потребитель читает старые данные;
- forward compatibility: старый потребитель читает новые данные;
- full compatibility: поддерживаются оба направления в оговорённом диапазоне версий.
Для событий я предпочитаю additive changes: добавлять необязательные поля, не менять смысл существующих и сначала обновлять потребителей. Удаление проходит через deprecation-период, а несовместимое изменение получает новую версию события или новый тип.
Версия сама по себе не спасает. Нужны автоматическая проверка схем, набор контрактных тестов и понятная матрица поддерживаемых producer/consumer версий.
Формат события в брокере
Помимо доменного payload полезен стабильный envelope:
{
"event_id": "01J...",
"event_type": "customer.updated",
"schema_version": 3,
"occurred_at": "2026-07-17T12:00:00Z",
"producer": "customer-profile",
"payload": {}
}
Идентификатор нужен для трассировки и дедупликации, тип и версия — для маршрутизации контракта, время — для анализа задержек и предполагаемого порядка. occurred_at не доказывает глобальный порядок: часы источников могут расходиться, а события — приходить поздно. Для строгого порядка по объекту нужен согласованный sequence/version и правила обработки пропусков. Для Protobuf или Avro те же поля могут быть частью схемы без JSON-envelope.
Как я выбираю формат
- Описываю жизненный цикл данных и срок поддержки старых записей.
- Выбираю правила backward/forward compatibility.
- Проверяю языки, инструменты, registry и возможности диагностики.
- Прогоняю реальные payload через измерение.
- Закрепляю schema review и тест совместимости в CI.
Для обычного HTTP API этого часто достаточно сделать на JSON с хорошей схемой. Для gRPC естественен Protobuf. Для потоков с длительным хранением и разными версиями читателей удобен Avro. MessagePack остаётся точечным выбором, когда JSON-модель устраивает, но измерения показывают цену текстового представления.
Официальные правила совместимости стоит сверять с документацией Protocol Buffers, Apache Avro и JSON Schema.
Практика: добавляем поле и не ломаем старого читателя
Возьмём учебное событие о заявке. В первой версии есть идентификатор и статус:
{ "request_id": "r-42", "status": "accepted" }
Во второй появился необязательный канал обращения:
{ "request_id": "r-42", "status": "accepted", "channel": "web" }
На вид изменение безопасное. Но старый JSON-валидатор с запретом дополнительных полей отвергнет вторую запись. Новый потребитель с обязательным channel не прочитает первую. Поэтому слово «добавили» ещё не означает «сохранили совместимость».
| Проверка | Ожидаемое поведение по выбранному контракту |
|---|---|
| Старый читатель, новая запись | Принимает запись, игнорируя разрешённое неизвестное поле; отклонение означает несовместимость |
| Новый читатель, старая запись | Обрабатывает отсутствие канала по согласованному правилу, без выдумывания источника |
Новое значение status | Не считает неизвестное состояние успешным по умолчанию |
| Архивная запись спустя год | Читается поддерживаемой версией схемы и кода |
Я бы положил эти пары в контрактные тесты и проверял их при изменении схемы. Отдельно проверил бы семантику: нельзя незаметно заменить accepted с «получено» на «одобрено». Парсер такого нарушения не заметит, а бизнес-процесс заметит сразу.
Впервые опубликовано в моём Telegram-канале @it_underside.
Ваш ДПУПП
Похожие статьи
RAG для компании: от поиска по документам до полезного ассистента
RAG простыми словами: как устроены поиск и индексация, зачем они нужны рядом с LLM, чем отличаются решения для небольшой команды и enterprise и как получить полезного ассистента.
EventStorming: польза, ограничения и практика
Практический разбор EventStorming: какие задачи решает метод, где возникают ограничения и как подготовить сессию, чтобы команда получила полезный результат.
UML на практике: где диаграмма помогает, а где становится лишней
Как выбрать UML-диаграмму под конкретный вопрос, определить срок её жизни и не превратить моделирование в отдельный проект.