Learning Platform
Глоссарий Troubleshooting
Урок 02.04 · 25 мин
Средний
MergeBackground ProcessPart Lifecycle

Merge Scheduling

Parts — immutable. ClickHouse никогда не изменяет уже записанный part. Вместо этого фоновый планировщик слияний непрерывно объединяет мелкие parts в более крупные. Понять, когда и почему происходят слияния — значит научиться диагностировать ошибки “too many parts” и проектировать эффективные пайплайны вставки.


Почему слияния необходимы

Без слияний каждый INSERT создаёт крошечный part. 1000 INSERTs — 1000 директорий на диске. Запрос SELECT count() должен открыть все 1000 parts, прочитать файлы из каждого, объединить результаты. Производительность деградирует линейно.

Фоновое слияние решает проблему: после нескольких INSERT появившиеся мелкие parts объединяются в один крупный. Вместо 1000 files operations — одна. Запросы видят меньше parts — I/O меньше, latency ниже.

WARNING

Каждый отдельный INSERT создаёт минимум один новый part. 1000 мелких INSERTs в секунду при медленных слияниях приведут к ошибке “too many parts”. Пакетируйте INSERT: минимум 1000+ строк за один вызов. Для автоматического пакетирования используйте async_insert.


Алгоритм планировщика слияний

Планировщик работает как фоновый поток в BackgroundProcessingPool. Он непрерывно сканирует активные parts каждой партиции и принимает решение о слиянии:

Планировщик слияний: цикл выбора кандидатов

BackgroundProcessingPool

BackgroundProcessingPool: пул потоков, выполняющих фоновые задачи MergeTree. Планировщик слияний — одна из задач этого пула. Число потоков контролируется параметром background_pool_size (по умолчанию: 16).

Сканировать активные parts

Сканирование активных parts: планировщик перебирает все активные parts таблицы, группирует их по партициям. Для каждой партиции отдельно оценивается, какие parts можно объединить. Parts разных партиций не смешиваются.

Выбрать кандидатов (одна партиция, смежные block numbers)

Выбор кандидатов: планировщик оценивает группы смежных по block number parts в одной партиции. Критерии выбора: размер parts (мелкие приоритетнее), стоимость слияния (I/O, CPU), текущая загрузка пула. Алгоритм балансирует число parts с затратами на слияние.

Проверить пороги (delay / throw)

Проверка порогов: перед запуском слияния планировщик проверяет parts_to_delay_insert и parts_to_throw_insert. Если число активных parts в партиции превышает parts_to_delay_insert — новые INSERT замедляются. Если превышает parts_to_throw_insert — INSERT возвращает ошибку.

Запустить задачу слияния

Запуск задачи слияния: задача передаётся в BackgroundProcessingPool. Пока слияние выполняется, исходные parts остаются активными — in-flight запросы видят старые parts и продолжают работу нормально. Новый merged part становится активным только после успешной записи.

Ключевые параметры планировщика:

ПараметрЗначение по умолчаниюОписание
max_parts_in_total100 000Лимит parts на всю таблицу. При превышении INSERT блокируется
parts_to_delay_insert150Parts в партиции, при которых INSERT замедляется
parts_to_throw_insert300Parts в партиции, при которых INSERT возвращает ошибку
background_pool_size16Число потоков BackgroundProcessingPool

Процесс слияния: шаг за шагом

Lifecycle Part: INSERT -> Merge
INSERT 1 202401_1_1_0INSERT 1 создаёт парт 202401_1_1_0. Формат имени: {partition}_{min_block}_{max_block}_{level}. Partition=202401 (год+месяц по колонке партиционирования). min_block=max_block=1 — монотонно возрастающий глобальный счётчик блоков. level=0 означает свежий парт, никогда не участвовавший в слиянии. Каждый INSERT создаёт отдельный парт на диске — batch writes предпочтительны.
INSERT 2 202401_2_2_0INSERT 2 создаёт парт 202401_2_2_0. min_block=max_block=2 — следующий номер блока. level=0 — свежий парт, не слитый. Большое число мелких партов ухудшает производительность SELECT (сканируются все активные парты партиции). Рекомендуется INSERT пакетами от 1000 строк, не делать INSERT в цикле по одной строке.
INSERT 3 202401_3_3_0INSERT 3 создаёт парт 202401_3_3_0. level=0 — все три парта ещё не слиты. После 3 партов в одной партиции background merge scheduler начинает планировать слияние. Порог too_many_parts (300 по умолчанию) вызывает throttle записей; max_parts_in_total (100000) — полную остановку INSERT.
background merge scheduler
202401_1_3_1 (merged, level=1)202401_1_3_1: результат слияния трёх партов уровня 0. min_block=1 (минимум из слитых), max_block=3 (максимум), level=1 (инкрементируется при каждом слиянии). Scheduler объединил данные трёх партов в один отсортированный парт. Исходные парты (202401_1_1_0, 202401_2_2_0, 202401_3_3_0) помечаются inactive и удаляются когда ref_count = 0 (нет активных SELECT).
дальнейшие INSERT + merge
202401_1_5_2 (level=2)202401_1_5_2: парт уровня 2, результат слияния 202401_1_3_1 (level=1) с партами из последующих INSERT. min_block=1, max_block=5 охватывают все пять блоков. level=2. Конечная цель MergeTree — один парт на партицию (OPTIMIZE TABLE FINAL). Но при постоянных INSERT scheduler поддерживает баланс между числом партов и частотой слияний.
activeвиден в SELECTСостояние active: парт участвует в обработке запросов SELECT. Только активные парты попадают в execution pipeline. При SELECT ClickHouse собирает список всех активных партов партиции и планирует параллельное чтение гранул. Парт становится active сразу после завершения записи (атомарно через rename).
inactiveожидает удаленияСостояние inactive: парт был superseded слиянием или мутацией. Физически файлы ещё существуют на диске — они будут удалены когда ref_count достигнет 0 (все in-flight SELECT завершены). Время хранения inactive партов ограничено old_parts_lifetime (8 минут по умолчанию). SELECT не видит inactive парты.
max_parts_in_total100 000 (дефолт)max_parts_in_total: жёсткий лимит на суммарное число активных партов по всем партициям таблицы. При превышении INSERT блокируются с ошибкой. Мягкий порог too_many_parts (300) вызывает замедление (throttle) записей. OPTIMIZE TABLE FINAL форсирует немедленное слияние всех партов партиции в один, но тяжёлая операция — не рекомендуется в продакшне при высоком темпе INSERT.

После выбора кандидатов фоновая задача выполняет слияние в 6 шагов:

  1. Выбор частей-кандидатов — смежные по block number parts в одной партиции
  2. Чтение всех кандидатов — параллельное чтение .bin файлов каждой части
  3. Merge-sort по ORDER BY ключу — объединение в один отсортированный поток строк
  4. Запись нового merged part — новый level = max(уровней кандидатов) + 1
  5. Atomic swap — новый part становится активным, старые помечаются inactive
  6. Удаление старых parts — когда ref count = 0 (все in-flight запросы завершены)
До слияния:  202401_1_1_0 + 202401_2_2_0 + 202401_3_3_0
После:       202401_1_3_1   ← level=1 (первое слияние)

Ещё одно:   202401_1_3_1 + 202401_4_5_1
Результат:  202401_1_5_2   ← level=2 (второе слияние)

Старые parts физически не удаляются мгновенно: они остаются на диске пока активны in-flight запросы, которые открыли их снапшот до swap. Только когда последний такой запрос завершается — части удаляются.

MVCC в PostgreSQL: xmin/xmax и версии строк

OPTIMIZE TABLE: ручное управление слияниями

OPTIMIZE TABLE позволяет вручную инициировать слияния:

-- Запустить один цикл слияния (необязательно объединит все parts)
OPTIMIZE TABLE events;

-- Принудительно объединить все parts всех партиций в один part на партицию
OPTIMIZE TABLE events FINAL;

-- Принудительное слияние конкретной партиции
OPTIMIZE TABLE events PARTITION '202401' FINAL;
WARNING

Никогда не используйте OPTIMIZE TABLE FINAL в production write path. FINAL на большой таблице крайне ресурсоёмко: читает все parts, сортирует и перезаписывает весь объём данных. Это заморозит слияния для других таблиц и создаст нагрузку на I/O. Используйте FINAL только для обслуживания или тестирования.

TIP

Мониторинг активных слияний:

-- Текущие слияния и их прогресс
SELECT * FROM system.merges;

-- Статистика merge-событий с момента запуска
SELECT event, value
FROM system.events
WHERE event LIKE '%Merge%';

-- Число активных parts по таблицам
SELECT table, count() AS parts, sum(rows) AS rows
FROM system.parts
WHERE active = 1
GROUP BY table
ORDER BY parts DESC;

Диагностика “too many parts”

Ошибка DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts означает, что планировщик слияний не успевает за темпом INSERT.

Типичные причины:

  1. Слишком мелкие INSERT — каждый INSERT с 1-100 строками создаёт отдельный part
  2. Перегруженный background pool — другие операции (мутации, TTL) занимают потоки
  3. Большие партицииPARTITION BY toYYYYMMDD(dt) при большом трафике создаёт много parts в одной дате

Диагностика:

-- Parts по партициям (где больше всего parts?)
SELECT
    partition,
    count() AS part_count,
    sum(rows) AS total_rows
FROM system.parts
WHERE table = 'events' AND active = 1
GROUP BY partition
ORDER BY part_count DESC
LIMIT 20;

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

  1. Parts immutable: ClickHouse никогда не изменяет записанный part. Слияние создаёт новый part и удаляет старые.
  2. Merge scheduler: фоновый поток в BackgroundProcessingPool, балансирует число parts с затратами на слияние.
  3. max_parts_in_total = 100 000: превышение блокирует INSERT. Пороги parts_to_delay_insert и parts_to_throw_insert работают на уровне партиции.
  4. Level в имени part: монотонно растёт с каждым слиянием. level=0 — свежий INSERT, level=2 — прошёл два слияния.
  5. OPTIMIZE TABLE FINAL: только для обслуживания. В production write path — никогда.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Мониторинг показывает 95 000 активных parts в таблице events. Порог max_parts_in_total = 100 000. Что произойдёт, если число parts превысит этот порог?

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

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

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

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