Система типов и схема
Схема — центральная концепция Avro
В Avro схема определяет всё: как данные сериализуются, десериализуются, и как работает эволюция. В отличие от Protobuf и Thrift, где схема компилируется в код заранее, Avro-схема — JSON-документ, который можно обрабатывать динамически. Это позволяет строить generic tools (ридеры, конвертеры, валидаторы), которые не знают конкретную схему на этапе компиляции.
Два формата описания схем:
- JSON — основной, machine-readable формат. Все Avro-инструменты работают с JSON-схемами
- Avro IDL — человекочитаемый, C-подобный синтаксис. Компилируется в JSON-схему
Примитивные типы
Avro определяет 8 примитивных типов. Каждый тип имеет фиксированное бинарное представление:
| Тип | Размер в binary encoding | Описание |
|---|---|---|
null | 0 байт | Отсутствие значения. Ничего не пишется. |
boolean | 1 байт | 0x00 = false, 0x01 = true |
int | 1–5 байт | 32-bit signed, zigzag + variable-length |
long | 1–10 байт | 64-bit signed, zigzag + variable-length |
float | 4 байта | IEEE 754 single-precision, little-endian |
double | 8 байт | IEEE 754 double-precision, little-endian |
bytes | variable | length (Avro long) + raw bytes |
string | variable | length (Avro long) + UTF-8 bytes |
Примитивные типы в JSON-схеме записываются как строки: "null", "int", "string". Но могут быть записаны и как объекты: {"type": "int"}. Вторая форма нужна для добавления логических типов — {"type": "int", "logicalType": "date"}. Обе формы эквивалентны для базовых типов.
Сложные типы
Avro определяет 6 сложных типов. Каждый описывается JSON-объектом со свойством "type":
Record
Именованный набор полей — основной тип для структурированных данных. Аналог struct в C, class в Java, message в Protobuf:
{
"type": "record",
"name": "User",
"namespace": "com.example.avro",
"doc": "A user record",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null},
{"name": "age", "type": "int", "default": 0}
]
}
Каждое поле имеет: name (обязательно), type (обязательно), default (опционально — критично для эволюции), doc, order, aliases.
Enum
Фиксированный набор символьных имён. Значение кодируется как 0-based index:
{
"type": "enum",
"name": "Status",
"symbols": ["ACTIVE", "INACTIVE", "DELETED"],
"default": "ACTIVE"
}
ACTIVE = 0, INACTIVE = 1, DELETED = 2 на wire. Поле default (с Avro 1.9) используется при schema evolution — если ридер встречает неизвестный символ, подставляется default.
Array
Упорядоченная коллекция элементов одного типа. Кодируется как серия blocks:
{
"type": "array",
"items": "string"
}
Map
Набор пар key-value. Ключ всегда string. Значение — произвольный тип:
{
"type": "map",
"values": "long"
}
Ключи Avro map — строго string. Нельзя создать map<int, string> или map<bytes, long>. Это ограничение спецификации (JSON-ключи — всегда строки). Если нужна map с нестроковым ключом — используйте array of records.
Union
Тип, который может быть одним из перечисленных типов. Описывается JSON-массивом:
["null", "string"]
Это union из null и string — аналог Optional<String>. На wire: индекс типа (0 = null, 1 = string) + значение. Union не может содержать другой union (нет вложенных union) и не может содержать два типа с одним именем.
Fixed
Фиксированное количество байт. Аналог byte[N], записывается без length prefix:
{
"type": "fixed",
"name": "MD5",
"size": 16
}
Примитивные (8)
int, long (variable-length zigzag)
Числовые типы с variable-length encoding. int — 32-bit zigzag (1–5 байт), long — 64-bit zigzag (1–10 байт). Экономичны для малых значений.float, double (fixed IEEE 754)
IEEE 754 floating point, little-endian. float — 4 байта, double — 8 байт. Фиксированный размер, в отличие от int/long.null, boolean, bytes, string
boolean — 1 байт. null — 0 байт. bytes — length + raw. string — length + UTF-8. Четыре типа с разными стратегиями кодирования.Сложные (6)
record, enum, fixed (named)
Named types — имеют имя и опционально namespace. Могут быть referenced по имени из других типов. Record, Enum, Fixed — всё named.array, map (containers)
Container types — содержат элементы другого типа. Array — упорядоченная коллекция. Map — key(string)-value пары. Оба используют block encoding.union (tagged choice)
Union — JSON-массив типов. На wire: index + value. Не может содержать другой union. Каноническое использование: ['null', 'type'] для optional полей.Named Types и Namespaces
Три типа в Avro являются named: record, enum, fixed. Они имеют:
name— обязательное имя типаnamespace— опциональное пространство имён (Java-style:com.example.avro)aliases— список альтернативных имён для schema evolutiondoc— документация
Полное имя (fullname) = namespace.name. Если namespace не указан, наследуется от ближайшего enclosing named type:
{
"type": "record",
"name": "Event",
"namespace": "com.example",
"fields": [
{
"name": "header",
"type": {
"type": "record",
"name": "Header",
"fields": [
{"name": "timestamp", "type": "long"},
{"name": "source", "type": "string"}
]
}
}
]
}
Здесь Header наследует namespace com.example → fullname = com.example.Header. Вложенный record можно ссылать по fullname из других типов: "type": "com.example.Header".
Aliases позволяют переименовывать типы и поля при schema evolution:
{
"type": "record",
"name": "UserEvent",
"aliases": ["UserAction", "com.legacy.UserEvent"],
"fields": [
{"name": "userId", "type": "long", "aliases": ["user_id", "uid"]}
]
}
Ридер со схемой UserAction успешно прочитает данные, записанные со схемой UserEvent — alias разрешается при schema resolution.
Avro IDL
JSON-схемы verbose — Avro IDL предоставляет человекочитаемую альтернативу с C-подобным синтаксисом:
@namespace("com.example.avro")
protocol UserProtocol {
enum Status {
ACTIVE, INACTIVE, DELETED
}
record User {
long id;
string name;
union { null, string } email = null;
int age = 0;
Status status = "ACTIVE";
array<string> tags = [];
}
record Event {
long timestamp;
string type;
union { null, User } user = null;
}
}
IDL компилируется в JSON-схему утилитой avro-tools:
java -jar avro-tools.jar idl2schemata user.avdl output/
IDL удобен для human review и version control (чистые диффы). Но runtime-инструменты (Schema Registry, Kafka serializers, Avro readers) работают только с JSON-схемами. IDL — это authoring format, не wire format.
Логические типы
Логические типы — семантическая надстройка над примитивными и fixed типами. Физическое представление одно и то же, но ридер интерпретирует значение по-другому:
Пример JSON-схемы с логическими типами:
{
"type": "record",
"name": "Transaction",
"fields": [
{
"name": "amount",
"type": {
"type": "bytes",
"logicalType": "decimal",
"precision": 10,
"scale": 2
}
},
{
"name": "created_at",
"type": {"type": "long", "logicalType": "timestamp-millis"}
},
{
"name": "date",
"type": {"type": "int", "logicalType": "date"}
},
{
"name": "id",
"type": {"type": "string", "logicalType": "uuid"}
}
]
}
Логический тип duration использует fixed из 12 байт — три 32-bit unsigned little-endian значения: months, days, milliseconds. Это не стандартный Unix duration — месяцы и дни хранятся отдельно, потому что их длина непостоянна. Модель аналогична ISO 8601 duration P1M2DT3H → months=1, days=2, millis=10800000.
Каноническая форма схемы
Avro определяет Parsing Canonical Form (PCF) — нормализованное JSON-представление схемы, используемое для вычисления fingerprint:
Правила нормализации:
- Удалить все свойства кроме
type,name,fields,symbols,items,values,size - Из полей record оставить только
nameиtype(убратьdefault,doc,aliases,order) - Заменить именованные типы на fullname при повторном использовании
- Убрать пробелы
Fingerprint вычисляется как 64-bit Rabin hash от PCF — используется в Single Object Encoding (урок 05) для идентификации схемы по 8-байтовому значению.
Сравнение с другими системами типов
| Аспект | Avro | Protobuf | Thrift |
|---|---|---|---|
| Описание схемы | JSON / IDL | .proto файлы | .thrift файлы |
| Кодогенерация | Опциональная | Обязательная | Обязательная |
| Идентификация полей | По имени + позиции | По числовому tag | По числовому ID |
| Nullable | union с null | optional + wrapper types | optional |
| Map key types | Только string | int32, int64, string, bool | Любой тип |
| Наследование | |||
| Default values | В JSON-схеме | В .proto файле | В .thrift файле |
| Эволюция | По имени поля + defaults | По field tag | По field ID |
Ключевые выводы
- 8 примитивных типов: null, boolean, int, long, float, double, bytes, string — каждый с фиксированным binary encoding
- 6 сложных типов: record (named struct), enum (indexed symbols), array (block-encoded list), map (string keys only), union (tagged choice), fixed (byte array)
- Named types (record, enum, fixed) имеют namespaces и aliases для schema evolution
- Avro IDL — человекочитаемая альтернатива JSON-схемам, компилируется в JSON
- Логические типы — семантическая надстройка: decimal, date, timestamps, uuid. Физический тип не меняется
- Parsing Canonical Form — нормализованная схема для вычисления 64-bit Rabin fingerprint