Learning Platform
Глоссарий Troubleshooting
Урок 10.05 · 23 мин
Средний
icebergoptimizemaintenancecompaction

Обслуживание таблиц: OPTIMIZE, expire_snapshots, remove_orphan_files

Из уроков 3 и 4 мы вынесли две вещи. Файлы данных Iceberg неизменяемы — каждый INSERT создаёт новые файлы. И снапшоты неизменяемы — они накапливаются в истории. Оба свойства дают атомарность и time travel, но имеют цену: таблица со временем «деградирует» — обрастает мелкими файлами и старыми снапшотами. Без обслуживания запросы замедляются, а счёт за object storage растёт. Iceberg-таблица — не «настроил и забыл», а живой объект, требующий регулярного ухода. Этот урок разбирает три ключевые процедуры обслуживания и то, какую конкретную деградацию каждая лечит.

Почему таблица деградирует

Две независимые проблемы накапливаются параллельно.

Проблема мелких файлов. Стриминговая загрузка или частые мелкие INSERT создают много маленьких Parquet-файлов — вспомните: каждый INSERT пишет новый файл, дописать в существующий нельзя. Через месяц у партиции вместо десяти файлов по 128 МБ могут быть тысячи файлов по 200 КБ. Это бьёт по чтению напрямую. Координатор генерирует сплит на файл (или его часть) — тысячи мелких файлов означают тысячи мелких сплитов, и накладные расходы на планирование и открытие файлов начинают доминировать над полезной работой. Каждый Parquet-файл — это ещё и footer с метаданными, который надо прочитать; для крошечного файла footer может быть сопоставим с данными. Колоночное сжатие на мелких файлах тоже хуже.

Проблема накопления метаданных. Каждое изменение создаёт снапшот, а старые снапшоты по умолчанию не удаляются. Их файлы данных, манифесты и manifest list остаются в object storage. Через год активной таблицы история — это десятки тысяч снапшотов и петабайты файлов, которые уже никем не читаются, но за хранение которых вы платите. Растёт и сам metadata-файл, в котором перечислены все снапшоты.

Две оси деградации Iceberg-таблицы
Мелкие файлыЧастые INSERT и стриминг плодят тысячи маленьких Parquet-файлов; растёт число сплитов и накладные расходы на чтение
лечит OPTIMIZE
Старые снапшотыНакопленная история снапшотов с их файлами данных и манифестами занимает место в object storage, за которое идёт плата
лечит expire_snapshots, затем remove_orphan_files
Здоровая таблицаКрупные файлы оптимального размера и компактная история — быстрые запросы и контролируемый счёт за хранение

Все процедуры обслуживания вызываются через ALTER TABLE ... EXECUTE <процедура>(параметры).

Apache Iceberg: обслуживание таблиц — compaction и snapshot expiry

OPTIMIZE: компакция мелких файлов

OPTIMIZE решает проблему мелких файлов. Процедура читает мелкие файлы данных и переписывает их содержимое в меньшее число крупных файлов. Логических изменений в данных нет — те же строки, но упакованные эффективно. Как любое изменение, компакция создаёт новый снапшот.

-- Скомпактить всю таблицу: мелкие файлы переписываются в крупные
ALTER TABLE iceberg.sales.orders EXECUTE optimize;

Ключевой параметр — file_size_threshold (по умолчанию 100MB). Процедура трогает только файлы меньше порога; файлы, уже достигшие хорошего размера, она не переписывает зря. Это важно: компакция — дорогая операция, она читает и пишет данные, и переписывать уже крупные файлы было бы пустой тратой I/O.

-- Компактить только файлы меньше 256 МБ
ALTER TABLE iceberg.sales.orders
  EXECUTE optimize(file_size_threshold => '256MB');

На больших таблицах компактить всё сразу нерационально — обычно деградирует только свежая партиция, в которую активно льются данные. OPTIMIZE принимает WHERE для ограничения области по партициям:

-- Компактить только вчерашнюю партицию, в которую шла загрузка
ALTER TABLE iceberg.sales.orders
  EXECUTE optimize
  WHERE order_date = DATE '2026-05-19';

Эффект виден через metadata-таблицу $files — до и после компакции:

-- До OPTIMIZE: много мелких файлов от стриминга
SELECT count(*) AS files, sum(record_count) AS rows,
       avg(file_size_in_bytes) AS avg_bytes
FROM iceberg.sales."orders$files";
--  files |  rows   | avg_bytes
-- -------+---------+-----------
--   1843 | 2950000 |     21400

-- После ALTER TABLE EXECUTE optimize: те же строки, мало крупных файлов
--  files |  rows   | avg_bytes
-- -------+---------+------------
--     24 | 2950000 | 122600000

Строк столько же (2 950 000), а файлов — 24 вместо 1843. Следующий SELECT сгенерирует 24 сплита вместо 1843.

NOTE

Родственная процедура — optimize_manifests. OPTIMIZE компактит файлы данных; optimize_manifests перезаписывает сами манифесты, кластеризуя записи по партиционирующим столбцам. Это ускоряет уже не чтение данных, а планирование запроса: координатору проще отбрасывать манифесты на этапе чтения manifest list. Нужна на таблицах с очень большим числом манифестов.

expire_snapshots: удаление старой истории

OPTIMIZE создал 24 новых файла. Но 1843 старых файла никуда не делись — на них всё ещё ссылаются прошлые снапшоты, а значит, time travel к ним по-прежнему работает. Пока эти снапшоты живы, старые файлы нельзя удалить: это сломало бы time travel. Удаление старых файлов данных — задача expire_snapshots.

Процедура удаляет снапшоты старше заданного возраста, а вместе с ними — файлы данных, манифесты и manifest list, на которые ссылались только эти снапшоты (и больше никто).

-- Удалить снапшоты старше срока retention по умолчанию
ALTER TABLE iceberg.sales.orders EXECUTE expire_snapshots;

-- Явно задать порог: удалить всё старше 3 дней
ALTER TABLE iceberg.sales.orders
  EXECUTE expire_snapshots(retention_threshold => '3d');

Здесь — критически важная защита. У expire_snapshots есть минимальный retention (по умолчанию 7 дней): процедура не даст удалить совсем свежие снапшоты, даже если вы попросите. Причина — параллельные операции и недавние записи могут ещё на них опираться, а слишком агрессивный expire мог бы оторвать данные у идущего запроса. Порог регулируется свойством таблицы, но осознанно понижать его ниже разумного не стоит.

Вторая защита: снапшот, на который указывает ветка или тег, не удаляется независимо от возраста. Это та самая причина, по которой в уроке 4 тег назывался способом «защитить» состояние от обслуживания. Голова любой ветки (включая main) и любой тег всегда переживают expire.

WARNING

Порядок имеет значение. Сначала OPTIMIZE (создаёт новые файлы, старые становятся не нужны новым снапшотам), затем expire_snapshots (удаляет старые снапшоты и осиротевшие вместе с ними старые файлы). Если сделать только OPTIMIZE без последующего expire — счёт за storage не уменьшится, потому что старые файлы держатся прошлыми снапшотами. OPTIMIZE без expire ускоряет чтение, но не освобождает место.

remove_orphan_files: уборка мусора вне метаданных

Третья процедура чинит особую ситуацию — orphan-файлы, файлы-сироты. Это файлы, физически лежащие в директории таблицы в object storage, но не упомянутые ни в одном снапшоте — то есть невидимые для метаданных Iceberg вообще.

Откуда они берутся? Вспомните урок 2: запись в Iceberg — это оптимистичная конкуренция. Запрос сначала пишет Parquet-файлы в object storage, и только потом пытается атомарно закоммитить снапшот. Если коммит проиграл гонку или процесс упал между записью файлов и коммитом — Parquet-файлы остались, а снапшот не создан. Эти файлы не принадлежат ни одному снапшоту: их не удалит expire_snapshots, потому что expire ходит по снапшотам, а сироты вне снапшотов. Они просто лежат и занимают место.

-- Удалить файлы в директории таблицы, не связанные ни с одним снапшотом
ALTER TABLE iceberg.sales.orders
  EXECUTE remove_orphan_files(retention_threshold => '7d');

remove_orphan_files сравнивает фактическое содержимое директории таблицы со списком файлов из всех снапшотов и удаляет лишнее. У неё тоже есть минимальный retention — и он критичен по особой причине. Сирота и недокоммиченный файл идущей прямо сейчас записи выглядят одинаково: и тот и другой — Parquet в директории без снапшота. Retention-порог защищает свежие файлы: процедура удаляет только то, что лежит без снапшота дольше порога, и не трогает файлы активной записи. Понижать этот порог особенно опасно — можно удалить данные коммита, который ещё не успел завершиться.

Три процедуры лечат три разные проблемы
OPTIMIZEКомпактит мелкие файлы данных в крупные; лечит замедление чтения; не освобождает место само по себе
expire_snapshotsУдаляет старые снапшоты и файлы, на которые ссылались только они; освобождает место, занятое историей
remove_orphan_filesУдаляет файлы в директории таблицы, не связанные ни с одним снапшотом — мусор от упавших или проигравших записей

Регламент обслуживания

На практике три процедуры ставят в расписание (cron, Airflow), типичный регламент:

ПроцедураЧто лечитЧастотаНа что смотреть
OPTIMIZE с WHERE по свежим партициямМелкие файлыЕжедневно/ежечасно для активных партицийЧисло и средний размер файлов в $files
expire_snapshotsНакопленную историю снапшотовЕжедневно или еженедельноЧисло строк в $snapshots, объём storage
remove_orphan_filesФайлы-сироты от сбоев записиРеже, еженедельноРасхождение размера директории и суммы $files

Базовый принцип: OPTIMIZE адресно по горячим партициям, затем expire_snapshots, и периодически — remove_orphan_files. Без обслуживания даже корректно спроектированная Iceberg-таблица через несколько месяцев станет медленной и дорогой.

Как обслуживание связано с записью данных

Полезно понимать, что потребность в обслуживании напрямую зависит от того, как в таблицу пишут данные, — и инженер может уменьшить деградацию ещё на этапе записи.

Главный фактор — размер порции записи. Один INSERT на 10 миллионов строк создаст несколько крупных файлов хорошего размера — такую таблицу почти не надо компактить. Десять тысяч INSERT по тысяче строк создадут десять тысяч мелких файлов — и потребуют агрессивного OPTIMIZE. Поэтому первое правило здоровой Iceberg-таблицы: писать большими порциями. Если источник по своей природе потоковый и порции мелкие, OPTIMIZE по свежим партициям должен идти часто, фактически как часть пайплайна загрузки.

Второй фактор — DML с удалением строк. DELETE, UPDATE и MERGE в Iceberg v2 не переписывают файлы данных, а создают delete-файлы (о версиях формата — в уроке 7). Это быстро, но delete-файлы накапливаются: чем их больше, тем дороже чтение, потому что движок применяет их поверх данных. OPTIMIZE решает и эту проблему — при компакции он схлопывает данные вместе с delete-файлами в чистые файлы. Поэтому таблица, в которую часто пишут MERGE (например, upsert из CDC-потока), нуждается в OPTIMIZE не только из-за мелких файлов, но и чтобы убирать накопленные delete-файлы.

Вывод: обслуживание и запись — две стороны одного процесса. Чем «грубее» пишут в таблицу (мелкими порциями, частым DML), тем интенсивнее должно быть обслуживание. Корректно спроектированный пайплайн закладывает OPTIMIZE и expire_snapshots в расписание с самого начала, а не вспоминает о них, когда таблица уже деградировала.

Попробуй сам

В песочнице создайте iceberg.sales.orders и сымитируйте стриминговую загрузку: выполните 30-50 отдельных INSERT по 1-3 строки — так вы получите много мелких файлов. Снимите метрику: SELECT count(*), avg(file_size_in_bytes) FROM "orders$files". Выполните ALTER TABLE EXECUTE optimize и снимите метрику снова — зафиксируйте, как изменилось число и размер файлов. Теперь главное упражнение на понимание: посмотрите orders$snapshots после OPTIMIZE и объясните, почему общее число файлов в object storage пока не уменьшилось, хотя «полезных» файлов стало мало. Затем выполните expire_snapshots с малым (но не нарушающим минимальный retention) порогом и проследите по $snapshots, сколько снапшотов осталось. Письменно ответьте: в каком порядке нужно запускать три процедуры обслуживания и что произойдёт, если делать только OPTIMIZE и никогда не делать expire_snapshots.


Проверка знанийKnowledge check
Зачем Iceberg-таблице нужны три отдельные процедуры обслуживания — OPTIMIZE, expire_snapshots и remove_orphan_files — и почему важен порядок их запуска?
ОтветAnswer
Каждая процедура лечит свою отдельную проблему деградации, которая накапливается из-за неизменяемости файлов и снапшотов. OPTIMIZE решает проблему мелких файлов: частые INSERT и стриминг плодят тысячи маленьких Parquet-файлов, потому что дописать в существующий файл нельзя, и это замедляет чтение — растёт число сплитов и накладные расходы. OPTIMIZE переписывает мелкие файлы в крупные, не меняя данные логически. expire_snapshots решает проблему накопления истории: старые снапшоты по умолчанию не удаляются, и их файлы данных с манифестами занимают место в object storage, за которое идёт плата. expire_snapshots удаляет снапшоты старше заданного возраста вместе с файлами, на которые ссылались только они. remove_orphan_files убирает файлы-сироты — Parquet-файлы, физически лежащие в директории таблицы, но не упомянутые ни в одном снапшоте; они появляются, когда запись записала файлы, но проиграла гонку коммита или процесс упал до коммита. Порядок важен потому, что OPTIMIZE создаёт новые файлы, после чего старые становятся не нужны новым снапшотам, но всё ещё держатся прошлыми снапшотами — поэтому сначала OPTIMIZE, затем expire_snapshots, который и удалит осиротевшие старые файлы вместе со старыми снапшотами. Если делать только OPTIMIZE без expire_snapshots, счёт за storage не уменьшится: чтение ускорится, но место не освободится, так как старые файлы держатся прошлыми снапшотами. У expire_snapshots и remove_orphan_files есть минимальный retention, защищающий свежие файлы и снапшоты от удаления, потому что их могут ещё использовать идущие или недавние записи.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Почему множество мелких Parquet-файлов в Iceberg-таблице замедляет запросы на чтение?

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

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

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

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