Бинарное кодирование
Принцип: минимум overhead
Avro binary encoding оптимизировано для компактности — ни один байт не тратится на идентификацию типа или поля. Ридер должен знать схему, чтобы интерпретировать байты. Без схемы поток байт бессмыслен — это ключевое отличие от self-describing форматов (JSON, MessagePack) и tagged форматов (Protobuf, Thrift).
Следствие: Avro encoding не содержит field names, field tags, type markers и separators. Record — это конкатенация закодированных значений полей в порядке объявления. Массив — серия блоков элементов. Union — index + значение. Простота encoding компенсируется зависимостью от схемы.
Целые числа: zigzag + variable-length
Типы int (32-bit) и long (64-bit) кодируются двумя шагами:
Шаг 1: Zigzag encoding — маппит signed → unsigned, помещая маленькие значения (по модулю) в маленькие unsigned:
| Signed | Unsigned (zigzag) |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| 2 | 4 |
| … | … |
Формула: (n << 1) ^ (n >> 31) для int, (n << 1) ^ (n >> 63) для long (арифметический сдвиг).
Шаг 2: Variable-length encoding — unsigned значение разбивается на группы по 7 бит. Старший бит каждого байта (continuation bit): 1 = есть ещё байты, 0 = последний байт.
Пример: кодирование числа 150
Zigzag encoding идентичен Protobuf sint32/sint64. Без zigzag отрицательные числа занимали бы максимум байт (5 для int, 10 для long), потому что в two’s complement старшие биты заполнены единицами. Zigzag перемешивает положительные и отрицательные значения так, что значения около нуля кодируются минимальным числом байт.
Floating point: фиксированный размер
float и double записываются в IEEE 754 формате, little-endian:
float: 4 байта (sign: 1 bit, exponent: 8 bits, mantissa: 23 bits)double: 8 байт (sign: 1 bit, exponent: 11 bits, mantissa: 52 bits)
Без variable-length, без zigzag — фиксированный размер. Little-endian (LSB first) — то же соглашение, что и в x86/ARM по умолчанию.
float 1.0: 00 00 80 3F (little-endian IEEE 754)
double 1.0: 00 00 00 00 00 00 F0 3F
Bytes и String: length-prefixed
bytes и string кодируются одинаково — длина + содержимое:
- Длина в байтах, записанная как Avro
long(zigzag + variable-length) - Raw bytes (для
bytes) или UTF-8 encoded string (дляstring)
string "hello": 0x0A 68 65 6C 6C 6F
│ └── "hello" в UTF-8 (5 байт)
└── zigzag(5) = 10 = 0x0A
Длина кодируется zigzag — поэтому 5 становится 10 (0x0A) на wire. Это неочевидно при ручном парсинге hex dump: чтобы получить настоящую длину, нужно декодировать variable-length integer, затем обратный zigzag: n = (unsigned >> 1) ^ -(unsigned & 1).
Record: конкатенация полей
Самая важная идея кодирования Avro. Record сериализуется как конкатенация закодированных значений полей в порядке объявления. Никаких field names, field tags, separators:
Avro Record
Protobuf Message
Avro record: 9 байт. Protobuf message: 13 байт. Разница — 4 байта на field tags. При миллиардах записей это существенная экономия. Цена: без схемы Avro-данные нечитаемы. Protobuf может декодировать поля (как unknown fields) даже без .proto файла.
Порядок полей в Avro record критичен. Если writer записал поля в порядке [id, name, age], ридер обязан читать в том же порядке. Перестановка полей в схеме — breaking change. В Protobuf порядок полей не имеет значения (каждое поле идентифицируется tag).
Block Encoding: массивы и maps
Arrays и maps кодируются серией блоков (не путать с data blocks контейнера). Каждый блок:
- Count — Avro
long(zigzag). Количество элементов в блоке.0= конец массива/map. - Items — закодированные элементы подряд
Специальное правило: отрицательный count означает, что после абсолютного значения count следует block size (длина блока в байтах). Это позволяет пропустить блок без декодирования элементов.
Массив ["a", "b", "c", "d", "e"]
count=0 (терминатор)
Терминатор — Avro long со значением 0. На wire: один байт 0x00. Завершает массив. Без терминатора ридер не знает, где конец.Map кодируется аналогично: каждый элемент блока — пара (string key + value). Ключ — всегда Avro string.
На практике writer обычно записывает все элементы в один блок (count = N, затем N элементов, затем count = 0). Многоблочная запись полезна для потоковой генерации, когда общее число элементов неизвестно заранее.
Union Encoding
Union кодируется как index + value:
- Index — Avro
long(zigzag), 0-based позиция типа в union array - Value — закодированное значение выбранного типа
Для union ["null", "string"]:
- null: index=0, value=ничего (null кодируется 0 байт) → wire:
0x00(1 байт) - string
"hello": index=1, value="hello"→ wire:0x02 0x0A 68 65 6C 6C 6F(7 байт)
Union ["null", "string"] — идиоматический способ выразить optional field в Avro. Конвенция: null идёт первым в union (index=0), и default поля = null. Это позволяет при schema evolution добавлять новые optional-поля с минимальным overhead (1 байт на запись).
Fixed Encoding
Тип fixed кодируется как raw bytes без length prefix — размер известен из схемы:
fixed MD5 (size=16): D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E
└── 16 байт ровно, без length prefix
Это наиболее компактное кодирование для данных фиксированного размера — zero overhead. Используется для хешей (MD5, SHA), UUID в бинарной форме, IP-адресов, и как backing для логического типа duration.
Hex Dump: полная запись
Соберём всё вместе — закодируем полную запись по схеме:
{
"type": "record",
"name": "Event",
"fields": [
{"name": "id", "type": "long"},
{"name": "type", "type": {"type": "enum", "name": "EventType", "symbols": ["CLICK", "VIEW", "PURCHASE"]}},
{"name": "tags", "type": {"type": "array", "items": "string"}},
{"name": "metadata", "type": ["null", "string"], "default": null}
]
}
Sort Order
Avro спецификация определяет каноническую сортировку для каждого типа, что позволяет сравнивать записи побайтово (без десериализации) для некоторых типов:
| Тип | Порядок сортировки |
|---|---|
null | Всегда равны |
boolean | false < true |
int, long | Числовой (по signed значению) |
float, double | IEEE 754 числовой |
bytes | Unsigned лексикографический |
string | UTF-8 лексикографический |
enum | По порядку символов (ACTIVE < INACTIVE < DELETED) |
array | Лексикографический поэлементно |
record | Поле за полем, по order-атрибуту |
Поле record может иметь атрибут "order": "ascending" (по умолчанию), "descending" или "ignore" (пропускается при сравнении).
Sort order — практичная возможность для MapReduce: reducer получает записи, отсортированные по ключу, и Avro-framework может сортировать записи без десериализации — побайтовым сравнением с учётом sort order спецификации. Сегодня это менее актуально (Spark использует собственную сортировку), но спецификация остаётся.
Итоговая карта кодирований
| Тип | Encoding | Пример значения | Wire bytes |
|---|---|---|---|
null | Ничего (0 байт) | null | — |
boolean | 1 байт | true | 01 |
int | zigzag + varlen | 42 | 54 |
long | zigzag + varlen | 1000 | D0 0F |
float | 4 bytes LE IEEE 754 | 1.5 | 00 00 C0 3F |
double | 8 bytes LE IEEE 754 | 1.5 | 00 00 00 00 00 00 F8 3F |
bytes | long len + raw | 3 байта | 06 XX XX XX |
string | long len + UTF-8 | ”hi” | 04 68 69 |
enum | int (index) | 2nd symbol | 02 |
fixed | raw (no prefix) | 16-byte MD5 | 16 байт |
array | blocks + terminator | 2 items | 04 ... 00 |
map | blocks + terminator | 1 entry | 02 ... 00 |
union | long index + value | null (idx 0) | 00 |
record | concatenation | 3 fields | field₁field₂field₃ |
Ключевые выводы
- Zigzag encoding маппит signed → unsigned, делая значения около нуля компактными:
(n << 1) ^ (n >> 31)для int - Record = конкатенация полей в порядке объявления. Нет field names, нет tags, нет separators — полная зависимость от схемы
- Block encoding для arrays/maps: count + items, zero-count terminates. Отрицательный count добавляет block size для skip
- Union = index + value — 1 байт overhead для nullable fields через
["null", "type"] - Avro encoding минимальнее Protobuf за счёт отсутствия field tags, но требует полной схемы для декодирования
- Sort order — встроенная спецификация сортировки позволяет побайтовое сравнение записей без десериализации