Learning Platform
Глоссарий Troubleshooting
Урок 14.01 · 40 мин
Продвинутый
Apache HudiTimeline.hoodieFileGroupFileSliceInstantTimeline ServerArchived TimelineStorage Layout

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 логу.

NOTE

Этот курс использует 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-таблицы

hudi_table/

Корневая директория Hudi-таблицы. Содержит партиционированные данные и служебную директорию .hoodie/ с timeline и метаданными.
Партиции данныхКаждая партиция — директория с base-файлами (Parquet) и log-файлами (Avro). Имена файлов содержат File Group ID, commit timestamp и расширение.
.hoodie/Центральная директория метаданных. Содержит timeline instants (JSON/Avro), hoodie.properties (конфигурация таблицы), архивированный timeline (LSM в Hudi 1.0), и метаданные таблицы (partition metadata, column stats).

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
WARNING

Поля 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 — запись на временной шкале с тремя атрибутами:

Анатомия Timeline Instant
TimestampМонотонный таймстемп в формате yyyyMMddHHmmssSSS (17 цифр). Определяет порядок instant'ов на timeline. Генерируется из системного времени или из managed timeline server (Hudi 1.0).
Action TypeТип операции: commit (COW запись), deltacommit (MOR запись), compaction (слияние log-файлов в base), clean (удаление устаревших FileSlice), rollback (отмена failed instant), savepoint (защита snapshot от clean).
StateТри состояния: requested (план операции), inflight (операция выполняется), completed (операция завершена). Переходы: requested → inflight → completed. Если inflight зависает, rollback-механизм возвращает в requested.

Каждый 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

Lifecycle Timeline Instant

requested

Операция запланирована. Для commit/deltacommit — сразу переходит в inflight. Для compaction/clean — может ожидать в requested, пока async-сервис не подхватит.
begin

inflight

Операция выполняется. Writer записывает данные в FileGroup. Если writer упадёт во время inflight, Hudi обнаружит это при следующем запуске и инициирует rollback.
success

completed

Операция успешно завершена. Instant содержит полный план записи: список файлов, схему, статистику. Instant теперь видим для всех reader'ов.

rollback (при сбое)

Если inflight instant зависает (writer упал), Hudi откатывает его при следующем запуске. Rollback удаляет частично записанные файлы и создаёт rollback instant на timeline.

Типы action

Типы Action на Timeline
commitЗапись данных в COW-таблицу. Создаёт новые base-файлы (Parquet). Каждый commit полностью перезаписывает затронутые FileGroup — read path читает только base-файлы.
deltacommitЗапись данных в MOR-таблицу. Создаёт log-файлы (Avro) рядом с base-файлами. Не перезаписывает base — дописывает дельту. Быстрее commit, но читать дороже (нужен merge).
compactionСливает log-файлы в base-файл в MOR-таблице. Асинхронная операция: scheduled → inflight → completed. После compaction read path работает с чистым base без merge.
cleanУдаляет устаревшие FileSlice (старые версии base и log файлов). Управляется политикой: KEEP_LATEST_COMMITS (по умолчанию 10 коммитов) или KEEP_LATEST_FILE_VERSIONS.
rollbackОтменяет failed instant: удаляет частично записанные файлы, возвращает таблицу в состояние до операции. Автоматический при обнаружении зависшего inflight.
savepointЗащищает конкретный snapshot от clean-операции. Используется для disaster recovery: savepoint гарантирует, что файлы этого snapshot не будут удалены при очистке.
TIP

В отличие от 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.

Timeline Server Architecture

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 из единого кэша.
HTTP GET timeline

Timeline Server (на Driver)

Timeline Server запущен на driver'е Spark/Flink. Держит в памяти: активный timeline, file listing по партициям, метаданные FileGroup/FileSlice. Обновляется при каждом commit.
LIST (один раз)

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
NOTE

Timeline Server не реплицируется. Он живёт на driver’е и обслуживает только текущий job. При параллельных writer’ах (Hudi 1.0 NBCC) каждый job имеет свой Timeline Server, а конфликты разрешаются через lock provider (подробнее — в Уроке 04).

Архивированный Timeline

Активный timeline (файлы .hoodie/*.commit, *.deltacommit и т.д.) растёт с каждой операцией. Hudi автоматически архивирует старые instants:

Активный vs Архивированный Timeline
Активный Timeline (.hoodie/)Последние N completed instants (по умолчанию 30). Доступны для инкрементальных запросов и time-travel. Файлы лежат прямо в .hoodie/ — каждый instant = отдельный файл.
archive trigger
Архивированный Timeline (.hoodie/.archived/)Instants старше порога (по умолчанию: больше 30 completed, минимум 20 оставить). В Hudi 1.0 — LSM-формат: compacted Avro-файлы вместо отдельных файлов на instant. Бесконечный time-travel.
# Минимальное количество 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-дерево сливает уровни.

TIP

LSM-архив решает проблему «мелких файлов» на timeline. Если таблица живёт годами с частыми коммитами (тысячи в день), старый формат создавал миллионы archived instants. LSM-формат сжимает их до управляемого количества файлов с сохранением полной истории для time-travel.

FileGroup и FileSlice: модель хранения данных

Данные в Hudi организованы в двухуровневую иерархию — FileGroup и FileSlice:

Иерархия FileGroup / FileSlice

Hudi Table

Hudi-таблица состоит из партиций (или одной root-партиции для непартиционированных таблиц). Каждая партиция содержит один или несколько FileGroup.

Partition: year=2024

Партиция year=2024. Содержит FileGroup'ы — логические группы файлов, объединённые общим File Group ID. Каждая запись принадлежит ровно одному FileGroup (определяется record key + индекс).
FileGroup A (fg-001)FileGroup — логическая единица данных. Содержит цепочку FileSlice (по одному на commit). Каждый FileGroup идентифицируется UUID (File Group ID). Запись всегда принадлежит одному FileGroup на протяжении всего жизненного цикла.
FileGroup B (fg-002)Второй FileGroup в той же партиции. Новые записи распределяются по FileGroup через индекс (Bloom, HFile, bucket). Количество FileGroup растёт при INSERT новых ключей, не найденных в существующих группах.

FileGroup

FileGroup — это логическая группа файлов с фиксированным идентификатором (UUID). Ключевые свойства:

  • Каждая запись (record) навсегда принадлежит одному FileGroup — определяется при первой вставке через индекс
  • FileGroup содержит цепочку FileSlice — по одному на каждый instant, затронувший эту группу
  • File Group ID — UUID, записанный в имени файла: fg-001_0_20240115120000000.parquet

FileSlice

FileSlice — это снимок FileGroup на конкретный instant. Состав зависит от типа таблицы:

Состав FileSlice: COW vs MOR

COW FileSlice

В COW-таблице каждый FileSlice содержит только base-файл (Parquet). При UPDATE весь FileGroup перезаписывается — старый base заменяется новым. Просто для чтения, дорого для записи.
Base FileParquet-файл с полными данными. В COW-таблице — единственный файл в FileSlice. Содержит все текущие записи FileGroup после применения операции. Имя: {fg-id}_{write-token}_{instant}.parquet.

MOR FileSlice

В MOR-таблице FileSlice содержит base-файл (Parquet) + цепочку log-файлов (Avro). Log-файлы — это дельты (upserts, deletes), которые reader сливает с base при чтении. Compaction объединяет их в новый base.
Base File + Log FilesBase-файл (Parquet) — snapshot на момент последней compaction. Log-файлы (Avro) — дельты после base. Формат: .{fg-id}_{instant}.log.{version}. Версия инкрементируется при каждом deltacommit.

Анатомия имени файла

Каждый файл данных в Hudi содержит метаинформацию прямо в имени:

{file_group_id}_{write_token}_{instant_time}.parquet ← base файл
.{file_group_id}_{instant_time}.log.{version} ← log файл (точка в начале!)
Декомпозиция имени файла
Base FileИмя base-файла: File Group ID (UUID) + write token (номер попытки записи) + instant time + расширение. Write token = 0 для успешной первой попытки, инкрементируется при retry.
Log FileИмя log-файла начинается с точки (скрытый файл). Содержит File Group ID + instant base time + расширение .log + версия. Версия инкрементируется при каждом deltacommit в этот FileGroup.
WARNING

Log-файлы начинаются с точки (.fg-...). На файловых системах это делает их скрытыми (ls без -a не покажет). На object storage (S3, GCS) точка в имени не имеет специального значения, но это важно знать при отладке на локальной FS.

Анатомия Base File

Base file — это стандартный Parquet-файл с дополнительными Hudi-метаданными в каждой строке:

Структура Hudi Base File (Parquet)
Hudi Meta-колонки (5 штук)Каждая запись в Hudi-таблице содержит 5 служебных колонок, добавляемых автоматически. Они занимают ~50-100 байт на запись. _hoodie_commit_time и _hoodie_commit_seqno используются для инкрементальных запросов.
Пользовательские колонкиДанные пользователя: все колонки, определённые в схеме таблицы. Хранятся в стандартном Parquet-формате с поддержкой всех Parquet-кодировок (RLE, Dictionary, Delta и т.д.).
Parquet FooterСтандартный Parquet footer: Row Group metadata, column statistics (min/max/null_count), схема (Parquet schema + Hudi Avro schema), Bloom filter (для Bloom-индекса), и Hudi-специфичные ключи в key-value metadata.

Мета-колонки _hoodie_* — это цена Hudi: ~50-100 байт overhead на каждую запись. Для широких таблиц (сотни колонок) overhead незначителен, но для узких (2-3 колонки) может увеличить размер на 10-20%.

Анатомия Log File

Log-файлы — специфика MOR-таблиц. Каждый log-файл содержит последовательность блоков:

Структура Hudi Log File
.fg-001_20240115.log.1Log-файл — бинарный файл с последовательностью блоков. Каждый блок — самодостаточная единица: magic bytes для валидации, заголовок с метаданными, и содержимое (данные, delete, или rollback).
Data BlockСодержит записи для upsert: Avro-сериализованные строки с Hudi мета-колонками + пользовательскими данными. Заголовок: instant time, schema, record count, content length, block type.
Delete BlockСодержит только ключи записей для удаления. Не хранит полные записи — только record key. При merge reader помечает эти ключи как удалённые.
Rollback BlockОтменяет предыдущий блок в этом же log-файле. Используется при rollback failed instant. Reader пропускает блоки, на которые указывает rollback block.

Каждый блок начинается с 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-запроса:

Read Path: Snapshot Query

Snapshot Query

Snapshot query — чтение актуального состояния таблицы. Для COW: читать только последние base-файлы. Для MOR: читать base + merge с log-файлами.
  1. Читаем Timeline
Шаг 1: Прочитать активный timeline из .hoodie/ (или из Timeline Server). Найти последний completed commit/deltacommit. Это определяет, какие FileSlice актуальны.
  1. Определяем FileSlice
Шаг 2: Для каждой партиции определить набор FileGroup. Для каждого FileGroup найти последний FileSlice (base file + log files до текущего instant).
  1. Читаем данные (COW: base, MOR: base + logs)
Шаг 3 (COW): Читаем только base-файлы — они содержат полные данные. Шаг 3 (MOR): Читаем base + merge с log-файлами. Merge по record key: log-записи перезаписывают base-записи.

Результат: актуальные данные

Результат: актуальный snapshot таблицы. Для COW — как чтение обычных Parquet-файлов. Для MOR — добавляется overhead на merge log'ов с base.

Сравнение с Delta Lake и Iceberg

Metadata Architecture: Hudi vs Delta vs Iceberg
Delta LakeMetadata встроена в таблицу: _delta_log/ с JSON-коммитами + Parquet-checkpoints. Нет внешнего каталога (опционален). Log replay от checkpoint. Линейная история.
Apache IcebergMetadata-дерево через внешний каталог: catalog → metadata file → manifest list → manifest files → data files. Immutable файлы. Каталог обязателен (REST, Hive, Glue).
Apache HudiTimeline-based: .hoodie/ с typed instants. FileGroup/FileSlice иерархия. Timeline Server для кэширования. Архивированный timeline (LSM в 1.0). Типизированные операции (commit ≠ compaction ≠ clean).
АспектDelta LakeApache IcebergApache Hudi
Metadata location_delta_log/ (внутри таблицы)Внешний каталог + metadata files.hoodie/ (внутри таблицы)
Единица версииJSON commitSnapshot (metadata file)Timeline instant
Оптимизация чтенияCheckpoint (Parquet)Manifest pruningTimeline Server (in-memory)
Типизация операций (всё — commit) (всё — snapshot) (commit ≠ compaction ≠ clean)
Архив старых версийVACUUM (удаляет)Expire snapshotsАрхивированный timeline (сохраняет)
NOTE

Ключевое архитектурное отличие 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, и когда выбирать каждый.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Hudi-таблица содержит 50 completed instants на активном timeline. Настройки: hoodie.keep.min.commits=20, hoodie.keep.max.commits=30. Что произойдёт при следующем commit?

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

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

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

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