Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 30 мин
Средний
Schema EvolutionField IdentityBreaking ChangesSchema-on-ReadSchema-on-WriteAvroProtobufParquetORC

Основы эволюции схем

Данные живут годами. Схемы меняются каждый спринт. Между первой версией User с тремя полями и текущей с пятьюдесятью — десятки миграций, каждая из которых могла сломать downstream consumers. Schema evolution — это набор правил и механизмов, которые позволяют менять схему без остановки и без data rewrite.

В модуле 04 мы разобрали Avro ResolvingDecoder — writer schema vs reader schema, правила добавления и удаления полей, type promotions. В модуле 05 — Protobuf field numbers как wire identity contract. Теперь соберём всё в единую картину: как каждый формат решает проблему эволюции, чем отличаются их механизмы, и какие ограничения у каждого подхода.

Фундаментальная проблема: идентификация полей

Когда ридер получает бинарные данные, он должен понять: какой байт относится к какому полю. Механизм этой привязки — ключевое архитектурное решение формата. Именно он определяет, какие изменения схемы безопасны, а какие ломают совместимость.

Четыре механизма идентификации полей в форматах курса:

Механизмы идентификации полей: имена vs ID vs позиции
ФорматФормат сериализации или хранения данных
МеханизмКак формат привязывает байты к полям — от этого зависят все правила эволюции
Сильная сторонаГлавное преимущество выбранного механизма
ОграничениеГлавное ограничение, вытекающее из механизма
AvroApache Avro — row-based формат с обязательной схемой. Writer schema хранится рядом с данными (в OCF header или Schema Registry).
Имена полейResolvingDecoder сопоставляет writer и reader по имени поля (+ aliases). Порядок полей не имеет значения — важно совпадение имён.
Переименование через aliasesПоле можно 'переименовать': добавить alias со старым именем. Ридер найдёт поле по любому из имён.
Нет позиционной оптимизацииКаждое поле ищется по имени — нельзя просто пропустить N байт. На практике Avro читает поля последовательно, поэтому overhead невелик.
ProtobufProtocol Buffers — бинарный формат с field numbers в wire format. Каждое поле кодируется как (field_number, wire_type, value).
Числовые field numbersКаждое поле имеет уникальный числовой ID, записанный в wire format. Ридер ищет поля по ID, неизвестные — пропускает. Имена существуют только в .proto файле.
Переименование бесплатноИмя поля в .proto — только для сгенерированного кода. На уровне wire format имён нет. Можно переименовать поле, не трогая данные.
Номер навечноУдалённый field number нельзя использовать повторно — старые данные с этим номером будут интерпретированы неправильно. Нужен reserved.
ParquetApache Parquet — колоночный формат. Метаданные хранят имена и схему, но до Iceberg/Delta позиционная привязка была основной.
Позиция + именаParquet хранит имена колонок в schema метаданных. Но добавление колонки безопасно только в конец — старые файлы не знают о новых позициях. Iceberg решает это через field IDs.
Колоночный доступПроекция (чтение подмножества колонок) — нативная операция. Ненужные column chunks просто не читаются с диска.
Только добавление в конецНативно: новые колонки — только в конец. Переименование, удаление, reorder — без table format (Iceberg/Delta) невозможны без data rewrite.
ORCApache ORC — колоночный формат, оптимизированный для Hive. Привязка полей по позиции в struct.
Позиция в structORC идентифицирует колонки по позиции в schema tree. Column 0 = первая колонка. Добавление — только в конец, как в Parquet.
ПростотаНет overhead на хранение ID или поиск по имени. Позиция = offset, всё детерминировано.
Самый жёсткийПереименование, удаление, reorder — невозможны без перезаписи. Даже Hive 3.x ACID добавляет эволюцию через metadata overlay, не нативно.

Механизм идентификации — не техническая деталь, а архитектурный контракт. Avro привязывает байты к именам — поэтому rename требует aliases. Protobuf привязывает к числовым ID — rename бесплатен, но удалённый номер забронирован навечно. Parquet и ORC полагаются на позицию — безопасно только добавлять в конец.

TIP

Iceberg решает позиционную проблему Parquet, добавляя собственный слой field IDs поверх Parquet-файлов. Каждая колонка получает уникальный ID, и Iceberg сопоставляет по ID, а не по позиции — это разблокирует rename, drop, reorder без data rewrite. Подробнее — в модуле 12.

Breaking vs non-breaking: таксономия изменений

Не все изменения схемы равноценны. Одни безопасны для всех consumers, другие сломают downstream в ту же секунду. Таксономия делит изменения на три категории:

Non-breaking (безопасные) — существующие consumers продолжают работать без изменений. Примеры: добавление поля с default, расширение типа (int → long).

Conditional (условно безопасные) — безопасны при соблюдении условий (порядок деплоя, наличие defaults). Примеры: удаление поля (безопасно для FORWARD, опасно для BACKWARD без координации).

Breaking (ломающие) — сломают хотя бы одну сторону (reader или writer) при любом порядке деплоя. Примеры: смена типа (string → int), удаление required поля без default.

Таксономия изменений схемы по форматам
ОперацияТип изменения схемы
AvroAvro: идентификация по именам, ResolvingDecoder
ProtobufProtobuf: идентификация по field numbers
ParquetParquet: позиционная привязка (нативно)
ORCORC: позиционная привязка в struct
Добавить полеДобавление нового поля в схему
Да с defaultAvro: новое поле должно иметь default. Старый writer его не пишет — reader подставит default через ResolvingDecoder.
Да всегдаProtobuf: новый field number — старый ридер просто пропускает неизвестные поля. Default не нужен на уровне wire format.
Да в конецParquet: новая колонка добавляется в конец схемы. Старые файлы не содержат эту колонку — ридер получает NULL.
Да в конецORC: аналогично Parquet — только добавление в конец.
Удалить полеУдаление существующего поля из схемы
! conditionalAvro: удалённое поле в reader schema пропускается. Но если writer ожидает поле — нужен default. Зависит от направления совместимости (BACKWARD vs FORWARD).
! reservedProtobuf: поле можно перестать использовать, но field number нужно пометить reserved — иначе будущий разработчик может переиспользовать номер с другим типом.
Нет нативноParquet: нативно колонку не удалить — она останется в файле. Table format (Iceberg/Delta) может скрыть её через metadata, но физически данные на диске.
Нет нативноORC: аналогично Parquet — нативного удаления нет.
ПереименоватьСмена имени поля без изменения типа и данных
Да aliasesAvro: добавить alias со старым именем. ResolvingDecoder найдёт поле по alias. Фактически — не rename, а добавление альтернативного имени.
Да бесплатноProtobuf: имя не участвует в wire format. Смена имени в .proto ничего не ломает — только пересгенерировать код.
Нет нативноParquet: имя колонки записано в metadata. Rename = rewrite всех файлов (или column mapping в Iceberg/Delta).
Нет нативноORC: позиционная привязка делает rename бессмысленным — имя не используется для matching. Metadata-only rename через Hive ALTER TABLE.
Изменить типСмена типа поля (widening или narrowing)
! promotionAvro: поддерживает type promotion (int→long, float→double). Обратное сужение (long→int) — ошибка. Union добавление — conditional.
! limitedProtobuf: int32↔int64 совместимы (varint encoding). fixed32↔fixed64 — нет. string↔bytes — совместимы. Enum — опасная территория.
Нет обычноParquet: тип колонки зафиксирован при создании. Некоторые table formats поддерживают type widening (Delta, Iceberg), но нативно Parquet — нет.
НетORC: смена типа невозможна без полной перезаписи.
Reorder полейИзменение порядка полей в схеме
Да свободноAvro: ResolvingDecoder сопоставляет по имени — порядок в reader schema может быть любым. Writer schema определяет порядок байт.
Да свободноProtobuf: field numbers на проволоке не зависят от порядка в .proto файле. Ридер ищет по номеру.
Нет нативноParquet: порядок колонок определяет schema tree. Reorder нативно невозможен.
НетORC: порядок = identity. Reorder = перезапись.

Паттерн ясен: форматы с именной или числовой идентификацией (Avro, Protobuf) значительно гибче позиционных (Parquet, ORC). Но позиционные форматы компенсируют это через table formats (Iceberg, Delta Lake), которые добавляют слой метаданных поверх файлов.

Schema-on-Read vs Schema-on-Write

Два фундаментально разных подхода к тому, когда применяется схема:

Schema-on-Write vs Schema-on-Read

Schema-on-Write

Schema-on-Write: данные проверяются на соответствие схеме ДО записи. Если поле неправильного типа или missing required field — запись отклоняется. Гарантия: все данные на диске валидны.
ПринципДанные валидируются при записи. Хранилище гарантирует, что каждая запись соответствует текущей схеме. Мусор не попадает на диск.
ПримерыRDBMS (PostgreSQL, MySQL) — классический schema-on-write: INSERT с неправильным типом → ошибка. Delta Lake (schema enforcement) — аналог для data lake.

Schema-on-Read

Schema-on-Read: данные записываются as-is, без валидации. Схема применяется при чтении — ридер интерпретирует байты по своей схеме. Невалидные данные обнаруживаются только при чтении.
ПринципДанные записываются без проверки. Ридер сам решает, как интерпретировать. Гибкость максимальна, но мусор на диске возможен.
ПримерыAvro (ResolvingDecoder — две схемы при чтении), JSON/CSV (парсер при чтении решает, как интерпретировать), Hive (schema overlay при SELECT).
Реальность: гибридные подходыВ production обычно комбинация: schema enforcement при записи (catch мусор) + schema evolution при чтении (backward/forward compatibility). Iceberg, Delta Lake, Schema Registry — все используют гибрид.

Почему это важно для эволюции

Schema-on-write определяет что можно записать — и блокирует некоторые изменения. Если колонка age имеет тип INT в Delta Lake, запись строки "unknown" отклоняется. Менять тип — только через ALTER или overwriteSchema.

Schema-on-read определяет что можно прочитать — и обеспечивает гибкость. Avro ResolvingDecoder позволяет reader schema отличаться от writer schema — fields добавляются и удаляются прозрачно.

NOTE

Schema-on-write ≠ строгость, schema-on-read ≠ хаос. Оба подхода работают, если есть governance: правила совместимости, compatibility checks, CI/CD валидация схем до деплоя. Schema Registry — именно такой governance слой для schema-on-read систем.

Матрица эволюционных возможностей

Финальное сравнение — какой формат что может, и на каком уровне (нативно vs через table format):

Матрица эволюционных возможностей форматов
ВозможностьОперация schema evolution
AvroRow-based, schema-on-read
ProtobufRow-based, field numbers
ParquetColumnar, positional
ORCColumnar, positional
IcebergTable format поверх Parquet/ORC/Avro
Add fieldДобавление нового поля
ДаС default value
ДаВсегда безопасно — новый field number
ДаТолько в конец схемы
ДаТолько в конец struct
ДаВ любую позицию — field ID tracking
Remove fieldУдаление существующего поля
!Conditional — зависит от compatibility level
!Нужен reserved — иначе номер может быть переиспользован
НетНативно невозможно
НетНативно невозможно
ДаMetadata-only drop — файлы не перезаписываются
RenameПереименование поля
ДаЧерез aliases
ДаИмя не в wire format
НетИмя в metadata
НетПозиционная привязка
ДаField ID не меняется при rename
ReorderИзменение порядка полей
ДаПо именам — порядок не важен
ДаПо ID — порядок не важен
НетПозиционная привязка
НетПозиционная привязка
ДаField ID отвязывает от позиции
Type widenРасширение типа (int→long)
Даint→long, float→double и др.
!Только wire-compatible пары
НетТип фиксирован
НетТип фиксирован
Даint→long без data rewrite
  • Parquet нативно, без table format. † Iceberg добавляет metadata layer поверх Parquet/ORC.

Главный вывод: row-based форматы (Avro, Protobuf) нативно поддерживают большинство эволюционных операций. Columnar форматы (Parquet, ORC) жёстко ограничены — но table formats (Iceberg, Delta Lake) компенсируют это через metadata overlay.

Стоимость эволюции: metadata-only vs data rewrite

Не все поддерживаемые операции стоят одинаково:

Metadata-only — изменение только метаданных, файлы не перезаписываются. В Iceberg: add, drop, rename, reorder, type widen — все metadata-only. В Avro: add/remove с default — metadata (новая reader schema). Стоимость: O(1) — мгновенно.

Data rewrite — физическая перезапись всех файлов. В Parquet/ORC без table format: любое изменение = rewrite. В Delta Lake: overwriteSchema перезаписывает файлы. Стоимость: O(N) где N — объём данных. Для петабайтных таблиц — часы или дни.

Wire-compatible — Protobuf-специфичная концепция. Изменение .proto файла, но wire format одинаковый. Пример: int32 и int64 оба используют varint encoding — ридер, ожидающий int64, корректно прочтёт int32 value. Стоимость: O(0) — ничего менять не нужно, данные уже совместимы.

WARNING

Metadata-only операции мгновенны, но имеют скрытую стоимость: query time overhead. Iceberg при чтении старых файлов с удалённой колонкой должен проверить, что колонка отсутствует, и вернуть NULL. Для миллионов файлов это дополнительная нагрузка на metadata parsing. Partition evolution в Iceberg — metadata-only, но query planner должен учитывать два partition spec одновременно.

Dual-schema upgrade pattern

Безопасная стратегия развёртывания изменений схемы в распределённых системах с множеством producers и consumers:

  1. Новая схема с backward compatibility — зарегистрировать новую версию, совместимую со старой. Новые поля имеют defaults.
  2. Consumer обновляется первым — новый consumer может читать и старые, и новые данные (backward compatibility).
  3. Producer обновляется вторым — когда все consumers готовы, producer начинает писать по новой схеме.
  4. Cleanup (опционально) — после полной миграции убрать deprecated поля в следующем цикле.

Этот паттерн требует координации деплоя и compatibility validation — оба обеспечиваются Schema Registry, который мы разберём в уроке 03.

Что дальше

Мы увидели, что разные форматы поддерживают разный набор эволюционных операций. Но “поддерживает” — это ещё не “безопасно в production”. Уровни совместимости (compatibility levels) формализуют, какие изменения допустимы в конкретном deployment scenario. В следующем уроке мы разберём все 7 уровней: BACKWARD, FORWARD, FULL, их TRANSITIVE варианты, и NONE — с конкретными правилами для каждого формата.

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

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

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

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