Learning Platform
Глоссарий Troubleshooting
Урок 14.03 · 40 мин
Продвинутый
Apache HudiIndexBloom FilterHFileBucket IndexRecord IndexSecondary IndexExpression IndexTagging

Индексная подсистема

В Уроке 02 мы видели, что и COW, и MOR начинают upsert с index lookup — определения, в каком FileGroup находится каждая запись. Этот шаг критичен: без индекса Hudi пришлось бы сканировать все base files во всех партициях для поиска каждой записи.

Hudi предлагает 6 типов индексов, включая новую систему secondary indexes в версии 1.0. Выбор индекса — одно из ключевых архитектурных решений при проектировании таблицы: он определяет скорость записи, потребление памяти, и поведение при upsert.

NOTE

Индекс в Hudi — это не read-side оптимизация (как Z-order или Bloom filter на уровне Parquet). Это write-side структура: она используется при записи для маршрутизации записей по FileGroup. Read path не использует Hudi-индексы — он работает с Parquet statistics, partition pruning и column push-down.

Зачем нужен индекс: проблема tagging

При upsert Hudi должен пометить (tag) каждую входящую запись: INSERT (новая) или UPDATE (существующая). Для UPDATE нужно знать конкретный FileGroup, чтобы записать данные в правильный файл:

Index Lookup: Tagging записей

Батч: N записей (record_key, data)

Входящий батч содержит N записей. Каждая запись имеет record key (primary key). Writer должен определить для каждой записи: это INSERT или UPDATE? Если UPDATE — в какой FileGroup?
Index lookup

Index: record_key → (partition, FileGroup, file)

Индекс принимает набор record keys и возвращает маппинг: record_key → (partition, FileGroup ID, file_name). Если record_key не найден — это INSERT. Если найден — UPDATE с привязкой к конкретному FileGroup.
Tagged INSERTЗаписи, не найденные в индексе. Будут распределены по FileGroup: либо добавлены к существующему (если файл не полон), либо создадут новый FileGroup. Распределение зависит от типа индекса.
Tagged UPDATEЗаписи, найденные в индексе. Привязаны к конкретному FileGroup. COW: перезапись base file этого FileGroup. MOR: append в log file этого FileGroup. Record key навсегда остаётся в этом FileGroup.

Без индекса tagging потребует full table scan — чтение всех base files для поиска каждого record key. При таблице с 1B записей (1000 FileGroup × 1M записей) это 1000 Parquet-файлов на каждый upsert. Индекс сводит это к O(1) или O(log N) lookup.

Bloom Filter Index

Bloom Filter Index — индекс по умолчанию в Hudi. Использует Bloom filter, встроенный в footer каждого Parquet base file:

Bloom Filter Index: Lookup Flow

Батч record keys

Batch записей для upsert. Каждая запись имеет record key. Bloom index начинает с partition pruning: если таблица партиционирована, определяем partition path для каждой записи.
1. Partition pruning

Определить partition для каждого key

Для каждой записи определяем partition (по partition fields). Это сужает поиск: вместо всех FileGroup таблицы — только FileGroup в нужных партициях. Если партиции нет — ищем по всей таблице.
2. Bloom filter check

Читаем Bloom filter из Parquet footer (без чтения данных)

Для каждого base file в партиции читаем Bloom filter из Parquet footer (без чтения данных). Bloom filter — вероятностная структура: если говорит 'нет' — запись точно не в этом файле. Если 'да' — запись МОЖЕТ быть в файле (false positive возможен).
3. Candidate files

Кандидаты: K файлов (Bloom сказал «возможно»)

Bloom filter отсеял большинство файлов. Осталось K кандидатов (файлы, где Bloom сказал 'возможно'). Для подтверждения: читаем Record Key range из Parquet statistics (min/max record_key). Ещё одна фильтрация.
4. Confirm (read keys)

Читаем _hoodie_record_key из кандидатов → подтверждаем INSERT/UPDATE

Для оставшихся кандидатов: читаем колонку _hoodie_record_key из Parquet (column pruning). Это подтверждает или опровергает match. Только после этого запись tagged как INSERT или UPDATE.

Характеристики Bloom Filter Index

Bloom Filter: параметры и trade-offs
ПлюсыBloom filter хранится в Parquet footer — не нужна отдельная структура. Pruning без чтения данных (только footer). Хорошо работает при случайном распределении ключей. Индекс по умолчанию — нулевая настройка.
МинусыFalse positives: при FPP 0.001 и 1M записей — ~1000 ложных срабатываний. Для подтверждения нужно читать данные. Медленнее при high update ratio (много кандидатов). Не работает для non-partitioned таблиц с 100+ FileGroup.
# False Positive Probability (по умолчанию 0.000000001 — очень низкая)
hoodie.bloom.index.filter.type=DYNAMIC
hoodie.bloom.index.filter.dynamic.max.entries=100000
# Использовать min/max record key для доп. pruning
hoodie.bloom.index.use.metadata=true
# Параллелизм lookup
hoodie.bloom.index.parallelism=0
TIP

Bloom filter index лучше всего работает, когда record keys равномерно распределены по FileGroup (например, UUID). Если ключи монотонные (auto-increment ID), то все обновления попадают в одни и те же файлы, и Bloom filter не помогает — все файлы будут кандидатами.

Simple Index

Simple Index — brute-force подход: читает все record keys из всех base files в нужных партициях и делает join с входящим батчем:

Simple Index vs Bloom Filter
Simple IndexАлгоритм: для каждой партиции, затронутой батчем, читаем колонку _hoodie_record_key из всех base files. Join с батчем по record key. Гарантия: нет false positives. Цена: I/O на чтение всех ключей.
Bloom Filter IndexАлгоритм: читаем Bloom filter из footer (не данные). Отсеиваем файлы. Читаем ключи только из кандидатов. Есть false positives, но I/O значительно ниже при большом количестве файлов.

Когда Simple Index лучше Bloom

Simple Index выигрывает в двух сценариях:

  1. Очень маленькие партиции (< 10 файлов): overhead Bloom pruning не окупается
  2. High update ratio (> 50% записей — обновления): Bloom даёт много кандидатов, и приходится всё равно читать ключи
hoodie.index.type=SIMPLE

HFile Record Index

HFile Record Index использует HFile (формат HBase) для хранения маппинга record_key → FileGroup. Это точный индекс без false positives и без full scan:

HFile Record Index Architecture

Батч record keys

Record keys для lookup. HFile index не читает base files — он обращается к отдельным HFile-файлам, которые содержат отсортированный маппинг record_key → (partition, FileGroup ID).

HFile: sorted record_key → FileGroup Binary search: O(log N)

HFile — формат sorted key-value хранилища из HBase. Ключи отсортированы, поддерживается binary search. Block index на верхнем уровне позволяет найти нужный data block за O(log N). Каждый HFile покрывает один или несколько FileGroup.
Точный результатHFile даёт точный ответ: запись либо есть (с конкретным FileGroup ID), либо нет. Нет false positives. Нет необходимости читать данные для подтверждения.
Block CacheHFile поддерживает block cache: часто запрашиваемые блоки кэшируются в памяти. При повторных обновлениях тех же ключей — lookup без I/O. Размер кэша настраивается через hoodie.index.hbase.bucket.cache.size.

Record-Level Index (Hudi 1.0)

Hudi 1.0 представил Record-Level Index — эволюцию HFile Record Index, хранимую в metadata table:

Record-Level Index (Hudi 1.0)
Record Index (metadata table)Record index хранится в .hoodie/metadata/ — специальная Hudi-таблица внутри основной таблицы. Формат: HFile с record_key → (partition, FileGroup, instant). Обновляется автоматически при каждом commit/deltacommit.
ПроизводительностьДо 4x ускорение lookup по сравнению с Bloom filter. Нет false positives. Block cache минимизирует I/O при повторных операциях. При 1B записей lookup по 100K ключей — секунды, не минуты.
# Включить Record-Level Index (Hudi 1.0+)
hoodie.metadata.record.index.enable=true
hoodie.index.type=RECORD_INDEX
TIP

Record-Level Index — рекомендуемый индекс для Hudi 1.0+ при больших таблицах (> 100M записей). Он сочетает точность HFile (нет false positives) с автоматическим обслуживанием (хранится в metadata table, обновляется транзакционно). Для маленьких таблиц (< 10M записей) Bloom filter по-прежнему достаточен.

Bucket Index

Bucket Index — hash-based индекс, который распределяет записи по FileGroup на основе хэша record key:

Bucket Index: Hash Distribution

record_key

Record key каждой записи. Bucket Index вычисляет hash(record_key) mod N, где N — количество bucket'ов. Каждый bucket = один FileGroup. Маппинг детерминистичен: одинаковый ключ всегда попадает в один и тот же bucket.
hash(key) mod N
Bucket 0 (fg-000)FileGroup для записей с hash(key) mod N = 0. Содержит ~1/N всех записей. При INSERT — запись сразу попадает в правильный bucket без lookup. При UPDATE — тоже: hash даёт ответ мгновенно.
Bucket 1 (fg-001)FileGroup для hash = 1. Все записи с ключами, хэш которых даёт остаток 1 при делении на N. Нет необходимости в Bloom filter или HFile.
Bucket 2 (fg-002)FileGroup для hash = 2. Bucket Index не требует I/O для lookup — это чистая функция от record key. O(1) lookup.
...Bucket Index создаёт фиксированное количество FileGroup = N. Все записи детерминистически распределены по N bucket'ам. Не может быть больше или меньше N FileGroup в партиции.
Bucket N-1Последний bucket. Общее количество N задаётся при создании таблицы через hoodie.bucket.index.num.buckets. Изменить N после создания — невозможно (потребуется resharding).

Характеристики Bucket Index

СвойствоЗначение
Lookup complexityO(1) — чистый hash, нет I/O
False positives
Дополнительная структура — hash вычисляется на лету
Количество FileGroupФиксировано = N buckets
ReshardingНевозможно без пересоздания таблицы
hoodie.index.type=BUCKET
hoodie.bucket.index.num.buckets=256
hoodie.bucket.index.hash.field=record_key
WARNING

Количество bucket’ов нельзя изменить после создания таблицы. Если вы создали 256 bucket’ов, а данных стало в 100x больше — каждый bucket будет содержать огромный файл. Выбирайте N с запасом на рост. Ориентир: target file size / expected data per partition.

Consistent Hashing Bucket (Hudi 1.0)

Hudi 1.0 добавил consistent hashing вариант bucket index, который поддерживает динамическое масштабирование — добавление и удаление bucket’ов без полной пересортировки:

Consistent Hashing vs Fixed Bucket
Fixed BucketФиксированное N. При росте данных — файлы раздуваются. При уменьшении — файлы слишком мелкие. Resharding невозможен. Простой и предсказуемый.
Consistent Hashing (1.0)Динамическое масштабирование: bucket'ы могут split (при переполнении) и merge (при недозаполнении). Consistent hashing минимизирует перемещение записей: при split только ~1/N записей мигрирует. Async resharding.
hoodie.index.type=BUCKET
hoodie.index.bucket.engine=CONSISTENT_HASHING
hoodie.bucket.index.max.num.buckets=512
hoodie.bucket.index.min.num.buckets=32

Secondary Indexes (Hudi 1.0)

Hudi 1.0 представил систему secondary indexes — индексов по произвольным колонкам (не только record key). Это read-side оптимизация, в отличие от primary index (write-side):

Secondary Index Architecture
Query: WHERE status = "shipped"Query с предикатом по не-ключевой колонке: WHERE status = 'shipped' AND amount > 1000. Без secondary index — full table scan. С secondary index — pruning до конкретных FileGroup.

Secondary Index: status → FileGroups

Secondary index хранится в metadata table (.hoodie/metadata/). Формат: value → list of (partition, FileGroup, record position). Обновляется при каждом commit/deltacommit транзакционно.

Pruned: только релевантные FileGroup

Результат: список FileGroup, содержащих записи с status='shipped'. Query engine читает только эти файлы. Data skipping: вместо 1000 файлов читаем 50.

Expression Indexes (Hudi 1.0)

Expression Index — подвид secondary index, где индексируется выражение над колонками, а не сами колонки:

Expression Index: примеры
Функциональный индексИндекс на выражение: например, MONTH(event_date). Позволяет ускорить запросы типа WHERE MONTH(event_date) = 3 без полного сканирования. Hudi вычисляет MONTH() при записи и хранит результат в индексе.
Column Stats IndexИндекс на min/max/null_count по колонке на уровне FileGroup. Аналог Parquet statistics, но на уровне Hudi metadata table. Ускоряет range predicates: WHERE amount BETWEEN 100 AND 500.
-- Создание secondary index (Hudi 1.0+)
CREATE INDEX idx_status ON orders USING secondary_index(status);

-- Expression index
CREATE INDEX idx_event_month ON events USING expression_index(month(event_date));

-- Column stats index
CREATE INDEX idx_amount_stats ON orders USING column_stats(amount);
NOTE

Secondary и expression indexes в Hudi 1.0 — metadata-based: они хранятся в .hoodie/metadata/ table и обновляются атомарно вместе с данными. Это отличается от Iceberg (partition stats в manifest files) и Delta Lake (data skipping в checkpoint). Hudi-подход: индексы — это данные в таблице метаданных, транзакционно согласованные с основной таблицей.

Гайд по выбору индекса

Decision Tree: выбор индекса

Размер таблицы?

Первый вопрос: размер таблицы. Для маленьких таблиц (< 10M записей) Bloom filter достаточен и не требует настройки. Для больших — нужен более эффективный индекс.
< 10M → Bloom (default)Маленькая таблица (< 10M записей): Bloom Filter Index. По умолчанию, нулевая настройка, достаточная производительность. Overhead false positives незначителен.
> 10M → далее...Большая таблица: зависит от паттерна ключей и требований к resharding.

Паттерн ключей?

Второй вопрос: паттерн ключей. Случайные UUID → Record Index. Монотонные (timestamp-based) → Bucket Index. Неравномерное распределение → Record Index.

Случайные → Record Index

Случайные ключи (UUID): Record-Level Index (Hudi 1.0). Точный lookup, block cache, автоматическое обслуживание. Лучший choice для большинства production workloads.

Монотонные → Bucket

Монотонные ключи (timestamp, auto-increment): Bucket Index с consistent hashing. Hash разбивает монотонность, dynamic buckets обеспечивают масштабирование.

Сравнительная таблица индексов

ИндексLookupFalse PositivesI/O на lookupДоп. хранениеReshardingРекомендация
Bloom FilterO(N files × filter check)Чтение footer’ов (в Parquet)N/ADefault, < 10M записей
SimpleO(N files × key scan)Чтение ключейN/AМаленькие партиции
HFileO(log N)Чтение HFile блоковHFile индексыN/AУстаревший (→ Record Index)
Bucket (fixed)O(1) I/OПредсказуемый размер данных
Bucket (consistent)O(1) I/OРастущие данные
Record Index (1.0)O(log N)Block cacheMetadata tableАвтоProduction default 1.0+
TIP

Для новых таблиц на Hudi 1.0+ рекомендуемая стратегия:

  • Record-Level Index как primary index (write-side, для upsert tagging)
  • Column Stats Index для read-side data skipping (range predicates)
  • Expression Index для часто используемых предикатов с функциями

Эта комбинация покрывает и write-path (upsert маршрутизация) и read-path (query pruning).

Индекс и глобальная уникальность

По умолчанию Hudi-индекс — partition-scoped: он ищет record key только внутри одной партиции. Это значит, что одинаковый record key может существовать в разных партициях:

Partition-scoped vs Global Index
Partition-scoped (default)Lookup только в целевой партиции. Быстрый: не нужно сканировать другие партиции. Но допускает дубликаты record key в разных партициях. Используется когда partition key — часть логического ключа.
Global IndexLookup по всей таблице. Гарантирует глобальную уникальность record key. Медленнее: нужно проверить все партиции. Обязателен, когда запись может мигрировать между партициями (например, статус меняется = другая партиция).

Когда нужен Global Index

Пример: таблица orders партиционирована по status (pending, processing, shipped). При обновлении заказа order_id=123 статус меняется с pendingshipped. С partition-scoped индексом:

  • Hudi ищет order_id=123 в партиции shipped (целевой)
  • Не находит (запись в pending)
  • Считает INSERT → создаёт дубликат

С global индексом Hudi найдёт запись в pending и выполнит cross-partition update: удалит из pending, вставит в shipped.

# Включить global index
hoodie.index.type=GLOBAL_BLOOM
# или
hoodie.index.type=GLOBAL_SIMPLE
# Record Index в Hudi 1.0 — глобальный по умолчанию
hoodie.metadata.record.index.enable=true
WARNING

Global index значительно медленнее partition-scoped: при 1000 партиций lookup проверяет все 1000 вместо одной. Используйте его только когда запись может мигрировать между партициями. Для большинства use case (partition по дате, данные не мигрируют) partition-scoped индекс достаточен.

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

АспектDelta LakeApache IcebergApache Hudi
Write-side index (append-only log) (manifest pruning)Bloom, HFile, Bucket, Record Index
Read-side optimizationZ-order, liquid clusteringHidden partitioning, sort ordersSecondary indexes, column stats, expression
Upsert без indexFull scan + mergeFull scan + mergeImpossible (index обязателен)
Index хранениеN/AN/AParquet footer / metadata table
Глобальная уникальностьЧерез MERGE conditionЧерез MERGE conditionGlobal Index (встроенный)
NOTE

Ключевое отличие: Hudi требует индекс для upsert — это архитектурное решение, а не опция. Delta Lake и Iceberg выполняют MERGE через full scan + join (оптимизированный через statistics, но без dedicated index). Hudi’s approach: индекс делает upsert O(1) или O(log N) вместо O(N) — цена: дополнительная структура данных и обслуживание индекса.

Итоги

  • Index lookup (tagging) — обязательный шаг перед каждым upsert: определяет INSERT vs UPDATE и целевой FileGroup
  • Bloom Filter Index (default): вероятностный, footer-based, достаточен для < 10M записей
  • Simple Index: brute-force чтение ключей, без false positives, для маленьких партиций
  • HFile Record Index: точный lookup через sorted key-value HFile, block cache
  • Bucket Index: O(1) hash-based lookup, фиксированное N (или dynamic с consistent hashing в 1.0)
  • Record-Level Index (Hudi 1.0): HFile в metadata table, автоматическое обслуживание, до 4x быстрее Bloom — рекомендуемый для production
  • Secondary Indexes (1.0): read-side индексы по произвольным колонкам, expression indexes, column stats
  • Global vs partition-scoped: global нужен только при миграции записей между партициями
  • Рекомендуемая комбинация для Hudi 1.0: Record Index (write) + Column Stats (read) + Expression Index (custom predicates)

В следующем уроке мы разберём контроль конкурентности — как Hudi обеспечивает ACID при нескольких writer’ах, и что изменил NBCC-протокол в версии 1.0.

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

Результат: 0 из 0
Аналитический
Вопрос 1 из 4. Таблица orders партиционирована по status (pending, shipped, delivered). Запись order_id=42 находится в партиции pending. Поступает upsert с order_id=42 и status=shipped. Индекс — partition-scoped Bloom. Что произойдёт?

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

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

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

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