Обслуживание, экосистема и версии спецификации
Iceberg-таблица — живая система. Каждый коммит создаёт новый metadata file, manifest list, потенциально delete files. Без обслуживания таблица деградирует: растёт число мелких файлов, накапливаются устаревшие snapshots, orphan files занимают storage.
В этом уроке — три блока: обслуживание (snapshot expiry, orphan cleanup, compaction), версии спецификации (V1→V2→V3 и перспективы), экосистема каталогов и runtime (REST Catalog, Polaris, Nessie, Gravitino, Lakekeeper, iceberg-rust).
Примеры кода — на Python с pyiceberg 0.11.1 (текущий релиз, март 2026) и Spark SQL (для maintenance procedures). Данные по экосистеме актуальны на март 2026.
Snapshot expiry
Каждый коммит создаёт новый snapshot. Snapshots иммутабельны и хранятся до явного удаления. Без expiry:
- Metadata file растёт (массив
snapshots[]+snapshot-log[]) - Manifest lists и manifest files от старых snapshots занимают storage
- Data files, которые были replaced/deleted, не удаляются физически
Механика
# pyiceberg: информация о snapshots
from pyiceberg.catalog import load_catalog
catalog = load_catalog("my_catalog")
table = catalog.load_table("db.orders")
# Число snapshots
snapshots = table.metadata.snapshots
print(f"Snapshots: {len(snapshots)}")
# → Snapshots: 1247 (после года CDC)
Expiry удаляет snapshots старше заданного порога и физически удаляет файлы, которые стали unreachable:
-- Spark: удалить snapshots старше 5 дней
CALL catalog.system.expire_snapshots(
table => 'db.orders',
older_than => TIMESTAMP '2026-03-22 00:00:00',
retain_last => 10 -- оставить минимум 10 последних
);
Snapshot expiry безвозвратна. После expiry time travel к удалённым snapshots невозможен. Для compliance-данных используйте tags (иммутабельные ссылки на snapshot), чтобы защитить конкретные snapshots от expiry.
Автоматический expiry
-- Настроить auto-expiry через свойства таблицы
ALTER TABLE db.orders SET TBLPROPERTIES (
'history.expire.max-snapshot-age-ms' = '432000000', -- 5 дней
'history.expire.min-snapshots-to-keep' = '10'
);
Движки (Spark, Flink) проверяют эти свойства и автоматически запускают expiry при каждом коммите.
Orphan file cleanup
Orphan files — файлы на storage, на которые не ссылается ни один snapshot. Возникают при:
- Прерванных записях — writer начал писать data file, но упал до коммита
- Compaction race conditions — compaction переписал файлы, но старые не удалились
- Ошибках планирования — файл создан, но не добавлен в manifest
-- Spark: найти и удалить orphan files
CALL catalog.system.remove_orphan_files(
table => 'db.orders',
older_than => TIMESTAMP '2026-03-20 00:00:00',
dry_run => true -- сначала посмотреть, потом удалять
);
Все файлы на storage
Шаг 1: собрать множество ВСЕХ файлов на storage (листинг S3/GCS). Это все .parquet, .avro, .metadata.json файлы в директории таблицы.Файлы из metadata
Шаг 2: собрать множество файлов, на которые ссылаются ALL snapshots (включая expired). Это все файлы, которые 'известны' Iceberg.Всегда используйте older_than с запасом (минимум 24 часа). Без этого можно удалить файл, который writer ещё не закоммитил — операция записи занимает время, и файл на storage появляется раньше, чем commit в metadata.
Compaction (rewrite data files)
Small file problem — самая частая проблема производительности Iceberg-таблиц. Streaming-инъекция (Flink, Kafka) создаёт тысячи мелких файлов (1-10 MB вместо оптимальных 256-512 MB).
Bin-packing compaction
Объединяет мелкие файлы в крупные без изменения порядка данных:
-- Spark: bin-packing compaction
CALL catalog.system.rewrite_data_files(
table => 'db.orders',
strategy => 'binpack',
options => map(
'target-file-size-bytes', '268435456', -- 256 MB
'min-file-size-bytes', '67108864', -- 64 MB (не трогать файлы > 64 MB)
'max-file-size-bytes', '536870912', -- 512 MB
'min-input-files', '5' -- минимум 5 файлов для merge
)
);
200 файлов × 5 MB = 1 GB
До compaction: 200 мелких файлов по 5 MB каждый = 1 GB данных в 200 файлах. Каждый file open, metadata read, Parquet footer parse — overhead на каждый файл.4 файла × 256 MB = 1 GB
После bin-packing: 4 файла по 256 MB. Те же данные, но в оптимальных файлах. Меньше file opens, меньше metadata reads, лучше column chunk alignment в Parquet.Sort-based compaction
Объединяет файлы и пересортировывает данные по указанным колонкам. Улучшает data skipping (min/max статистика плотнее):
CALL catalog.system.rewrite_data_files(
table => 'db.orders',
strategy => 'sort',
sort_order => 'order_date ASC, region ASC',
options => map('target-file-size-bytes', '268435456')
);
Z-ORDER compaction
Пересортировка по нескольким колонкам одновременно (Z-ORDER/Hilbert curve). Полезно, когда запросы фильтруют по разным комбинациям колонок:
CALL catalog.system.rewrite_data_files(
table => 'db.orders',
strategy => 'sort',
sort_order => 'zorder(customer_id, order_date)',
options => map('target-file-size-bytes', '268435456')
);
Rewrite delete files
V2 таблицы с множеством position delete files нужно компактировать:
-- Компактировать delete files
CALL catalog.system.rewrite_position_delete_files(
table => 'db.orders'
);
В V3 с deletion vectors проблема delete file proliferation решена на уровне спецификации: writer обязан merge DVs. Отдельный rewrite_position_delete_files нужен только для V2 таблиц или после upgrade V2→V3.
Рекомендуемый maintenance pipeline
┌─────────────────────────────────────────────┐
│ Ежечасно (для streaming-таблиц): │
│ > rewrite_data_files (binpack, >5 files) │
│ > rewrite_position_delete_files (V2) │
├─────────────────────────────────────────────┤
│ Ежедневно: │
│ > expire_snapshots (older_than = 5 days) │
│ > remove_orphan_files (older_than = 3 days)│
├─────────────────────────────────────────────┤
│ Еженедельно: │
│ > rewrite_data_files (sort-based) │
│ > compute_partition_stats (incremental) │
└─────────────────────────────────────────────┘
Версии спецификации
V1 → V2 → V3
V3 feature mapping
| Feature | V1 | V2 | V3 |
|---|---|---|---|
| Metadata hierarchy | |||
| Hidden partitioning | |||
| Schema evolution (field ID) | |||
| Copy-on-Write | |||
| Position delete files | — | ! Deprecated (read-only) | |
| Equality delete files | — | ||
| Sequence numbers | — | ||
| Branches/tags | — | ||
| Deletion vectors | — | — | |
| Row lineage | — | — | |
| Default column values | — | — | |
| VARIANT type | — | — | |
| Geospatial types | — | — | |
| Nanosecond timestamps | — | — |
Upgrade V2 → V3
-- Upgrade существующей таблицы
ALTER TABLE db.orders SET TBLPROPERTIES ('format-version' = '3');
Мгновенная операция — меняется только metadata. Существующие V2 position delete files продолжают читаться. Новые deletes пишутся как deletion vectors.
Будущее: V4
Обсуждения в community (по состоянию на март 2026):
- Adaptive metadata tree — оптимизация metadata file для таблиц с миллионами partitions
- Content-addressable metadata — дедупликация metadata files
- Server-side scan planning — каталог планирует скан (REST API v2), клиент получает готовый план
- Native indexing — встроенные индексы для point lookups
V4 — на стадии обсуждений (DISCUSS в dev mailing list). Нет утверждённого feature set. Приведённые items — из публичных тредов community.
REST Catalog specification
Iceberg определяет стандартный REST API для каталогов. Любой каталог, реализующий этот API, совместим со всеми движками.
Ключевые endpoints
| Endpoint | Метод | Описание |
|---|---|---|
/v1/namespaces | GET/POST | List/create namespaces |
/v1/namespaces/{ns}/tables | GET/POST | List/create tables |
/v1/namespaces/{ns}/tables/{table} | GET/DELETE | Load/drop table |
/v1/namespaces/{ns}/tables/{table} | POST | Update table (commit) |
/v1/namespaces/{ns}/tables/{table}/metrics | POST | Report metrics |
OAuth2 и credential vending
REST Catalog поддерживает credential vending — каталог выдаёт временные credentials для доступа к storage:
Клиент → OAuth2 → Каталог
Клиент (Spark, Flink, pyiceberg) аутентифицируется в каталоге через OAuth2. Каталог проверяет RBAC: имеет ли клиент доступ к таблице. Если да — выдаёт временные S3/GCS credentials.Metadata + temp S3/GCS credentials
Каталог возвращает metadata таблицы + временные credentials для S3 (STS AssumeRole) или GCS (signed URLs). Credentials ограничены по времени и scope — только к файлам этой таблицы.Клиент → S3/GCS (direct read)
Клиент читает data files напрямую с S3/GCS используя полученные credentials. Каталог не проксирует данные — только metadata и auth. Минимальная нагрузка на каталог.Credential vending — главное преимущество REST Catalog перед Hive Metastore. В Hive каждый клиент имеет полные AWS credentials. В REST — каталог выдаёт scoped временные credentials. Это zero-trust архитектура: клиент не видит master credentials.
Экосистема каталогов
Apache Polaris (incubating)
Reference implementation REST Catalog от Apache Software Foundation.
- Версия: 1.3.0-incubating (январь 2026)
- Runtime: Quarkus (Java 21+)
- Storage backends: PostgreSQL, H2 (embedded), JDBC
- Интеграции: Spark, Flink, Trino, DuckDB, Dremio, StarRocks, Apache Doris
- Фичи: OAuth2, credential vending (AWS STS, GCS), RBAC, Generic Tables (GA в 1.3), Apache Ozone support
# Запуск Polaris
docker run -p 8181:8181 -p 8182:8182 apache/polaris:1.3.0-incubating
# Подключение из pyiceberg
from pyiceberg.catalog import load_catalog
catalog = load_catalog("polaris", **{
"type": "rest",
"uri": "http://localhost:8181/api/catalog",
"credential": "CLIENT_ID:CLIENT_SECRET",
"warehouse": "my_warehouse"
})
Выбор каталога
| Критерий | Polaris | Nessie | AWS Glue | Gravitino | Lakekeeper |
|---|---|---|---|---|---|
| Тип | REST Catalog | Version control | Managed service | Meta-catalog | REST Catalog |
| Self-hosted | (AWS only) | ||||
| Branching | |||||
| Credential vending | (IAM) | ||||
| Multi-format | (Generic Tables) | (Iceberg focus) | (Iceberg/Delta/Hudi) | (любой) | (Iceberg only) |
| Runtime | Java (Quarkus) | Java (Quarkus) | Serverless | Java | Rust |
Runtime-реализации
iceberg-rust 0.8.0
Нативная Rust-реализация спецификации Iceberg. Релиз 0.8.0 (январь 2026, текущий стабильный) — 144 PR от 37 контрибьюторов, охватывает работу с конца ноября 2025 по начало января 2026.
Ключевые возможности:
- V3 metadata support — чтение и запись V3 manifest files с deletion vectors
- DataFusion интеграция — INSERT INTO partitioned tables, partition column projection
- Delete file support — position + equality deletes, shared caching
- Каталоги — REST, Hive, Glue, SQL, S3 Tables, Memory
- Writers — clustered и fanout writers для разных стратегий распределения
use iceberg::memory::MemoryCatalogBuilder;
use iceberg::{Catalog, CatalogBuilder, TableIdent};
use futures::TryStreamExt;
#[tokio::main]
async fn main() -> iceberg::Result<()> {
let catalog = MemoryCatalogBuilder::default()
.load("memory", /* config */)
.await?;
let table = catalog
.load_table(&TableIdent::from_strs(["db", "orders"])?)
.await?;
let stream = table.scan()
.select(["order_id", "amount"])
.build()?
.to_arrow()
.await?;
let batches: Vec<_> = stream.try_collect().await?;
Ok(())
}
pyiceberg-core
Python-биндинги для iceberg-rust. Используются внутри pyiceberg для высокопроизводительного чтения:
# pyiceberg использует pyiceberg-core автоматически
# pip install pyiceberg[pyarrow] # включает pyiceberg-core
from pyiceberg.catalog import load_catalog
catalog = load_catalog("rest", uri="http://polaris:8181/api/catalog")
table = catalog.load_table("db.orders")
# Scan с filter pushdown — pyiceberg-core делает native Rust I/O
df = table.scan(
row_filter="order_date >= '2026-01-01'",
selected_fields=("order_id", "amount", "status")
).to_pandas()
Мониторинг таблицы
from pyiceberg.catalog import load_catalog
catalog = load_catalog("my_catalog")
table = catalog.load_table("db.orders")
# Общая статистика
metadata = table.metadata
print(f"Format version: {metadata.format_version}")
print(f"Snapshots: {len(metadata.snapshots)}")
print(f"Schemas: {len(metadata.schemas)}")
print(f"Partition specs: {len(metadata.partition_specs)}")
print(f"Sort orders: {len(metadata.sort_orders)}")
print(f"Current snapshot: {metadata.current_snapshot_id}")
print(f"Last column ID: {metadata.last_column_id}")
print(f"Location: {metadata.location}")
# Текущий snapshot details
snapshot = table.current_snapshot()
if snapshot:
print(f"Snapshot ID: {snapshot.snapshot_id}")
print(f"Sequence number: {snapshot.sequence_number}")
print(f"Timestamp: {snapshot.timestamp_ms}")
print(f"Operation: {snapshot.summary['operation']}")
print(f"Total data files: {snapshot.summary.get('total-data-files', 'N/A')}")
print(f"Total records: {snapshot.summary.get('total-records', 'N/A')}")
Для production monitoring: отслеживайте total-data-files и total-records в snapshot summary. Резкий рост total-data-files без пропорционального роста total-records — признак small file problem. Запустите compaction.
Итоги
- Snapshot expiry — удаляет старые snapshots и физически unreachable файлы. Настройте
max-snapshot-age-msиmin-snapshots-to-keep - Orphan cleanup — находит файлы на storage, не упомянутые в metadata. Всегда используйте
older_thanс запасом - Compaction — bin-packing (мелкие → крупные), sort-based (улучшает data skipping), Z-ORDER (multi-column)
- V1→V2→V3: CoW only → position/equality deletes → deletion vectors (Roaring/Puffin) + row lineage + default values + VARIANT
- REST Catalog — стандартный API с OAuth2 и credential vending. Zero-trust архитектура
- Polaris 1.3.0 — reference REST Catalog (ASF incubating), Generic Tables GA, Ozone support
- iceberg-rust 0.8.0 (январь 2026) и PyIceberg 0.11.1 (март 2026) — V3 metadata, DataFusion, delete files. Python-биндинги через pyiceberg-core
- Maintenance pipeline: ежечасный binpack, ежедневный expiry + orphan cleanup, еженедельный sort compaction