Learning Platform
Глоссарий Troubleshooting
Урок 05.03 · 35 мин
Продвинутый
AvroBinary EncodingZigzagVariable-LengthBlock EncodingUnionSort OrderWire Format

Бинарное кодирование

Принцип: минимум 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:

SignedUnsigned (zigzag)
00
-11
12
-23
24

Формула: (n << 1) ^ (n >> 31) для int, (n << 1) ^ (n >> 63) для long (арифметический сдвиг).

Шаг 2: Variable-length encoding — unsigned значение разбивается на группы по 7 бит. Старший бит каждого байта (continuation bit): 1 = есть ещё байты, 0 = последний байт.

Zigzag + Variable-Length Encoding

Пример: кодирование числа 150

Signed: 150 → Zigzag: 300Исходное signed значение. Zigzag encoding: (150 << 1) ^ (150 >> 31) = 300 ^ 0 = 300. Положительные числа → чётные unsigned.
300 = 0b100101100 → группы: [0101100, 0000010]300 в бинарном: 100101100. Разбиваем на группы по 7 бит справа: 0101100 (младшие 7), 0000010 (старшие). Little-endian — младшая группа первой.
Wire bytesБайт 1: 1_0101100 = 0xAC (continuation bit = 1, ещё есть данные). Байт 2: 0_0000010 = 0x02 (continuation bit = 0, последний байт). Итого: 2 байта.
Значение 0Zigzag(0) = 0. Variable-length: 0x00 — один байт. Минимальное кодирование для нуля.
Значение -1Zigzag(-1) = 1. Variable-length: 0x01 — один байт. Отрицательные единицы так же компактны, как положительные.
Значение 64Zigzag(64) = 128. Variable-length: 0x80 0x02 — два байта. Граница: 128 не помещается в 7 бит (max 127).
TIP

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 кодируются одинаково — длина + содержимое:

  1. Длина в байтах, записанная как Avro long (zigzag + variable-length)
  2. Raw bytes (для bytes) или UTF-8 encoded string (для string)
string "hello": 0x0A 68 65 6C 6C 6F
 │ └── "hello" в UTF-8 (5 байт)
 └── zigzag(5) = 10 = 0x0A
NOTE

Длина кодируется zigzag — поэтому 5 становится 10 (0x0A) на wire. Это неочевидно при ручном парсинге hex dump: чтобы получить настоящую длину, нужно декодировать variable-length integer, затем обратный zigzag: n = (unsigned >> 1) ^ -(unsigned & 1).

Record: конкатенация полей

Самая важная идея кодирования Avro. Record сериализуется как конкатенация закодированных значений полей в порядке объявления. Никаких field names, field tags, separators:

Record: конкатенация полей vs Tagged формат (Protobuf)

Avro Record

SchemaСхема определяет порядок полей. Ридер знает: первые байты — id (long), следующие — name (string), далее — age (int). Без схемы — бессмыслица.
id=42 | name="Alice" | age=30Значения полей идут подряд без разделителей. id=42 → zigzag(42)=84=0x54. name='Alice' → len=5 → 0x0A + UTF-8. age=30 → zigzag(30)=60=0x3C.
Wire bytesВсего 9 байт: 54 (id) + 0A 41 6C 69 63 65 (name with length) + 3C (age). Без тегов, без имён полей, без разделителей. Максимально компактно.

Protobuf Message

Schema (.proto)Protobuf использует числовые field tags (1, 2, 3). Каждое поле на wire предваряется tag+wire_type. Порядок полей не фиксирован.
tag+id | tag+name | tag+ageКаждое поле: tag (field number + wire type) + value. Tag для поля 1 (varint) = 0x08. Tag для поля 2 (length-delimited) = 0x12. Tag для поля 3 (varint) = 0x18.
Wire bytes08 2A (tag 1 + id=42) + 12 05 41 6C 69 63 65 (tag 2 + name='Alice') + 18 1E (tag 3 + age=30). Итого 13 байт — на 4 байта больше из-за тегов.

Avro record: 9 байт. Protobuf message: 13 байт. Разница — 4 байта на field tags. При миллиардах записей это существенная экономия. Цена: без схемы Avro-данные нечитаемы. Protobuf может декодировать поля (как unknown fields) даже без .proto файла.

WARNING

Порядок полей в Avro record критичен. Если writer записал поля в порядке [id, name, age], ридер обязан читать в том же порядке. Перестановка полей в схеме — breaking change. В Protobuf порядок полей не имеет значения (каждое поле идентифицируется tag).

Block Encoding: массивы и maps

Arrays и maps кодируются серией блоков (не путать с data blocks контейнера). Каждый блок:

  1. Count — Avro long (zigzag). Количество элементов в блоке. 0 = конец массива/map.
  2. Items — закодированные элементы подряд

Специальное правило: отрицательный count означает, что после абсолютного значения count следует block size (длина блока в байтах). Это позволяет пропустить блок без декодирования элементов.

Block Encoding для Arrays

Массив ["a", "b", "c", "d", "e"]

Block 1Count = 3 (положительный). Содержит 3 элемента: 'a', 'b', 'c'. Каждый — length-prefixed string. Block size не указан (count положительный).
Block 2Count = -2 (отрицательный!) → абсолютное значение = 2 элемента, далее block size = 6 байт. Ридер может пропустить 6 байт вместо декодирования '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:

  1. Index — Avro long (zigzag), 0-based позиция типа в union array
  2. 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 Encoding
Union ["null", "string"]Самый распространённый union — optional field. Index 0 = null (0 байт значения), index 1 = string (length + UTF-8). Всего 1 байт overhead на nullable field.
Union ["null", "int", "string"]Трёхвариантный union. Index 0 = null, 1 = int, 2 = string. Допустим по спецификации, но при schema evolution поддержка ограничена. Рекомендуется держать unions простыми.
NOTE

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}
 ]
}
Hex Dump: кодирование записи Event
id = 1000 (long)Zigzag(1000) = 2000. Variable-length encoding: 2000 = 0b11111010000 → группы: 1010000, 0001111. Wire: 0xD0 0x0F — два байта.
type = VIEW (enum, index=1)Enum кодируется как Avro int — index символа в массиве symbols. VIEW = index 1. Zigzag(1) = 2. Wire: 0x02 — один байт.
tags = ["web", "mobile"]Array block encoding: count=2 (zigzag=4) → 0x04. Затем 2 string: 'web' (len=3 → zigzag=6 → 0x06, + 3 bytes) и 'mobile' (len=6 → zigzag=12 → 0x0C, + 6 bytes). Терминатор: count=0 → 0x00.
metadata = null (union index=0)Union ['null', 'string']. Значение null → index=0. Zigzag(0) = 0. Wire: 0x00 — один байт. Null-значение ничего не добавляет после index.
Полная запись на wireВсе 4 поля подряд без разделителей. Общий размер: 2 + 1 + 13 + 1 = 17 байт. Для сравнения: JSON-представление той же записи ≈ 70 байт.

Sort Order

Avro спецификация определяет каноническую сортировку для каждого типа, что позволяет сравнивать записи побайтово (без десериализации) для некоторых типов:

ТипПорядок сортировки
nullВсегда равны
booleanfalse < true
int, longЧисловой (по signed значению)
float, doubleIEEE 754 числовой
bytesUnsigned лексикографический
stringUTF-8 лексикографический
enumПо порядку символов (ACTIVE < INACTIVE < DELETED)
arrayЛексикографический поэлементно
recordПоле за полем, по order-атрибуту

Поле record может иметь атрибут "order": "ascending" (по умолчанию), "descending" или "ignore" (пропускается при сравнении).

TIP

Sort order — практичная возможность для MapReduce: reducer получает записи, отсортированные по ключу, и Avro-framework может сортировать записи без десериализации — побайтовым сравнением с учётом sort order спецификации. Сегодня это менее актуально (Spark использует собственную сортировку), но спецификация остаётся.

Итоговая карта кодирований

ТипEncodingПример значенияWire bytes
nullНичего (0 байт)null
boolean1 байтtrue01
intzigzag + varlen4254
longzigzag + varlen1000D0 0F
float4 bytes LE IEEE 7541.500 00 C0 3F
double8 bytes LE IEEE 7541.500 00 00 00 00 00 F8 3F
byteslong len + raw3 байта06 XX XX XX
stringlong len + UTF-8”hi”04 68 69
enumint (index)2nd symbol02
fixedraw (no prefix)16-byte MD516 байт
arrayblocks + terminator2 items04 ... 00
mapblocks + terminator1 entry02 ... 00
unionlong index + valuenull (idx 0)00
recordconcatenation3 fieldsfield₁field₂field₃

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

  1. Zigzag encoding маппит signed → unsigned, делая значения около нуля компактными: (n << 1) ^ (n >> 31) для int
  2. Record = конкатенация полей в порядке объявления. Нет field names, нет tags, нет separators — полная зависимость от схемы
  3. Block encoding для arrays/maps: count + items, zero-count terminates. Отрицательный count добавляет block size для skip
  4. Union = index + value — 1 байт overhead для nullable fields через ["null", "type"]
  5. Avro encoding минимальнее Protobuf за счёт отсутствия field tags, но требует полной схемы для декодирования
  6. Sort order — встроенная спецификация сортировки позволяет побайтовое сравнение записей без десериализации

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Avro record сериализуется как конкатенация полей в порядке объявления. Запись User: id=42 (long), name='Alice' (string), age=30 (int). Сколько байт на wire и что они содержат?

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

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

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

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