Schema registry для событий: контракты, совместимость и где это окупается
Когда первый раз поднимаешь Kafka и пишешь в неё JSON, всё кажется простым. Продьюсер кладёт байты, консьюмер читает байты, согласование держится на устной договорённости. Через шесть месяцев у вас десять консьюмеров, четыре продьюсера, в схеме события поле незаметно поменяло тип, и в логах одного из потребителей лежит миллион записей с JsonParseException. Каждое нечитаемое сообщение — это либо потерянные данные, либо перезапуск с offset reset.
Schema registry — это не про модный AVRO и не про обязательное добавление лишнего сервиса. Это место, где живёт контракт между писателями и читателями, и инструмент, чтобы этот контракт нельзя было сломать незаметно. Расскажу, как я подхожу к выбору формата, как настраиваю совместимость, и какие практические ошибки регулярно встречаю.
Зачем вообще registry
Без registry схема живёт в трёх местах: в голове разработчика, в коде продьюсера и в коде каждого консьюмера. Каждое изменение требует обхода всех трёх. На малых командах это работает, на больших — нет. Регистр даёт три вещи.
- Единое место правды. Что значит поле
amountи какого оно типа — смотрят туда, не в голову коллеги. - Версионирование. Каждое изменение схемы — новая версия. Старые версии не выкидываются, читатель может вычитать старые сообщения и понять их.
- Принудительная проверка совместимости. Регистр не даёт зарегистрировать изменение, которое сломает существующих потребителей.
Третий пункт — главный. Без него регистр превращается в просмотрщик схем. С ним — в инструмент, который реально защищает от инцидентов.
Когда стоит, когда нет
Признаки, что регистр оправдан:
- В системе больше двух команд, публикующих или читающих события.
- События живут долго (хранятся неделями или дольше) и могут читаться повторно.
- Схема событий нетривиальная, и риск поломок при изменениях реален.
- Хотя бы один потребитель критичен (биллинг, аналитика, audit log).
Признаки, что без него можно обойтись:
- Один продьюсер, один консьюмер, обе команды — это два человека за соседними столами.
- События недолговечны (минуты-часы), повторное чтение не нужно.
- Транспорт — внутренний, и вы готовы пересоздавать топик при поломке.
На стартапе ставить Confluent Schema Registry «потому что так делают взрослые» — обычно перебор. Достаточно завести репозиторий с YAML/JSON-схемами и проверять их в CI.
Форматы: AVRO, Protobuf, JSON Schema
AVRO
Исторически родной формат экосистемы Kafka. Сильные стороны — компактность, наличие явной схемы в каждом сообщении (по id), хорошая поддержка эволюции. Слабая — местами громоздкий синтаксис, особенно для тех, кто не работал с ним раньше.
{
"type": "record",
"name": "OrderPlaced",
"namespace": "com.shop.orders",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "customerId", "type": "string"},
{"name": "total", "type": {"type": "long", "logicalType": "decimal", "precision": 12, "scale": 2}},
{"name": "placedAt", "type": {"type": "long", "logicalType": "timestamp-millis"}}
]
}Protobuf
Если у вас уже gRPC и есть proto-файлы, разумно использовать их же для событий. Это даёт единый источник схем для синхронных и асинхронных контрактов.
syntax = "proto3";
package com.shop.orders;
message OrderPlaced {
string order_id = 1;
string customer_id = 2;
string total = 3; // строка для decimal, или Money
int64 placed_at = 4; // unix millis
}Protobuf хорошо эволюционирует при соблюдении правил: не менять номера полей, не менять типы, не помечать существующие поля required (в proto3 их и нет). Минус — без явной поддержки registry схема в самом сообщении не лежит, нужен внешний механизм версионирования.
JSON Schema
Подойдёт, если вы уже на JSON и не хотите менять формат. Менее компактный, чуть тяжелее эволюция, но проще для людей, которые читают сообщения глазами.
Я обычно выбираю по контексту: AVRO — если стек крепко в Kafka и команда привыкла, Protobuf — если уже есть gRPC, JSON Schema — если важна читаемость в логах и не хочется новых форматов на ровном месте.
Стратегия совместимости
В Confluent Schema Registry есть несколько стратегий, и от выбора зависит, что ломается при изменениях.
- BACKWARD. Новая схема может читать данные, написанные старой. Это нужно, когда вы сначала обновляете консьюмеров, потом продьюсеров. На практике встречается чаще всего.
- FORWARD. Старая схема может читать данные, написанные новой. Полезно, когда продьюсер обновляется первым, а консьюмеры — потом.
- FULL. И то, и другое. Самая строгая, ломает многие изменения.
- NONE. Никаких проверок. Это «зачем мы вообще ставили regist».
На большинстве систем рекомендую начинать с BACKWARD. Этого достаточно, чтобы продьюсер не уничтожил консьюмеров случайным изменением, при этом схема может развиваться. Если у вас критичная цепочка с строгим порядком обновлений — рассмотрите FULL, но осознайте, что любое поле станет тяжелее менять.
Что можно и нельзя менять при BACKWARD
Без подробного цитирования спецификаций, на пальцах:
Можно: добавлять поля с дефолтами; делать поле опциональным; убирать поле, у которого был дефолт.
Нельзя: добавлять поля без дефолтов; менять тип поля; переименовывать поля (если нет alias-механизма); менять enum-значения, особенно убирать существующие.
Главное правило — изменения должны быть «накопительными». Старые сообщения должны оставаться читаемыми новыми консьюмерами без специальных ручных манипуляций.
Идентификаторы схем в сообщениях
В Kafka с Confluent Schema Registry работает простой механизм: первые байты payload — это магия и id схемы. По id консьюмер достаёт схему из registry и десериализует. Если registry недоступен — сообщения не читаются.
Это нормально, но требует мониторинга. Регистр должен быть доступен с тем же SLA, что Kafka. Часто это решается локальным кешированием схем у клиентов: один раз получили id — закешировали, дальше не ходим. Confluent-клиенты так и делают, но проверять стоит, особенно при использовании самописных клиентов.
Контрактное тестирование
Регистр сам по себе не гарантирует, что консьюмеры действительно справятся с новыми сообщениями. Он проверяет совместимость на уровне формата, но не на уровне семантики.
Поэтому я в дополнение завожу контрактные тесты на стороне консьюмера: список «золотых» сообщений (примеров), которые консьюмер должен обработать корректно. При изменении схемы продьюсер обновляет примеры, консьюмер прогоняет тесты, видит, что что-то не сходится, и поправляет код до релиза.
Это не Pact, это проще. Pact больше про синхронные контракты. Для событий обычно достаточно фикстур и интеграционных тестов вокруг них.
Эволюция через события деления и слияния
Иногда нужно изменить схему сильнее, чем позволяет BACKWARD. Например, было одно событие OrderUpdated с десятью полями, нужно разбить на OrderItemAdded и OrderShippingAddressChanged. Прямое изменение сломает совместимость.
Рабочий приём — переходный период с двумя событиями. Сначала продьюсер публикует и старое, и новое. Консьюмеры по очереди переходят на новое. Когда все перешли, старое снимается. Это требует месяцев, не дней, и явного владельца, который следит, что переход реально завершён.
Альтернатива — топик-конвертер: отдельный сервис читает старый топик, преобразует, кладёт в новый. Используется, когда продьюсер изменить нельзя (легаси), а потребителям нужен новый формат.
Регистр и owner-ship
Регистр без явного владения схемами — это helpdesk, в который никто не звонит. Я закрепляю каждую схему за командой, и любое изменение подразумевает review со стороны команды-владельца. Это не бюрократия, это нормальная инженерная практика, такая же, как code review.
В Confluent Schema Registry это технически делается через subject-naming strategy и права доступа. Но даже без специальных средств достаточно соглашения «схема X живёт в репозитории команды Y, изменения — через PR».
Чек-лист при внедрении
- Выбрана стратегия совместимости (обычно BACKWARD), и команда её понимает.
- Каждая схема имеет владельца и живёт в одном репозитории, а не в десяти.
- Изменения схем проходят через CI, который проверяет совместимость до публикации.
- У потребителей есть локальный кеш схем, чтобы недоступность регистра не валила прод.
- Есть мониторинг ошибок десериализации (по типу exception) — это первый сигнал поломки контракта.
- Документированы поля: что значит, какие диапазоны, единицы измерения. В schema-формате — через doc/description.
Что запомнить
Schema registry — это инфраструктура для контрактов между сервисами, не модный аксессуар к Kafka. Окупается, когда команд и потребителей событий больше, чем легко удержать в голове, и когда события долгоживущие. На малых системах достаточно репозитория со схемами и CI-проверки. На больших — без регистра вы рано или поздно получите цепочку инцидентов вокруг «опять кто-то поменял формат», и каждый такой инцидент стоит больше, чем недели на правильную настройку.