Protobuf: Schema Evolution
Field Numbers как контракт
В Protobuf field number — это единственный идентификатор поля на wire. Имя поля используется только в сгенерированном коде, на wire его нет. Это означает:
- Переименование безопасно: поменять
user_name→username— ОК, field number не меняется - Переназначение запрещено: присвоить field number 3 другому полю после удаления оригинального — нарушение контракта
- Изменение типа ограничено: wire type должен совпадать, иначе десериализация ломается
Backwards и Forwards Compatibility
Два направления совместимости:
Protobuf обеспечивает оба направления — если соблюдать правила эволюции. Ключевой механизм: wire type позволяет пропускать неизвестные поля без знания их типа.
Правила безопасной эволюции
Добавление полей
Самая частая операция. Правила:
- Выбрать новый field number (никогда ранее не использованный)
- Выбрать appropriate type и wire type
- Добавить default value (proto3: implicit zero/empty; proto2: explicit default)
Удаление полей и reserved
При удалении поля critical: зарезервировать его номер и имя. Иначе будущий разработчик может переиспользовать номер для нового поля — и старые данные на wire будут интерпретированы неправильно.
message User {
reserved 3, 7; // зарезервированные номера
reserved "email", "phone"; // зарезервированные имена
int32 id = 1;
string name = 2;
// email (3) и phone (7) удалены
int32 age = 4;
}
reserved проверяется только компилятором protoc. На wire никакой защиты нет — если кто-то обойдёт проверку и использует зарезервированный номер, данные будут corrupted. reserved — это safety net на уровне tooling, не wire-level enforcement.
Type Compatibility
Не все типы совместимы друг с другом. Ключевое ограничение — wire type должен совпадать:
Proto2 vs Proto3 vs Editions
Proto2: required — антипаттерн
required в proto2 казалось хорошей идеей: гарантировать наличие поля. На практике — бомба замедленного действия:
Бизнес-требование: email стал необязательным
Через год: email больше не обязателен в бизнес-логике. Нужно сделать optional.Google’s official recommendation: никогда не используйте required. Именно поэтому proto3 полностью убрал required. Все поля в proto3 — implicit optional. Валидация обязательности — ответственность application layer, не wire format.
Field Presence в proto3
Proto3 убрал required, но создал новую проблему: нельзя отличить «поле явно = 0» от «поле не установлено». Решение — optional keyword (вернулся в proto3 с версии 3.15):
Edition 2024
Editions — новый подход к версионированию Protobuf. Вместо syntax = "proto2" или syntax = "proto3" — edition = "2024" с настраиваемыми features:
edition = "2024";
message User {
int32 id = 1; // IMPLICIT presence (edition default)
string name = 2;
string email = 3 [features.field_presence = EXPLICIT]; // override
}
Миграция proto2/proto3 → Edition: инструмент protodelegate автоматически конвертирует .proto файлы, добавляя explicit overrides для сохранения текущего поведения. Wire format не меняется — editions влияют только на кодогенерацию.
Сценарии эволюции
Wire: [tag1=0x08, 42] [tag2=0x12, “Alice”] [tag3=0x1A, “[email protected]”] [tag4=0x20, 30]
Байтовый поток на wire: 4 пары (tag, value). Порядок не гарантирован, но обычно следует порядку field numbers.Сравнение с Avro Schema Resolution
| Аспект | Protobuf | Avro |
|---|---|---|
| Идентификация поля | Field number на wire | Имя + позиция в схеме |
| Unknown fields | Skip по wire type | Требует writer’s schema для skip |
| Добавление поля | Новый field number → default | Новое поле + default → resolution |
| Удаление поля | reserved + skip | Writer-only field → skip по writer’s schema |
| Type promotion | Ограничен одним wire type | int→long→float→double |
| Schema transport | .proto файлы распространяются отдельно | Writer’s schema в header / registry |
| Required | Deprecated (proto2 only) | default = обязателен |