lenec ru

← все посты

Schema registry для событий: контракты, совместимость и где это окупается

19K

Когда первый раз поднимаешь 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-проверки. На больших — без регистра вы рано или поздно получите цепочку инцидентов вокруг «опять кто-то поменял формат», и каждый такой инцидент стоит больше, чем недели на правильную настройку.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.