Timeline Architecture и Storage Layout
Apache Hudi (Hadoop Upserts Deletes Incrementals) — это открытый формат транзакционного lakehouse-хранилища, изначально созданный в Uber для инкрементальной обработки петабайтных датасетов. В отличие от Delta Lake (единый _delta_log/) и Apache Iceberg (каталог + дерево metadata файлов), Hudi использует timeline-based архитектуру: каждая операция над таблицей записывается как instant на временной шкале, а данные организованы в FileGroup/FileSlice иерархию.
В Модуле 11 и Модуле 12 мы разобрали подходы Delta Lake и Iceberg. Hudi — третий major формат, и его архитектура ближе к LSM-дереву, чем к append-only логу.
Этот курс использует engine-agnostic подход. Hudi — это протокол и формат хранения, хотя исторически он был привязан к Spark. Начиная с версии 1.0 GA (декабрь 2024), Hudi поддерживает Spark, Flink и Trino как равноправные движки. Библиотека hudi для Python (hudi-rs, v0.4.0) — только чтение: она позволяет читать таблицы, но не создавать и не обновлять их.
Структура .hoodie/
Каждая Hudi-таблица содержит директорию .hoodie/ в корне — аналог _delta_log/ в Delta Lake. Это метаданные, описывающие весь жизненный цикл таблицы:
hudi_table/
Корневая директория Hudi-таблицы. Содержит партиционированные данные и служебную директорию .hoodie/ с timeline и метаданными.hoodie.properties — конфигурация таблицы
Файл hoodie.properties — неизменяемый после создания для ключевых полей. Он определяет фундаментальные характеристики таблицы:
# Тип таблицы — COPY_ON_WRITE или MERGE_ON_READ
hoodie.table.type=MERGE_ON_READ
# Версия формата таблицы (8 в Hudi 1.0+)
hoodie.table.version=8
# Имя таблицы
hoodie.table.name=orders
# Поле первичного ключа
hoodie.table.recordkey.fields=order_id
# Поле для разрешения конфликтов (precombine)
hoodie.table.precombine.field=updated_at
# Тип payload (слияние записей)
hoodie.compaction.payload.class=org.apache.hudi.common.model.OverwriteWithLatestAvroPayload
# Partitioning
hoodie.table.partition.fields=dt
# Checksum для валидации
hoodie.table.checksum=3847592
Поля hoodie.table.type, hoodie.table.recordkey.fields и hoodie.table.partition.fields нельзя изменить после создания таблицы. Выбор COW vs MOR и ключа — необратимое архитектурное решение. Если нужно изменить тип таблицы, придётся пересоздать таблицу целиком.
Что не хранится в .hoodie/
В отличие от Delta Lake, где _delta_log/ содержит полную схему данных, Hudi хранит текущую схему внутри каждого commit instant (в Avro-формате), а не в отдельном schema registry. Partition metadata лежит внутри каждой партиции в файлах .hoodie_partition_metadata.
Timeline: временная шкала операций
Timeline — центральная абстракция Hudi. Каждое действие над таблицей создаёт instant — запись на временной шкале с тремя атрибутами:
Каждый instant представлен файлами в .hoodie/. Имя файла кодирует все три атрибута:
20240115120000000.commit ← completed commit
20240115120000000.commit.inflight ← inflight commit
20240115120000000.commit.requested ← requested commit
20240115130000000.deltacommit ← completed deltacommit (MOR)
20240115140000000.compaction.requested ← scheduled compaction
20240115140000000.compaction.inflight ← running compaction
20240115140000000.compaction ← completed compaction
Жизненный цикл instant
requested
Операция запланирована. Для commit/deltacommit — сразу переходит в inflight. Для compaction/clean — может ожидать в requested, пока async-сервис не подхватит.inflight
Операция выполняется. Writer записывает данные в FileGroup. Если writer упадёт во время inflight, Hudi обнаружит это при следующем запуске и инициирует rollback.completed
Операция успешно завершена. Instant содержит полный план записи: список файлов, схему, статистику. Instant теперь видим для всех reader'ов.rollback (при сбое)
Если inflight instant зависает (writer упал), Hudi откатывает его при следующем запуске. Rollback удаляет частично записанные файлы и создаёт rollback instant на timeline.Типы action
В отличие от Delta Lake, где все операции — это commit с разными action (add, remove), Hudi типизирует действия на уровне instant. Это даёт возможность обрабатывать compaction, clean и rollback как отдельные сущности, что критично для async table services (подробнее — в Уроке 06).
Timeline Server
Timeline Server — это in-process кэширующий прокси, который хранит timeline и file listing в памяти Java-процесса. Без Timeline Server каждый task в Spark/Flink-задаче вызывал бы LIST на object storage для каждого обращения к timeline.
Executor Task 1
Spark/Flink executor task. Вместо обращения к object storage за timeline, делает HTTP-запрос к Timeline Server на driver'е. Результат кэшируется локально.Executor Task 2
Каждый executor task получает timeline через Timeline Server. Это исключает LIST-вызовы к S3/GCS из каждого task, что критично при тысячах задач.Executor Task N
Task N — аналогичная схема. Timeline Server обслуживает все tasks одного job из единого кэша.Timeline Server (на Driver)
Timeline Server запущен на driver'е Spark/Flink. Держит в памяти: активный timeline, file listing по партициям, метаданные FileGroup/FileSlice. Обновляется при каждом commit.Object Storage (S3/GCS/ADLS)
Object storage (S3, GCS, ADLS). Timeline Server делает LIST один раз и кэширует результат. Без Timeline Server каждый task делал бы свой LIST — при 1000 tasks это 1000 LIST-запросов.Конфигурация Timeline Server:
# Включён по умолчанию для Spark
hoodie.embed.timeline.server=true
# Порт (по умолчанию 26754)
hoodie.embed.timeline.server.port=26754
# Размер кэша file listing
hoodie.filesystem.view.remote.timeout.secs=300
Timeline Server не реплицируется. Он живёт на driver’е и обслуживает только текущий job. При параллельных writer’ах (Hudi 1.0 NBCC) каждый job имеет свой Timeline Server, а конфликты разрешаются через lock provider (подробнее — в Уроке 04).
Архивированный Timeline
Активный timeline (файлы .hoodie/*.commit, *.deltacommit и т.д.) растёт с каждой операцией. Hudi автоматически архивирует старые instants:
# Минимальное количество instants на активном timeline
hoodie.keep.min.commits=20
# Максимальное — после этого архивация запускается
hoodie.keep.max.commits=30
LSM-стиль архива (Hudi 1.0)
До Hudi 1.0 архив хранил каждый instant как отдельный файл в .hoodie/.archived/ — при миллионах коммитов это порождало миллионы мелких файлов на object storage. В Hudi 1.0 архивированный timeline использует LSM-формат: instants группируются в compacted Avro-файлы, аналогично тому как LSM-дерево сливает уровни.
LSM-архив решает проблему «мелких файлов» на timeline. Если таблица живёт годами с частыми коммитами (тысячи в день), старый формат создавал миллионы archived instants. LSM-формат сжимает их до управляемого количества файлов с сохранением полной истории для time-travel.
FileGroup и FileSlice: модель хранения данных
Данные в Hudi организованы в двухуровневую иерархию — FileGroup и FileSlice:
Hudi Table
Hudi-таблица состоит из партиций (или одной root-партиции для непартиционированных таблиц). Каждая партиция содержит один или несколько FileGroup.Partition: year=2024
Партиция year=2024. Содержит FileGroup'ы — логические группы файлов, объединённые общим File Group ID. Каждая запись принадлежит ровно одному FileGroup (определяется record key + индекс).FileGroup
FileGroup — это логическая группа файлов с фиксированным идентификатором (UUID). Ключевые свойства:
- Каждая запись (record) навсегда принадлежит одному FileGroup — определяется при первой вставке через индекс
- FileGroup содержит цепочку FileSlice — по одному на каждый instant, затронувший эту группу
- File Group ID — UUID, записанный в имени файла:
fg-001_0_20240115120000000.parquet
FileSlice
FileSlice — это снимок FileGroup на конкретный instant. Состав зависит от типа таблицы:
COW FileSlice
В COW-таблице каждый FileSlice содержит только base-файл (Parquet). При UPDATE весь FileGroup перезаписывается — старый base заменяется новым. Просто для чтения, дорого для записи.MOR FileSlice
В MOR-таблице FileSlice содержит base-файл (Parquet) + цепочку log-файлов (Avro). Log-файлы — это дельты (upserts, deletes), которые reader сливает с base при чтении. Compaction объединяет их в новый base.Анатомия имени файла
Каждый файл данных в Hudi содержит метаинформацию прямо в имени:
{file_group_id}_{write_token}_{instant_time}.parquet ← base файл
.{file_group_id}_{instant_time}.log.{version} ← log файл (точка в начале!)
Log-файлы начинаются с точки (.fg-...). На файловых системах это делает их скрытыми (ls без -a не покажет). На object storage (S3, GCS) точка в имени не имеет специального значения, но это важно знать при отладке на локальной FS.
Анатомия Base File
Base file — это стандартный Parquet-файл с дополнительными Hudi-метаданными в каждой строке:
Мета-колонки _hoodie_* — это цена Hudi: ~50-100 байт overhead на каждую запись. Для широких таблиц (сотни колонок) overhead незначителен, но для узких (2-3 колонки) может увеличить размер на 10-20%.
Анатомия Log File
Log-файлы — специфика MOR-таблиц. Каждый log-файл содержит последовательность блоков:
Каждый блок начинается с magic bytes (#HUDI#) для валидации целостности. Если блок повреждён (неполная запись из-за crash), reader обнаружит это по несовпадению magic и пропустит блок.
Log Block Header:
├── instant_time: "20240115130000000"
├── schema: "{"type":"record","name":"orders",...}"
├── record_count: 1500
├── content_length: 245760
├── block_type: DATA_BLOCK | DELETE_BLOCK | ROLLBACK_BLOCK
└── log_version: 1
Полная картина: от записи до чтения
Соединим все части в единую схему read path для snapshot-запроса:
Snapshot Query
Snapshot query — чтение актуального состояния таблицы. Для COW: читать только последние base-файлы. Для MOR: читать base + merge с log-файлами.- Читаем Timeline
- Определяем FileSlice
- Читаем данные (COW: base, MOR: base + logs)
Результат: актуальные данные
Результат: актуальный snapshot таблицы. Для COW — как чтение обычных Parquet-файлов. Для MOR — добавляется overhead на merge log'ов с base.Сравнение с Delta Lake и Iceberg
| Аспект | Delta Lake | Apache Iceberg | Apache Hudi |
|---|---|---|---|
| Metadata location | _delta_log/ (внутри таблицы) | Внешний каталог + metadata files | .hoodie/ (внутри таблицы) |
| Единица версии | JSON commit | Snapshot (metadata file) | Timeline instant |
| Оптимизация чтения | Checkpoint (Parquet) | Manifest pruning | Timeline Server (in-memory) |
| Типизация операций | (всё — commit) | (всё — snapshot) | (commit ≠ compaction ≠ clean) |
| Архив старых версий | VACUUM (удаляет) | Expire snapshots | Архивированный timeline (сохраняет) |
Ключевое архитектурное отличие Hudi — типизированные операции. Delta Lake и Iceberg оперируют абстрактными «коммитами» или «снапшотами». Hudi явно разделяет запись данных, compaction, очистку и rollback на уровне метаданных. Это позволяет запускать table services (compaction, clean, clustering) как отдельные асинхронные процессы, не блокируя основной write path.
Итоги
.hoodie/— центральная директория метаданных, аналог_delta_log/- Timeline — упорядоченная последовательность typed instants (commit, deltacommit, compaction, clean, rollback, savepoint)
- Каждый instant проходит lifecycle: requested → inflight → completed
- hoodie.properties задаёт необратимые решения: тип таблицы, record key, partition fields
- Timeline Server — in-memory кэш на driver, убирающий LIST-запросы к object storage
- Архивированный timeline (LSM в Hudi 1.0) — бесконечный time-travel без мелких файлов
- FileGroup — логическая единица данных с постоянным UUID; запись навсегда привязана к одному FileGroup
- FileSlice — snapshot FileGroup на instant: base file (Parquet) + опциональные log files (MOR)
- Base file = Parquet + 5 мета-колонок
_hoodie_*+ Bloom filter в footer - Log file = последовательность блоков (Data, Delete, Rollback) с magic bytes для валидации
В следующем уроке мы разберём два типа таблиц — COW и MOR — в деталях: write path, read path, и когда выбирать каждый.