Apache Iceberg: архитектура табличного формата для Data Lake

Погружение в архитектуру и внутреннее устройство Apache Iceberg — открытого табличного формата для lakehouse. Курс охватывает метаданные, снапшоты, манифесты, ACID-транзакции, эволюцию схем, оптимизацию запросов и интеграцию с Spark, Trino и Flink. Фокус на инженерных аспектах и практических решениях для реальных Big Data-проектов.

1. Архитектура и метаданные Apache Iceberg: слои catalog, metadata file, snapshots, manifest list, manifest files и data files

Архитектура и метаданные Apache Iceberg: слои catalog, metadata file, snapshots, manifest list, manifest files и data files

Представьте, что вы открыли директорию на S3 с 50 000 Parquet-файлов и вам нужно найти все записи за вчерашний день. В классическом Hive вы бы запустили list-операцию по тысячам каталогов, потратив минуты на планирование запроса ещё до чтения первого байта данных. Apache Iceberg решает эту проблему архитектурно: он вводит многоуровневую иерархию метаданных, которая позволяет движку определить нужные файлы за миллисекунды, не перечисляя каталоги вообще.

Почему метаданные — это сердце Iceberg

Iceberg — это не сервис и не база данных. Это открытая спецификация табличного формата плюс набор библиотек, которые говорят движку (Spark, Trino, Flink), как правильно читать и писать данные поверх объектного хранилища. Вся магия — в том, что между движком и файлами данных располагается структурированный слой метаданных. Именно он обеспечивает ACID, time travel и мгновенный pruning ненужных файлов.

> Iceberg — это прослойка между движком и данными. По факту это просто библиотека о том, как хранить данные. Не более. Не сервис. Не СУБД. Просто набор библиотек для каждого движка, который записывает данные. > > habr.com

Шесть слоёв архитектуры

Архитектура Iceberg организована как иерархия из шести уровней. Двигаясь от верха к низу, каждый уровень ссылается на следующий — от точки входа до сырых данных.

Catalog — точка входа

Catalog — это внешний сервис, который хранит отображение «имя таблицы → путь к текущему metadata file». Когда движок выполняет запрос к таблице analytics.events, он первым делом обращается именно к catalog.

Реализации catalog:

| Реализация | Хранение указателя | Когда использовать | |---|---|---| | Hive Metastore | В базе данных HMS | Существующая инфраструктура Hive | | AWS Glue | Managed-сервис AWS | Облачные деплойменты на AWS | | REST Catalog | Отдельный HTTP-сервис | Кроссплатформенные сценарии, Docker-развёртывания | | JDBC Catalog | PostgreSQL / MySQL | Простые on-premise стенды | | Hadoop Catalog | Файловая система (HDFS) | Тестовые среды, legacy |

Catalog не хранит данные — он хранит указатель. Это ключевое отличие от монолитных СУБД: catalog можно заменить, не трогая ни данные, ни метаданные.

Metadata file — паспорт таблицы

Metadata file (или просто metadata) — JSON-файл, содержащий:

  • Схему таблицы (имена, типы, column ID)
  • Partition spec (правила партиционирования)
  • Список всех snapshot'ов данной таблицы
  • Ссылку на текущий (последний) snapshot
  • Историю схем и spec'ов (для schema/partition evolution)
  • Каждый metadata file — это полное описание таблицы на момент коммита. При каждом изменении таблицы (INSERT, DELETE, ALTER) создаётся новый metadata file, а catalog обновляет указатель. Старые metadata file не удаляются — они нужны для time travel.

    Snapshot — снимок состояния таблицы

    Snapshot — это неизменяемое состояние таблицы в конкретный момент времени. Каждый коммит порождает новый snapshot. Один snapshot ссылается на один manifest list.

    Snapshot содержит:

  • Уникальный snapshot ID
  • Timestamp коммита
  • Ссылку на manifest list
  • Опционально — summary с метриками (количество добавленных/удалённых строк и файлов)
  • > 1 manifest list — это 1 snapshot. Это важное сведение. Супер важное. > > habr.com

    Snapshot — это единица, на которой строятся ACID-изоляция и time travel. Движок всегда читает конкретный snapshot, а не «текущее состояние» — именно поэтому запросы воспроизводимы.

    Manifest list — оглавление снапшота

    Manifest list — Avro-файл, содержащий список manifest files, входящих в данный snapshot. Помимо путей к manifest files, он хранит агрегированную статистику: диапазоны значений partition columns для каждого manifest file. Это позволяет движку пропустить целые manifest files ещё до их чтения — первый уровень pruning.

    Manifest files — индекс файлов данных

    Manifest file — Avro-файл, описывающий набор data files. Для каждого data file в manifest хранятся:

  • Путь к data file
  • Partition data (значения partition columns)
  • File-level statistics: min/max/null count для каждого столбца
  • Column-level metrics (опционально, настраивается через write.metadata.metrics.*)
  • Именно manifest files позволяют реализовать file-level pruning: движок сравнивает min/max каждого файла с предикатом запроса и пропускает файлы, которые заведомо не содержат подходящих данных.

    Data files — сырые данные

    Data files — Parquet (или Avro/ORC) файлы с фактическими данными. Это единственный слой, который содержит строки таблицы. Iceberg не навязывает конкретный формат, но на практике Parquet де-факто стандарт благодаря колоночному хранению, встроенному сжатию и поддержке predicate pushdown.

    Как работает чтение: путь запроса

    Когда Trino выполняет SELECT * FROM events WHERE date = '2024-01-15', происходит следующее:

  • Catalog lookup — движок получает путь к metadata file таблицы events
  • Чтение metadata file — загружается схема, partition spec, список snapshot'ов
  • Выбор snapshot — определяется консистентный snapshot (последний или указанный для time travel)
  • Чтение manifest list — загружается список manifest files для этого snapshot
  • Pruning manifest list — по агрегированной статистике отбрасываются manifest files, не пересекающиеся с date = '2024-01-15'
  • Чтение manifest files — для оставшихся manifest files загружается информация о data files
  • Pruning data files — по min/max статистике каждого data file определяются только релевантные файлы
  • Сканирование data files — движок читает только отобранные Parquet-файлы
  • Двойной pruning (на уровне manifest list и manifest files) — это то, что делает Iceberg быстрым на таблицах с миллионами файлов. Движку не нужно перечислять каталоги: вся информация о файлах уже в метаданных.

    Как работает запись: путь коммита

    При INSERT INTO events VALUES (...) движок:

  • Пишет data files (Parquet) в object storage
  • Создаёт manifest file(s) с описанием новых data files и их статистикой
  • Создаёт manifest list, ссылающийся на manifest files (старые + новые)
  • Создаёт metadata file с обновлённым списком snapshot'ов
  • Атомарно обновляет catalog — указатель переключается на новый metadata file
  • Шаг 5 — это и есть атомарный коммит. Если два писателя пытаются закоммитить одновременно, только один победит (optimistic concurrency), а второй получит конфликт и повторит попытку. Данные, записанные проигравшим, остаются в storage как orphan files — их позже подчистит maintenance.

    Практический пример: что лежит на диске

    После нескольких INSERT-операций в таблицу analytics.events в S3 вы увидите примерно такую структуру:

    Каждый файл в директории metadata/ — это часть иерархии метаданных. Catalog хранит только одну строку: «таблица analytics.eventsv3.metadata.json». Всё остальное — цепочка ссылок вниз к data files.

    Неочевидные нюансы

    Metadata file не удаляются автоматически. Старые версии накапливаются и нужны для time travel. Без периодической очистки (expire_snapshots) директория metadata будет расти.

    Manifest files могут описывать data files из разных партиций. Один manifest file не обязательно содержит файлы только одной партиции — Iceberg группирует их для оптимизации, но это не жёсткое правило.

    Catalog — это единственная точка согласованности. Если catalog недоступен, новые запросы не смогут определить актуальный snapshot. При этом чтение по уже известному snapshot ID продолжит работать — metadata file и data files лежат в object storage независимо.

    2. ACID-транзакции, time travel и эволюция схемы: скрытое партиционирование и column-level изоляция

    ACID-транзакции, time travel и эволюция схемы: скрытое партиционирование и column-level изоляция

    Что произойдёт, если два ETL-джоба одновременно запишут данные в одну директорию S3? В классическом data lake — дубли, пропуски или битые файлы. В Hive до версии 3.0 — вообще никаких гарантий. Apache Iceberg решает эту проблему на уровне архитектуры: каждое изменение таблицы — это атомарный коммит поверх неизменяемых snapshot'ов, а схема таблицы эволюционирует без перезаписи данных.

    ACID через snapshot isolation

    Iceberg реализует snapshot isolation — модель изоляции транзакций, при которой каждая транзакция видит согласованный снимок данных, а записи не мешают чтению.

    Механизм работает так:

  • Движок читает текущий metadata file и определяет базовый snapshot
  • Пишет data files и manifest files в object storage
  • Пытается атомарно обновить указатель в catalog с условием: «текущий snapshot не изменился с момента моего чтения»
  • Если условие выполнено — коммит успешен. Если другой писатель успел раньше — optimistic concurrency conflict, и транзакция повторяется
  • Это optimistic locking — нет блокировок на чтение, нет блокировок на запись до момента коммита. Конфликты обнаруживаются только в финале, что даёт высокую пропускную способность при редких конфликтах.

    > Iceberg применяет MVCC: запись не мешает чтению; каждое чтение обращается к последнему актуальному снапшоту. > > habr.com

    Что гарантируется

  • Atomicity: коммит либо целиком применяется, либо не применяется. Нет «частично записанных» snapshot'ов
  • Consistency: каждый snapshot — валидное состояние таблицы со схемой и partition spec
  • Isolation: чтение всегда видит консистентный snapshot; параллельные писатели не создают грязное чтение
  • Durability: metadata file и data files лежат в object storage с собственными гарантиями durability (S3 — 99.999999999%)
  • Где ACID не спасает

    Iceberg гарантирует ACID на уровне одной таблицы. Кросс-табличные транзакции не поддерживаются из коробки — если вам нужна атомарная запись в две таблицы, потребуется внешняя оркестрация (например, через catalog с поддержкой multi-table transactions, что доступно в некоторых реализациях).

    Time travel: чтение любого snapshot'а

    Поскольку каждый snapshot — это неизменяемое состояние таблицы, а старые metadata file не удаляются, Iceberg естественным образом поддерживает time travel.

    Синтаксис в Spark SQL:

    Time travel полезен для:

  • Аудита: что именно содержалось в таблице на момент отчёта
  • Отладки: воспроизвести баг на данных, которые уже изменились
  • Rollback: вернуть таблицу к предыдущему состоянию через CALL rollback_to_snapshot
  • Reproducible queries: аналитик может гарантировать, что повторный запуск запроса даст тот же результат
  • Управление жизненным циклом snapshot'ов

    Snapshot'ы занимают место — каждый хранит manifest list и ссылается на data files, которые нельзя удалить, пока жив хотя бы один snapshot. Для управления этим Iceberg предоставляет процедуры:

    Без expire_snapshots таблица будет хранить все исторические версии навсегда — это быстро приведёт к metadata bloat.

    Эволюция схемы: column IDs вместо позиций

    В Hive изменение схемы — это боль: переименование столбца ломает чтение старых Parquet-файлов, потому что Hive полагается на позиционные индексы. Iceberg решает это через column IDs.

    Каждый столбец в схеме Iceberg имеет уникальный числовой column ID. Parquet-файлы хранят данные с привязкой к этим ID, а не к именам или позициям. Результат:

    | Операция | Hive | Iceberg | |---|---|---| | Добавить столбец | Перезаписать данные | Обновить metadata file | | Переименовать столбец | Перезаписать данные | Обновить metadata file | | Удалить столбец | Перезаписать данные | Обновить metadata file | | Изменить тип | Не поддерживается | Ограниченно (widening) |

    Данные не перезаписываются. Старые файлы продолжают читаться по column ID, а metadata file просто отображает новое имя.

    Schema evolution и time travel

    При запросе к старому snapshot'у движок использует схему из этого snapshot'а, а не текущую. Это значит, что SELECT * FROM events VERSION AS OF 8475390120 вернёт столбец с именем user_id, даже если в текущей схеме он называется customer_id.

    Скрытое партиционирование: partition transforms

    В Hive partitioning вы обязаны явно указывать partition columns в WHERE-клаузе, иначе движок сканирует все партиции. Более того, вы должны знать структуру каталогов: year=2024/month=01/day=15.

    Iceberg вводит hidden partitioning — партиционирование скрыто от пользователя. Вы задаёте partition transform, а Iceberg автоматически распределяет данные и использует partition pruning без участия пользователя.

    Доступные transforms

    | Transform | Описание | Пример результата | |---|---|---| | identity(x) | Значение как есть | region = 'us-east' | | year(ts) | Год из timestamp | 2024 | | month(ts) | Месяц из timestamp | 2024-01 | | day(ts) | День из timestamp | 2024-01-15 | | hour(ts) | Час из timestamp | 2024-01-15T10 | | bucket(n, col) | Хеш по N бакетам | 7 (из 256 бакетов) | | truncate(w, col) | Усечение строки до W символов | 'exampl' (при w=6) |

    Пользователь пишет:

    Iceberg автоматически определяет, что нужно читать только файлы из партиции month=2024-01, и применяет bucket-pruning по region. Пользователю не нужно знать о партиционировании — достаточно фильтра по исходной колонке.

    Partition spec evolution

    Iceberg позволяет менять стратегию партиционирования на лету:

    При этом старые данные остаются в прежней схеме партиционирования, а новые записываются по новой. Metadata file хранит все исторические spec'ы, и движок корректно обрабатывает файлы с разными spec'ами в одном запросе.

    > При изменении стратегии партиционирования существующие файлы остаются в прежней схеме, а к новым данным применяется новая стратегия. > > habr.com

    Copy-on-Write vs Merge-on-Read

    Iceberg поддерживает две стратегии обработки row-level изменений (UPDATE, DELETE, MERGE):

    Copy-on-Write (CoW) — при изменении строки перезаписывается весь data file. Быстрое чтение (нет дополнительной обработки), но дорогая запись.

    Merge-on-Read (MoR) — изменения записываются в отдельные delete files. Быстрая запись, но чтение должно «на лету» применить удаления к основным данным.

    | Критерий | CoW | MoR | |---|---|---| | Скорость записи | Медленнее (перезапись файлов) | Быстрее (append delete files) | | Скорость чтения | Быстрее (чистые данные) | Медленнее (merge при чтении) | | Когда использовать | Редкие UPDATE/DELETE | Частые точечные изменения, streaming |

    MoR требует периодического compaction — процедуры, которая применяет delete files к data files и создаёт чистые Parquet-файлы. Без compaction чтение будет деградировать по мере накопления delete files.

    Практический сценарий: GDPR-удаление

    Пользователь запросил удаление своих данных (right to be forgotten). С MoR это делается мгновенно:

    Iceberg создаёт equality delete file с правилом «удалить строки, где user_id = 12345». Чтение таблицы сразу отражает удаление. Позже compaction физически удалит эти строки из data files.

    Без Iceberg пришлось бы перезаписывать партиции целиком — на таблице с терабайтами данных это часы работы.

    3. Оптимизация производительности: сортировка при записи, управление метриками и векторизованное удаление

    Оптимизация производительности: сортировка при записи, управление метриками и векторизованное удаление

    Почему два запроса к одной и той же таблице Iceberg могут отличаться по скорости в 100 раз — без изменения SQL? Ответ кроется не в движке, а в том, как организованы файлы на диске и какие метрики по ним собраны. Iceberg даёт инженеру тонкие инструменты управления производительностью, но большинство команд ограничиваются базовым compaction'ом, упуская значительную часть потенциала.

    Сортировка при записи: превращаем случайные данные в упорядоченные

    Iceberg хранит в manifest files min/max значения по каждому столбцу для каждого data file. Движок использует эти диапазоны для pruning — если предикат запроса не пересекается с min/max файла, файл пропускается целиком.

    Проблема в том, что при обычной записи данные идут в случайном порядке. Каждый Parquet-файл содержит почти полный диапазон значений, и pruning не срабатывает — движок вынужден читать все файлы.

    Рассмотрим конкретный пример. Таблица с миллионом строк, записанная через repartition(16) без сортировки:

    Каждый файл покрывает почти весь диапазон id. Запрос WHERE id < 1000 прочитает все файлы.

    Теперь добавим сортировку:

    После повторной записи статистика становится полезной:

    Запрос WHERE id < 196470 теперь читает один файл вместо шестнадцати. Эффект масштабируется: чем больше таблица, тем сильнее выигрыш.

    > За счет данной оптимизации, если мы читаем данные с фильтрацией по id < 196470, мы прочитаем 1 data файл, вместо всей таблицы, что положительно скажется на выполнении аналитических запросов. > > habr.com

    Когда сортировка не нужна

    Сортировка стоит ресурсов — shuffle и sort при записи. Не стоит сортировать по столбцам, которые не используются в фильтрах. Оптимальные кандидаты — столбцы с высокой селективностью в типичных запросах: временные метки, ID сущностей, категориальные признаки с умеренной кардинальностью.

    Управление метриками: меньше статистики — быстрее запись

    По умолчанию Iceberg собирает полные метрики (min, max, null count, value count) для каждого столбца каждого data file. Это полезно для pruning, но имеет цену:

  • Запись: дополнительные вычисления при создании Parquet-файлов
  • Хранение: метрики занимают место в manifest files
  • Чтение manifest: больше данных для десериализации
  • Если таблица содержит 50 столбцов, но фильтрация идёт только по 2–3 из них, сбор метрик по остальным 47 — пустая трата.

    Iceberg позволяет настраивать метрики на уровне столбца:

    Уровни метрик:

    | Уровень | Что собирает | Стоимость | |---|---|---| | none | Ничего | Минимальная | | counts | null count, value count | Низкая | | truncate(N) | min/max усечённые до N символов | Средняя | | full | Полные min/max | Максимальная |

    > Для 8 колонок считается бесполезная статистика. Для того, чтобы исправить это недоразумение, необходимо настроить расчет статистик. > > habr.com

    Практическое правило

    Ставьте full только на столбцы, которые реально используются в WHERE-фильтрах. Для остальных — counts или none. На широких таблицах (50+ столбцов) это даёт ощутимый прирост скорости записи без потери качества pruning.

    Векторизованное удаление: от position delete к deletion vectors

    При Merge-on-Read стратегии Iceberg создаёт delete files, которые логически помечают строки как удалённые. Качество чтения зависит от того, насколько эффективно движок применяет эти удаления.

    Position delete (format v2)

    Position delete file хранит пары (file_path, row_position) — какие именно строки в каких data files удалены. При чтении движок выполняет join между data file и delete file по позиции строки.

    Проблема: join дорогой. При большом количестве удалений чтение деградирует.

    Deletion vectors (format v3)

    Начиная с Iceberg format v3, position delete эволюционировал в deletion vectors — битовые маски, хранящиеся в Puffin файлах.

    Вместо join'а движок загружает компактный bitmap и применяет его к data file за одну побитовую операцию. Это принципиально быстрее:

    | Аспект | Position delete | Deletion vector | |---|---|---| | Формат хранения | Avro-файл с парами | Bitmap в Puffin-файле | | Применение при чтении | Join по позиции | Побитовая маска | | Стоимость при 1% удалений | Заметная | Пренебрежимая | | Стоимость при 50% удалений | Критическая | Умеренная |

    Equality delete (хранение значений для удаления, а не позиций) в v3 признан deprecated — рекомендуется использовать deletion vectors.

    Стратегии оптимизации чтения

    Помимо сортировки и метрик, есть дополнительные приёмы ускорения запросов.

    Predicate pushdown в Parquet

    Iceberg передаёт предикаты движку Parquet, который применяет row group filtering и page index filtering внутри файла. Если min/max row group'а не пересекается с предикатом — row group пропускается без чтения.

    Column projection

    Iceberg передаёт в движок только запрошенные столбцы. Parquet хранит данные колоночно, поэтому чтение подмножества столбцов радикально снижает I/O.

    Data file sizing

    Размер data files влияет на баланс между параллелизмом и overhead'ом:

  • Слишком мелкие файлы ( 1 GB) — плохой параллелизм, долгий single-file scan
  • Целевой размер — 128–512 MB на файл. Это настраивается через write.target-file-size-bytes.

    Практический чек-лист оптимизации

  • Определите столбцы фильтрации — по ним сортируйте при записи и ставьте full метрики
  • Отключите метрики для столбцов, не участвующих в WHERE
  • Настройте размер файлов под ваш движок (128–512 MB)
  • Используйте MoR для частых точечных изменений, но не забывайте про compaction
  • Следите за ratio delete files — если удалений > 10–20%, пора запускать compaction
  • 4. Интеграция с вычислительными движками: Spark, Trino, Flink и единый формат метаданных

    Интеграция с вычислительными движками: Spark, Trino, Flink и единый формат метаданных

    Главная обещание Apache Iceberg — одна таблица, несколько движков. Записали данные через Spark Streaming, запросили через Trino, обновили через Flink — и все видят одно и то же состояние. Но как это работает на практике, если у Spark свой планировщик, у Trino — свой, а у Flink — вообще streaming-семантика? Разберёмся, что обеспечивает совместимость и где она ломается.

    Единый контракт: спецификация как источник истины

    Iceberg — это открытая спецификация, а не привязанная к конкретному движку библиотека. Любой движок, реализующий эту спецификацию, может читать и писать таблицы. Ключевой принцип: метаданные — это контракт. Metadata file, manifest list, manifest files — всё это формализованные структуры с чётким форматом (JSON, Avro), которые любой движок интерпретирует одинаково.

    Когда Spark записывает data file и создаёт manifest, Trino видит этот manifest и точно так же использует его min/max статистику для pruning. Нет «приватных» метаданных, которые понимает только один движок.

    Apache Spark: основной движок для batch и streaming

    Spark — наиболее зрелая интеграция с Iceberg. Библиотека iceberg-spark-runtime предоставляет полный набор операций.

    Настройка

    Batch-операции

    Spark поддерживает полный SQL-синтаксис Iceberg:

    Streaming через Structured Streaming

    Spark Structured Streaming может писать в Iceberg-таблицы через micro-batch и continuous режимы:

    Важный нюанс: каждый micro-batch создаёт отдельный snapshot. При высокой частоте триггеров (например, каждые 10 секунд) это приводит к explosion of snapshots и small files. Решение — настройка trigger на разумный интервал и регулярный compaction.

    Производительность Spark + Iceberg

    Spark использует vectorized reader для Parquet-файлов, что даёт значительный прирост при сканировании. Дополнительные оптимизации:

  • Распределённое чтение manifest: Spark параллельно читает manifest files на executor'ах
  • Dynamic partition pruning: при join'ах Spark пропускает партиции на основе данных из другой таблицы
  • AQE (Adaptive Query Execution): Spark адаптирует план на основе статистики, собранной во время выполнения
  • Trino: SQL-движок для интерактивной аналитики

    Trino (ранее PrestoSQL) — распределённый SQL-движок без собственного хранилища. Iceberg connector позволяет Trino запрашивать Iceberg-таблицы как обычные SQL-таблицы.

    Настройка

    Файл etc/catalog/iceberg.properties:

    Особенности Trino + Iceberg

    Trino оптимизирован для интерактивных запросов с низкой латентностью. Его интеграция с Iceberg имеет ряд нюансов:

  • Cost-based optimizer использует статистику из manifest files для выбора плана join'а
  • Table statistics можно обновлять через ANALYZE TABLE
  • Оптимизация через ALTER TABLE EXECUTE: Trino поддерживает процедуры maintenance прямо из SQL
  • Где Trino уступает Spark

    Trino не поддерживает запись в Iceberg через streaming и имеет ограниченную поддержку MERGE-операций в старых версиях. Для batch-загрузки данных Spark обычно предпочтительнее, а Trino — для ad-hoc аналитики.

    Apache Flink: streaming-first интеграция

    Flink — движок потоковой обработки с exactly-once семантикой. Интеграция Flink + Iceberg позволяет материализовать Kafka-топики в Iceberg-таблицы с ACID-гарантиями.

    Настройка

    Streaming sink

    Flink записывает данные в Iceberg с настраиваемой частотой коммитов через commit-interval-ms. Каждый коммит создаёт snapshot.

    Преимущества Flink + Iceberg

  • Exactly-once через checkpoint: Flink коммитит в Iceberg только после успешного checkpoint'а
  • Low-latency ingestion: данные доступны для запросов через секунды после появления в Kafka
  • Schema evolution из Kafka: Flink может автоматически адаптироваться к изменениям схемы в Avro/Protobuf
  • Ловушки

  • Small files: Flink при высоком параллелизме создаёт много мелких файлов. Обязателен регулярный compaction
  • Commit latency: слишком частые коммиты нагружают catalog и создают много snapshot'ов
  • Backpressure: при замедлении записи в Iceberg (например, из-за медленного S3) Flink может накапливать данные в state, что ведёт к росту потребления памяти
  • Совместимость движков: что работает, а что нет

    | Возможность | Spark | Trino | Flink | |---|---|---|---| | Чтение таблиц | ✅ | ✅ | ✅ | | INSERT | ✅ | ✅ | ✅ | | MERGE / UPDATE / DELETE | ✅ | ✅ | ❌ | | Streaming запись | ✅ (micro-batch) | ❌ | ✅ (native) | | Schema evolution (DDL) | ✅ | ✅ | ✅ | | Partition evolution | ✅ | ✅ | ❌ | | Time travel | ✅ | ✅ | ❌ | | Maintenance процедуры | ✅ | ✅ | ❌ | | Hidden partitioning | ✅ | ✅ | ✅ |

    Все три движка корректно читают данные, записанные друг другом — это и есть ключевое преимущество единого формата метаданных. Однако операции maintenance (compaction, expire snapshots) обычно выполняются через Spark или Trino, даже если данные записываются через Flink.

    Единый формат метаданных: почему это важно

    В архитектуре без Iceberg каждая система хранит свои метаданные: Hive — в metastore, Spark — в журнале транзакций, Kafka — в своих топиках. Результат — рассогласование, дублирование и сложная синхронизация.

    Iceberg выносит метаданные в единый формат поверх object storage. Любой движок видит одни и те же metadata file, manifest files и data files. Это даёт:

  • Zero-copy data sharing: данные не копируются между системами
  • Consistent reads: один и тот же snapshot гарантирует одинаковый результат в любом движке
  • Independent scaling: можно масштабировать Trino для аналитики и Flink для ingestion независимо
  • 5. Лучшие практики эксплуатации: compaction, small files, metadata scaling и стратегии в lakehouse

    Лучшие практики эксплуатации: compaction, small files, metadata scaling и стратегии в lakehouse

    Таблица Iceberg, которая летала на старте, через полгода может сканировать в 10 раз больше файлов, а планирование запроса занимать минуты вместо секунд. Причина не в Iceberg как таковом, а в отсутствии операционной дисциплины: без регулярного maintenance даже лучший табличный формат деградирует. Разберём, что именно ломается и как этому противостоять.

    Проблема small files: как она возникает

    Small files — это data files размером значительно ниже целевого (типично MB). Каждый small file — это:

  • Отдельный запрос к object storage (latency ~50–100 ms на S3)
  • Отдельная запись в manifest file (рост метаданных)
  • Накладные расходы на инициализацию Parquet reader'а
  • Источники small files:

    | Источник | Механизм | |---|---| | Streaming ingestion | Каждый micro-batch или checkpoint создаёт файлы по числу партилций writer'а | | Высокий parallelism | Spark с 200 shuffle-партициями создаёт до 200 файлов за INSERT | | Мелкие партиции | При партиционировании по дням вчера было 100 событий — один файл в 1 KB | | MERGE/UPDATE (CoW) | Перезапись файла с одной изменённой строкой — новый файл того же размера |

    Диагностика

    Проверить распределение размеров файлов можно через системные таблицы:

    Если avg_size_mb и file_count в десятки тысяч — у вас проблема.

    Compaction: стратегии и автоматизация

    Compaction (или rewrite data files) — процедура объединения мелких файлов в крупные. Это главная операция maintenance в Iceberg.

    Ручной запуск

    Стратегии compaction

    Full rewrite — перезаписывает все файлы в партиции. Максимальный эффект, но дорогой по I/O. Подходит для небольших таблиц или при сильной фрагментации.

    Binpack — объединяет мелкие файлы, не трогая крупные. Баланс между эффективностью и стоимостью. Стратегия по умолчанию в Spark.

    Sort — compaction с одновременной сортировкой. Дороже binpack, но даёт лучший pruning на последующих запросах.

    Автомизация

    Compaction должен быть регулярным, а не по факту проблем. Типичные подходы:

  • По расписанию: Airflow/Dagster-джоб, запускаемый раз в сутки в окне низкой нагрузки
  • По порогу: мониторинг количества файлов в партиции, alert при превышении порога
  • После streaming-окна: compaction запускается после завершения каждого часа/дня ingestion
  • Metadata scaling: когда метаданные становятся проблемой

    При миллионах data files manifest files разрастаются, manifest list'ы становятся длинными, и планирование запроса (query planning) начинает занимать значительное время.

    Симптомы

  • Запрос, который раньше планировался за 2 секунды, теперь планируется 30+
  • Рост потребления памяти на coordinator'е (Spark driver, Trino coordinator)
  • Увеличение времени list-операций в catalog
  • Причины

  • Накопление snapshot'ов без expire_snapshots
  • Слишком много small files → много записей в manifest files
  • Отсутствие rewrite_manifests
  • Решения

    Expire snapshots — удаление старых snapshot'ов и связанных с ними metadata file:

    Rewrite manifests — перезапись manifest files для уменьшения их количества. После compaction старые manifest files остаются (они описывают старые data files). rewrite_manifests создаёт новые компактные manifest files:

    Remove orphan files — удаление файлов в storage, которые не привязаны ни к одному snapshot'у (остались после неудачных коммитов или ручного удаления):

    Рекомендуемый порядок maintenance

  • rewrite_data_files (compaction) — сначала объединяем файлы
  • rewrite_manifests — затем оптимизируем метаданные
  • expire_snapshots — удаляем старые версии
  • remove_orphan_files — чистим мусор
  • Порядок важен: если сначала expire'ить snapshot'ы, а потом делать compaction, вы можете удалить snapshot'ы, которые ещё нужны для time travel к данным до compaction.

    Настройка размера файлов

    Целевой размер data files — один из самых влиятельных параметров. Он задаётся через table property:

    Рекомендации по выбору:

    | Размер таблицы | Целевой размер файла | Обоснование | |---|---|---| | GB | 128 MB | Быстрый scan, много параллелизма | | 100 GB – 1 TB | 256–512 MB | Баланс I/O и параллелизма | | | 512 MB – 1 GB | Минимизация количества файлов |

    Не ставьте слишком большой размер: один файл GB — это плохой параллелизм и долгий single-task scan.

    Стратегии партиционирования в lakehouse

    Выбор partition spec определяет производительность на годы вперёд. Типичные ошибки и их решения.

    Over-partitioning

    Партиционирование по столбцу с высокой кардинальностью (например, user_id) создаёт тысячи мелких партиций. Решение — bucket transform:

    Under-partitioning

    Партиционирование только по году при ежедневной загрузке терабайт — каждый запрос сканирует весь год. Решение — добавить гранулярность:

    Evolving partitioning

    Iceberg позволяет менять partition spec без миграции данных. Начните с грубого партиционирования, уточняйте по мере роста:

    Стратегия lakehouse: когда что использовать

    Iceberg — это формат, а не решение. В lakehouse-архитектуре нужно чётко понимать роль каждого слоя:

    | Слой | Формат | Задача | Движок | |---|---|---|---| | Raw / Landing | Parquet (без Iceberg) | Приём сырых данных | Flink / Spark Streaming | | Curated / Silver | Iceberg | Очищенные, валидированные данные | Spark batch | | Analytics / Gold | Iceberg + materialized views | Агрегаты для BI | Trino / StarRocks | | Serving | Кэш (Redis, материализованные представления) | Low-latency API | Прикладной сервис |

    На уровне Curated и Analytics — Iceberg с регулярным maintenance. На уровне Serving — данные извлекаются из Iceberg и кэшируются отдельно.

    Мониторинг: что отслеживать

    | Метрика | Источник | Порог тревоги | |---|---|---| | Количество data files на партицию | table.files | файлов на партицию | | Средний размер файла | table.files | MB | | Количество snapshot'ов | table.snapshots | без expire | | Количество manifest files | table.manifests | без rewrite | | Время планирования запроса | Движок (Spark UI / Trino UI) | секунд | | Orphan files | remove_orphan_files dry run | после 7 дней |

    Регулярный мониторинг этих метрик позволяет обнаружить деградацию до того, как она станет заметна пользователям.