Сообщение должно пережить новую версию

  1. 01ПроизводительСмысл и схема записи
  2. 02ФорматПредставление данных
  3. 03ПотребительПоддерживаемые версии
Проверяю оба направления: новые читатели со старыми данными и наоборот.

Брокер сообщений и HTTP-сервер передают байты. Смысл этим байтам задаёт контракт между производителем и потребителем: какие поля существуют, что они означают и как меняются со временем.

Поэтому формат обмена — не соревнование «текст против бинарника». В реальной интеграции важнее четыре вопроса:

  1. Как описана схема?
  2. Какие изменения совместимы со старыми клиентами и данными?
  3. Чем диагностировать сообщение в production?
  4. Даёт ли другой формат измеримый выигрыш на нашем payload и стеке?

Краткая карта форматов

ФорматСхемаСильная сторонаОсновная цена
JSONОпционально: JSON Schema, OpenAPIПростые инструменты и читаемостьКонтракт легко оставить только «в договорённостях»
XMLОпционально: XSDЗрелые схемы, пространства имён, enterprise-стекМногословность и сложность инструментов
YAMLЗависит от задачиРучное описание конфигураций и спецификацийНеоднозначности парсеров и неудобство wire-формата
Protobuf.proto, номера полей, генерация кодаКомпактный контракт для типизированных клиентовКодогенерация и дисциплина совместимости
AvroWriter 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.

Как я выбираю формат

  1. Описываю жизненный цикл данных и срок поддержки старых записей.
  2. Выбираю правила backward/forward compatibility.
  3. Проверяю языки, инструменты, registry и возможности диагностики.
  4. Прогоняю реальные payload через измерение.
  5. Закрепляю 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.

Ваш ДПУПП

Похожие статьи