Learning Platform
Глоссарий Troubleshooting
Урок 05.04 · 30 мин
Продвинутый
AvroSchema ResolutionSchema EvolutionResolvingDecoderCompatibilityType PromotionDefault Values

Schema Resolution и эволюция

Writer schema vs reader schema

В Protobuf и Thrift каждое поле имеет числовой tag — ридер сопоставляет поля по tag. В Avro тегов нет: поля записи — просто байты в порядке объявления. Как тогда ридер интерпретирует данные, если его схема отличается от схемы записи?

Ответ: schema resolution. При десериализации Avro работает с двумя схемами одновременно:

  • Writer schema — схема, с которой данные были записаны. В OCF хранится в header (avro.schema), в Kafka — в Schema Registry
  • Reader schema — схема, которую использует читающее приложение. Определяет, какие поля нужны и какого типа

ResolvingDecoder принимает обе схемы и строит план преобразования: какие поля прочитать из потока, какие пропустить, какие заполнить значениями по умолчанию.

Writer Schema → ResolvingDecoder → Reader Schema
Writer SchemaСхема, с которой данные были записаны. Определяет порядок и типы полей в бинарном потоке. Фиксирована — изменить нельзя.

ResolvingDecoder

Сопоставляет поля writer и reader по имени (с учётом aliases). Строит план: read, skip, default, promote. Ошибка — если нет совместимого плана.
Reader SchemaСхема читающего приложения. Определяет, какие поля нужны, в каком порядке и какого типа. Может отличаться от writer schema.
Результат ResolutionДля каждого поля reader schema — конкретное действие: прочитать из потока (read), подставить default, выполнить type promotion, или ошибка несовместимости.
TIP

Schema resolution — это compile-time операция (выполняется один раз при создании ридера), а не runtime. ResolvingDecoder строит план чтения из пары схем, затем применяет его к каждой записи без повторного анализа. Стоимость resolution не зависит от количества записей.

Алгоритм ResolvingDecoder

ResolvingDecoder сопоставляет поля writer schema и reader schema по имени, а не по позиции или тегу. Алгоритм для record:

  1. Для каждого поля writer schema: найти поле в reader schema с тем же именем или alias
  2. Если найдено — проверить совместимость типов. Если совместимы — включить в план чтения (с type promotion при необходимости)
  3. Если не найдено — включить в план пропуска (skip). Байты будут прочитаны из потока и отброшены
  4. Для каждого поля reader schema, отсутствующего в writer: подставить значение по умолчанию (default). Если default нет — ошибка
Алгоритм Resolution для record

Вход: writer schema + reader schema

На вход: writer schema (определяет бинарный поток) и reader schema (определяет результат). Оба — record с полями.

Для каждого writer field → поиск в reader по name/alias

Для каждого поля writer schema ищем совпадение по name в reader schema. Если у reader-поля есть aliases — проверяем и их.
Match найденТипы writer и reader совместимы → добавить в план чтения. Если типы разные, но promotable (int→long) — добавить promotion step.
Match не найденПоле writer schema отсутствует в reader schema. Байты нужно прочитать из потока и отбросить — пропустить нельзя без декодирования (variable-length encoding).
Reader field без парыПоле reader schema отсутствует в writer schema. Если есть default — подставить. Если default нет — schema resolution fails, данные нечитаемы этим reader.
WARNING

Пропуск поля (skip) — не бесплатная операция. Avro binary encoding использует variable-length encoding, поэтому нельзя просто «перепрыгнуть» N байт — нужно полностью декодировать значение, чтобы узнать его размер. Skip для вложенного record или массива с 10000 элементов — это полное декодирование без сохранения результата.

Добавление и удаление полей

Добавление поля

Writer записал данные без поля email. Reader ожидает email. Resolution:

  • Reader field email не найден в writer schema
  • Если email имеет "default": "" — подставляется пустая строка для каждой записи
  • Если default отсутствует — ошибка: данные нечитаемы этим reader

Правило: новое поле в reader обязано иметь default. Без default ридер не сможет прочитать старые данные.

Удаление поля

Writer записал данные с полем age. Reader не ожидает age. Resolution:

  • Writer field age не найден в reader schema
  • ResolvingDecoder добавляет skip: декодирует int из потока и отбрасывает
  • Данные успешно читаются — age просто игнорируется

Правило: удалённое поле в writer должно было иметь default (для обратной совместимости — чтобы старые ридеры могли читать новые данные без этого поля).

// Добавление: writer v1, reader v2
Writer: {id, name} → бинарный поток: [id_bytes][name_bytes]
Reader: {id, name, email} → id: read, name: read, email: default("")

// Удаление: writer v2, reader v1
Writer: {id, name, email} → бинарный поток: [id_bytes][name_bytes][email_bytes]
Reader: {id, name} → id: read, name: read, email: skip
NOTE

В Protobuf добавление поля с новым tag безусловно безопасно — ридер пропускает неизвестные теги. В Avro безопасность зависит от default: без default новое поле ломает чтение старых данных. Это более строгий контракт, но он явный — ошибка обнаруживается при resolution, а не при доступе к полю.

Type Promotions

Avro spec определяет ограниченный набор допустимых преобразований типов. Если writer написал int, а reader ожидает long — ResolvingDecoder автоматически выполнит promotion:

Цепочка Type Promotions

int

32-bit signed integer, zigzag + variable-length encoding. Диапазон: -2^31 до 2^31-1.

long

64-bit signed integer, zigzag + variable-length encoding. Расширение без потерь: каждый int — валидный long.

float

32-bit IEEE 754 float. Conversion из int/long может терять точность для больших значений (float имеет только 24 бита мантиссы).

double

64-bit IEEE 754 double. Conversion из float без потерь. Из int — без потерь (52 бита мантиссы > 32 бита int). Из long — возможна потеря точности.

bytes

Произвольные байты (length-prefixed). Бинарные данные без интерпретации.

string

UTF-8 строка (length-prefixed). Promotion из bytes: интерпретировать байты как UTF-8. Обратное тоже допустимо.

Правила promotion:

Writer typeДопустимые Reader types
intlong, float, double
longfloat, double
floatdouble
stringbytes
bytesstring
WARNING

Promotion intfloat и longfloat/double технически с потерей точности. Значение 2147483647 (max int) в float станет 2147483648.0. Avro spec допускает это, но в приложении нужно быть осторожным при promotion числовых типов через float. Цепочка int → long → double безопаснее, чем int → float → double.

Union Resolution

Union — особый случай. Writer schema ["null", "string"] и reader schema ["null", "string", "int"] — как работает resolution?

Правило: для каждого типа в writer union Avro ищет первый совместимый тип в reader union. Если writer записал union index = 1 (string), ResolvingDecoder ищет string (или promotable) тип в reader union.

Writer union: ["null", "string"]
Reader union: ["null", "string", "int"]

Writer index 0 (null) → Reader index 0 (null) + match
Writer index 1 (string) → Reader index 1 (string) + match

Порядок типов в union меняться может — resolution по типам, не по индексам. Но добавлять тип в union — это не backward-compatible: старый ридер с ["null", "string"] не сможет декодировать запись, если writer записал index = 2 (int).

Aliases и совместимость имён

Record fields и named types (record, enum, fixed) могут иметь aliases — альтернативные имена для resolution:

{
 "type": "record",
 "name": "UserProfile",
 "aliases": ["User", "Account"],
 "fields": [
 {"name": "userId", "type": "long", "aliases": ["id", "user_id"]}
 ]
}

ResolvingDecoder сначала ищет поле по name, затем по aliases. Это позволяет переименовывать поля без потери backward compatibility: старое имя остаётся как alias.

TIP

Aliases — односторонний механизм: они используются только при resolution (ридер ищет writer-поле по reader aliases). Alias не меняет имя поля в результате — reader видит поле под своим именем. Это не rename в SQL смысле, а mapping при десериализации.

Backward, Forward, Full Compatibility

Совместимость определяется направлением schema resolution:

Типы совместимости
BACKWARDНовая схема (reader) может читать данные, записанные старой схемой (writer). Consumer обновляется первым. Default compatibility в Confluent Schema Registry.
FORWARDСтарая схема (reader) может читать данные, записанные новой схемой (writer). Producer обновляется первым. Менее распространён.
FULLОбе направления: новый reader читает старые данные И старый reader читает новые данные. Самый строгий режим. Все поля должны иметь default.
ОперацияBACKWARDFORWARDFULL
Добавить поле с default (только с default)
Добавить поле без default
Удалить поле с default
Удалить поле без default
Rename поля (с alias)
Type promotion (int→long)
NOTE

BACKWARD — default в Confluent Schema Registry. Это значит: consumer (reader) обновляется первым, producer (writer) — вторым. Новый consumer уже умеет читать старые данные, когда producer начинает писать по новой схеме. Подробнее о TRANSITIVE вариантах и Schema Registry — в модуле 08.

Почему Avro не нужны field tags

В Protobuf и Thrift каждое поле имеет числовой tag, который включается в wire format:

// Protobuf: tag + type + value
field 1: [tag=1, type=varint] [value: 42]
field 2: [tag=2, type=LEN] [length: 5] [value: "Alice"]

// Avro: только values, порядок из схемы
[value: 42] [value: "Alice"]

Protobuf тратит 1-2 байта на tag каждого поля. Avro — 0 байт. На записи с 20 полями и миллиардом строк это экономит гигабайты. Цена — зависимость от schema resolution: без writer schema бинарный поток нечитаем.

Avro вместо тегов использует:

  1. Writer schema — определяет порядок байт в потоке
  2. ResolvingDecoder — маппит writer fields на reader fields по имени
  3. Aliases — поддерживает переименование полей
  4. Defaults — поддерживает добавление полей

Это архитектурный выбор: компактность wire format за счёт обязательного наличия схемы. Для Kafka (схема в Registry) и Hadoop (схема в OCF header) — идеальный trade-off. Для RPC между микросервисами (где schema versioning сложнее) — Protobuf с тегами удобнее.

Ключевые выводы

  1. Schema resolution — сопоставление writer и reader schema по именам полей. Выполняется однократно, результат — план чтения
  2. ResolvingDecoder обрабатывает три случая: read (поле есть в обоих), skip (только в writer), default (только в reader)
  3. Добавление поля требует default в reader schema. Удаление — writer записывает байты, reader пропускает их (skip)
  4. Type promotions ограничены: int→long→float→double и bytes↔string. Остальные преобразования запрещены
  5. BACKWARD compatibility (default): новый reader читает старые данные. FORWARD — наоборот. FULL — оба направления
  6. Отсутствие field tags — главное архитектурное отличие от Protobuf/Thrift. Экономит место, но требует schema для десериализации
Debezium: schema evolution в CDC pipelines

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Writer schema: {id (long), name (string), age (int)}. Reader schema: {id (long), name (string), email (string, default='')}. Какие действия выполнит ResolvingDecoder?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6