Learning Platform
Глоссарий Troubleshooting
Урок 15.01 · 40 мин
Продвинутый
Apache PaimonLSM-TreeMemTableSorted RunCompactionSST FileWrite AmplificationRead AmplificationUniversal CompactionSorted-Run Compaction

LSM-Tree Architecture

Apache Paimon — это открытый формат lakehouse-хранилища, спроектированный для стриминговых и батчевых workload’ов на object storage. Проект был создан в Alibaba как внутренняя система для обработки 100+ PB данных, затем передан в Apache Software Foundation как Flink Table Store (2022), переименован в Apache Paimon (2023) и получил статус Top-Level Project в апреле 2024. Версия 1.0 GA вышла в январе 2025, текущая стабильная — 1.3.1 (ноябрь 2025).

В Модуле 11 мы разобрали Delta Lake (append-only transaction log), в Модуле 12 — Iceberg (каталог + дерево metadata файлов), в Модуле 13 — Hudi (timeline + FileGroup/FileSlice). Paimon идёт другим путём: в основе его storage engine лежит LSM-дерево (Log-Structured Merge Tree) — та же структура данных, что используется в RocksDB, LevelDB и Apache HBase.

NOTE

Этот курс использует engine-agnostic подход. Paimon — это формат и протокол хранения. Основной движок — Apache Flink, но Paimon также поддерживает Spark, StarRocks, Trino, Presto и Doris. Библиотека pypaimon (чистый Python с версии 1.3, без JVM) поддерживает как чтение, так и запись таблиц.

Почему LSM-дерево для озера данных?

Delta Lake, Iceberg и Hudi хранят данные как иммутабельные файлы (Parquet). При update или delete они создают новые файлы или log-записи. Paimon добавляет уровень абстракции: данные организованы как LSM-дерево внутри каждого bucket’а, что даёт несколько преимуществ:

Почему LSM-Tree: запись vs чтение

Append-only (Delta/Iceberg)

Append-only формат: каждая операция создаёт новый файл. Update = копирование всего файла с изменённой строкой. Amplification растёт линейно с размером файла.
UpdateCOW-стратегия: перезапись файла целиком при изменении одной строки. Файл 128 МБ → перезаписать 128 МБ ради одной строки. Write amplification может быть 100x+.
ReadДанные всегда в свежем состоянии после записи — читай напрямую. Нет merge на чтение. Read amplification минимальный.

LSM-Tree (Paimon)

LSM-дерево: записи буферизуются в памяти (MemTable), сбрасываются как sorted run, объединяются через компакцию. Мелкозернистые обновления без перезаписи файлов.
UpdateЗапись уходит в in-memory MemTable. Когда MemTable заполнен — flush как sorted run (SST-файл). Никакой перезаписи старых файлов. Write amplification контролируется через compaction.
ReadЧтение merge-ит несколько sorted runs. Чем больше уровней — тем больше файлов нужно merge-ить. Read amplification зависит от количества sorted runs и частоты компакции.

LSM-дерево оптимизирует запись за счёт чтения. Для стриминговых pipeline’ов, где данные поступают непрерывно (Flink checkpoint каждые 1-2 минуты), это идеальный компромисс: мелкозернистые upsert без перезаписи больших файлов.

Анатомия LSM-дерева в Paimon

Каждый bucket в Paimon-таблице содержит собственное LSM-дерево. Дерево состоит из трёх компонентов:

Структура LSM-дерева: MemTable → Sorted Runs → Levels

MemTable (in-memory, sorted by PK)

Write-ahead буфер в памяти. Все входящие записи (insert, update, delete) сначала попадают сюда. Сортировка по primary key внутри MemTable. Размер контролируется write-buffer-size (по умолчанию 256 МБ).
flush (write-buffer-size exceeded)
Sorted Run 0 (Level 0)Свежий sorted run — результат flush одного MemTable. Каждый sorted run — набор SST-файлов (Parquet), внутренне отсортированных по primary key. На Level 0 может быть несколько sorted runs с перекрывающимися диапазонами ключей.
Sorted Run 1 (Level 0)Ещё один sorted run на Level 0. Диапазоны ключей этого run могут перекрываться с другими sorted runs на том же уровне. Это нормально для Level 0 — merge при чтении.
compaction (sorted-run-num exceeded)
Sorted Run (Level 1+)Результат компакции: несколько sorted runs объединяются в один с непрерывными, непересекающимися диапазонами ключей. На Level 1+ каждый sorted run гарантирует глобальный порядок — чтение одного sorted run не требует merge.

MemTable

MemTable — это in-memory буфер записи, аналог Write-Ahead Log в классических БД:

ПараметрЗначениеОписание
write-buffer-size256 MB (по умолчанию)Максимальный размер MemTable до flush
write-buffer-spillablefalseМожно ли сбросить MemTable на диск при нехватке памяти
page-size64 KBРазмер страницы внутри MemTable

Записи в MemTable сортируются по primary key. Когда размер превышает write-buffer-size, MemTable flush’ится на object storage как новый sorted run на Level 0.

WARNING

MemTable хранится в памяти Flink TaskManager (или Spark executor). При crash до flush данные из MemTable теряются. Flink восстанавливает их из последнего checkpoint — это не WAL в классическом смысле, а буфер между checkpoint’ами. Для батчевых job’ов: если job упадёт до commit, MemTable-данные потеряны и job нужно перезапустить.

Sorted Run и SST-файлы

Sorted run — это набор SST-файлов (Sorted String Table), отсортированных по primary key:

SST-файл: Parquet внутри

SST File = Parquet + Paimon Metadata

Один SST-файл — это обычный Parquet-файл с дополнительными metadata. Paimon использует Parquet как физический формат хранения, добавляя свои поля: _KEY_*, _VALUE_KIND (для операций +I/-U/+U/-D), sequence number.
Пользовательские колонкиДанные таблицы: все колонки, объявленные в схеме. Хранятся в стандартном Parquet columnar формате с row groups, pages, dictionary encoding.
Системные колонки_VALUE_KIND: тип операции (+I = insert, -U = update before, +U = update after, -D = delete). _SEQUENCE_NUMBER: монотонно растущий номер для определения порядка записей. _KEY_*: колонки primary key для merge.
Target file sizeПараметр target-file-size (по умолчанию 128 МБ) контролирует размер одного SST-файла. Если sorted run содержит больше данных — он разбивается на несколько SST-файлов, каждый не больше target-file-size.

Ключевое отличие от Delta Lake / Iceberg: SST-файлы — это не просто Parquet с данными. Они содержат _VALUE_KIND, который кодирует тип CDC-операции. Это позволяет Paimon нативно поддерживать changelog semantics — тему Урока 03.

Уровни (Levels)

LSM-дерево организовано в уровни:

Уровни LSM-дерева
Level 0Свежие sorted runs. Каждый sorted run — результат flush одного MemTable. Ключевые диапазоны МОГУТ перекрываться. Чтение с Level 0 требует merge всех sorted runs. Чем больше sorted runs на Level 0 — тем медленнее чтение.
Level 0Ещё один sorted run на Level 0. Его ключи могут пересекаться с sorted run A. При чтении key=50 нужно проверить ОБА sorted run и выбрать запись с бо́льшим sequence number.
Level 0Третий sorted run. Три перекрывающихся sorted run на Level 0 = read amplification 3x для пересекающихся ключей. Компакция уменьшает количество sorted runs.
compaction
Level 1Результат компакции Level 0. Один sorted run с непересекающимися ключевыми диапазонами. Чтение с Level 1 — один sorted run, но если запись новее на Level 0, нужно проверить оба уровня.
compaction
Level 2 (max-level)Максимальный уровень. Содержит наибольший объём данных. Sorted run здесь — полностью отсортированный, непересекающийся. Чтение ключа: сначала Level 0 → Level 1 → Level 2 (остановка при первом совпадении).
ПараметрЗначениеОписание
num-levels2 (по умолчанию)Количество уровней LSM-дерева (не считая Level 0)
num-sorted-run.compaction-trigger5Компакция запускается когда sorted runs на Level 0 ≥ этого значения
num-sorted-run.stop-trigger10Запись блокируется при ≥ этого количества sorted runs (backpressure)
TIP

Параметр num-sorted-run.stop-trigger — механизм backpressure: если компакция не успевает за записью, Paimon блокирует запись до завершения компакции. Это предотвращает неограниченный рост sorted runs и деградацию чтения.

Стратегии компакции

Компакция — процесс объединения sorted runs для уменьшения read amplification. Paimon поддерживает две стратегии:

Universal Compaction

Universal compaction (по умолчанию) — стратегия, заимствованная из RocksDB:

Universal Compaction: объединение всех sorted runs
SR-0 (newest)Самый свежий sorted run. При universal compaction ВСЕ sorted runs на всех уровнях объединяются в один. Это гарантирует минимальное количество sorted runs, но создаёт высокий write amplification при большом объёме данных.
SR-1Предыдущий sorted run. Universal compaction не различает уровни — все sorted runs равноценны. Компакция запускается по числу sorted runs, не по размеру.
SR-2Ещё один sorted run. При трёх sorted runs и compaction-trigger=5 компакция ещё не запускается — нужно накопить 5 sorted runs.
SR-3Четвёртый sorted run. При накоплении 5 sorted runs (включая этот) universal compaction merge-ит ВСЕ в один.
SR-4 (oldest)Самый старый sorted run. Содержит наибольший объём данных — результат предыдущих компакций.
merge all → single sorted run
ResultОдин объединённый sorted run. Все дубликаты ключей разрешены (оставлен последний по sequence number). Все delete-маркеры применены. Write amplification: 295 МБ прочитано + 295 МБ записано = 590 МБ I/O для объединения.

Характеристики:

  • Минимальное количество sorted runs (обычно 1-2) → лучшая скорость чтения
  • Высокий write amplification: каждая компакция перезаписывает все данные
  • Подходит для workload’ов с преобладанием чтения

Sorted-Run Compaction

Sorted-run compaction (аналог leveled compaction в RocksDB) — более гранулярная стратегия:

Sorted-Run Compaction: поуровневое объединение
Level 0: SR-0Свежий sorted run на Level 0. Sorted-run compaction merge-ит sorted runs одного уровня, продвигая результат на следующий. В отличие от universal — не трогает sorted runs на нижних уровнях.
Level 0: SR-1Второй sorted run на Level 0. При compaction-trigger=5 и 2 sorted runs на Level 0 — компакция ещё не нужна. Но если sorted runs перекрываются с Level 1 — может быть merge с Level 1.
merge Level 0 → push to Level 1
Level 1: merged SRРезультат merge Level 0 sorted runs. Если на Level 1 уже был sorted run с пересекающимися ключами — он тоже участвует в merge. Итог: один sorted run на Level 1 с непересекающимися ключами.
Level 1 overflow → push to Level 2
Level 2: final SRФинальный уровень. Данные сюда попадают только через каскадную компакцию Level 0 → Level 1 → Level 2. Меньший write amplification чем universal: перезаписываются только пересекающиеся диапазоны ключей, а не все данные.

Характеристики:

  • Меньший write amplification: merge только пересекающихся диапазонов
  • Больше sorted runs на промежуточных уровнях → медленнее чтение
  • Подходит для write-heavy workload’ов (стриминг)

Выбор стратегии

Выбор стратегии компакции
Universal CompactionИспользуйте для аналитических workload'ов: большие scan-запросы, OLAP, batch ETL. Меньше sorted runs = быстрее чтение. Цена: высокий write amplification при больших таблицах.
Sorted-Run CompactionИспользуйте для стриминговых pipeline'ов: частые мелкие записи, Flink CDC, real-time upsert. Меньший write amplification = стабильнее throughput записи. Цена: больше sorted runs для merge при чтении.
TIP

В Модуле 13 мы видели аналогичный компромисс в Hudi: COW (read-optimized, high write amp) vs MOR (write-optimized, merge on read). Paimon решает ту же проблему через стратегию компакции вместо двух разных типов таблиц.

Write Amplification vs Read Amplification

Центральный компромисс LSM-дерева:

МетрикаОпределениеPaimon контекст
Write AmplificationСколько раз одна запись перезаписывается на диск за время жизниКаждая компакция перезаписывает данные. Universal: весь sorted run. Sorted-run: только пересекающийся диапазон
Read AmplificationСколько sorted runs нужно прочитать для одного запросаБольше sorted runs = больше merge при чтении. Компакция уменьшает число sorted runs
Space AmplificationСколько дискового пространства занимают устаревшие версии записейДо компакции: старые версии записей живут в нижних sorted runs. После: очищены
Компромисс: Write Amp ↔ Read Amp

Редкая компакция

Редкая компакция: sorted runs накапливаются на Level 0. Запись быстрая (нет overhead от компакции), но чтение должно merge-ить 5-10 sorted runs. Подходит для write-only стриминга с редким чтением.
Write: быстроНет компакции = нет дополнительных записей. MemTable → flush → sorted run. Минимальный write amplification (1x). Идеально для высокочастотного стриминга.
Read: медленно10 sorted runs на Level 0 = merge 10 файлов для каждого запроса. Point lookup: O(10) вместо O(1). Scan: merge-sort 10 потоков. Для OLAP-запросов — заметная деградация.

Частая компакция

Частая компакция: sorted runs постоянно объединяются. Чтение быстрое (1-2 sorted runs), но каждая компакция перезаписывает данные. Подходит для read-heavy workload'ов с умеренной записью.
Write: медленноКомпакция перезаписывает данные. Universal: перезапись всего sorted run при каждой компакции. Для таблицы 1 ТБ: 1 ТБ чтения + 1 ТБ записи на каждую компакцию. Write amplification может достигать 10-20x.
Read: быстро1-2 sorted runs = минимальный merge при чтении. Point lookup: O(1). Scan: последовательное чтение одного отсортированного файла. Максимальная скорость чтения.

Write Path: от записи до snapshot

Полный путь записи в Paimon:

Write Path: запись → MemTable → flush → compaction → snapshot

Входящая запись (INSERT / UPDATE / DELETE)

Входящая запись: INSERT, UPDATE или DELETE. В стриминговом режиме — Flink checkpoint trigger. В батчевом — завершение job. Запись содержит primary key + данные + тип операции.
1. Buffer

MemTable (in-memory sorted buffer)

Запись добавляется в MemTable (in-memory, sorted by PK). MemTable — это skip-list или red-black tree, зависит от реализации. Записи с одинаковым PK merge-ятся в MemTable: последний UPDATE перезаписывает предыдущий.
2. Flush (write-buffer-size exceeded)

Flush → новый Sorted Run (Level 0)

MemTable сбрасывается на object storage как новый sorted run на Level 0. Физически — набор SST-файлов (Parquet). Каждый SST-файл ≤ target-file-size (128 МБ). Flush не блокирует запись — новый MemTable создаётся немедленно.
3. Compaction (trigger reached)

Compaction (merge sorted runs)

Компакция объединяет sorted runs: merge по primary key, применение delete-маркеров, удаление дубликатов. Выбор стратегии: universal (все → один) или sorted-run (поуровневая). Компакция может быть синхронной (блокирует commit) или асинхронной.
4. Snapshot commit

Snapshot (immutable table state)

Snapshot фиксирует состояние таблицы после записи: список всех SST-файлов, manifest, schema version. Snapshot — иммутабельный. Читатели используют snapshot для консистентного чтения. Аналог commit в Delta Lake.
NOTE

Snapshot в Paimon — аналог commit в Delta Lake и snapshot в Iceberg. Каждый snapshot содержит полную информацию о состоянии таблицы: какие SST-файлы существуют, их уровни, schema version, и watermark (для стриминга). Snapshots хранятся в директории snapshot/ в корне таблицы.

Физическая структура таблицы

На object storage Paimon-таблица выглядит так:

Директория Paimon-таблицы на object storage
s3://bucket/warehouse/db.db/table_name/Корневая директория таблицы на object storage. Содержит schema, snapshot, manifest и данные. Путь: s3://bucket/warehouse/db.db/table_name/ или file:///tmp/paimon/db.db/table_name/.
schema/Версионированные схемы таблицы: schema-0, schema-1, ... Каждый файл — JSON с полями fields, partitionKeys, primaryKeys, options. Schema evolution: добавление/удаление колонок создаёт новую версию.
snapshot/Иммутабельные snapshot-файлы: snapshot-1, snapshot-2, ... Каждый snapshot ссылается на manifest list. Файл LATEST — текущий snapshot. Файл EARLIEST — самый старый доступный snapshot для time travel.
manifest/Manifest-файлы: список SST-файлов с их уровнями, ключевыми диапазонами, размерами. Аналог manifest files в Iceberg. Manifest list (в snapshot) → manifests → SST files.
bucket-0/Директория bucket'а. Содержит SST-файлы этого bucket'а. Каждая партиция содержит N bucket'ов (по умолчанию -1 = dynamic). Имя файла: data-{UUID}.parquet.
tag/Именованные snapshot-метки (tags): tag-v1.0, tag-2024-01-01. Tag — это закладка на snapshot, предотвращающая его удаление при expire. Используется для time travel и release management.

Для партиционированной таблицы структура добавляет уровень:

table_name/
├── schema/
├── snapshot/
├── manifest/
├── pt=2024-01-01/
│ ├── bucket-0/
│ │ ├── data-abc123.parquet (SST file)
│ │ └── data-def456.parquet (SST file)
│ └── bucket-1/
│ └── data-ghi789.parquet
├── pt=2024-01-02/
│ └── bucket-0/
│ └── data-jkl012.parquet
└── tag/

Сравнение с другими форматами

Storage Engine: Delta Lake vs Iceberg vs Hudi vs Paimon
Delta LakeAppend-only transaction log (_delta_log/). Каждый коммит — JSON/Parquet файл с add/remove actions. Данные — иммутабельные Parquet файлы. Update = COW (перезапись файла) или Deletion Vectors (lazy delete).
Apache IcebergКаталог-first + дерево metadata файлов. Metadata file → manifest list → manifest files → data files. Данные — иммутабельные Parquet/ORC/Avro. Update: COW или MOR (position delete files).
Apache HudiTimeline + FileGroup/FileSlice. COW: перезапись base file. MOR: append в log file (Avro) + merge при чтении. Индекс для маршрутизации записей. Compaction: merge log files в base file.
Apache PaimonLSM-дерево внутри каждого bucket'а. MemTable → sorted runs → levels. SST-файлы (Parquet + _VALUE_KIND). Компакция: universal или sorted-run. Нативная поддержка changelog через _VALUE_KIND.
АспектDelta LakeIcebergHudiPaimon
Update modelCOW / DVCOW / position deleteCOW / MORLSM merge
Write amplificationВысокий (COW)Высокий (COW)Средний (MOR)Контролируемый
Streaming nativeЧастично (LSM)
Changelog native (CDF)CDC (log files) (_VALUE_KIND)
КомпакцияOPTIMIZErewrite_data_filesasync servicesuniversal/sorted-run
TIP

Paimon — единственный из четырёх форматов, где LSM-дерево является фундаментальной структурой данных, а не оптимизацией поверх файлов. Это делает его наиболее эффективным для streaming-first workload’ов с частыми мелкозернистыми обновлениями.

Ключевые выводы

  1. LSM-дерево — фундамент Paimon: MemTable → sorted runs → уровни с компакцией
  2. SST-файл — Parquet с дополнительными системными колонками (_VALUE_KIND, _SEQUENCE_NUMBER)
  3. Компакция — механизм управления компромиссом write amp vs read amp
  4. Universal — merge всех sorted runs, лучше для чтения; sorted-run — поуровневый merge, лучше для записи
  5. Snapshot — иммутабельное состояние таблицы, аналог commit в Delta Lake
  6. Backpressure (num-sorted-run.stop-trigger) — защита от неограниченного роста sorted runs при отставании компакции

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Paimon-таблица с primary key, compaction.style=universal, num-sorted-run.compaction-trigger=5. В bucket-0 находятся 4 sorted runs на Level 0 с перекрывающимися ключевыми диапазонами. Flink checkpoint записывает ещё один MemTable — пятый sorted run. Что произойдёт?

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

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

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

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