Часть 6. Вставка данных: батчи, async_insert и потоки из Kafka

Почему мелкие вставки убивают ClickHouse, какой размер батча брать, как работает async_insert и его настройки, куда вставлять при шардировании и как устроена дедупликация.
Опубликовано:

Если выбрать одну тему, где чаще всего ломают ClickHouse, — это вставка. Модель хранения (иммутабельные куски + фоновые слияния) наказывает за частые мелкие записи жёстче, чем любая OLTP-база.

Почему мелкие вставки — это яд

Каждый INSERT создаёт отдельный кусок данных на диске. Дальше фоновые потоки должны эти куски слить. Если вставок много и они мелкие:

Вставка по одной строке из приложения — самый быстрый способ довести кластер до этого состояния.

Правильный размер батча

Базовое правило: вставляйте большими пачками и редко. Ориентир — от десятков тысяч строк за вставку; на потоках обычно целятся в диапазон 10 000–100 000+ строк или несколько десятков мегабайт, с частотой порядка одной вставки в секунду на таблицу, а не сотен.

Второе правило — не более одной вставки в секунду на партицию в среднем. Если данные распределяются по многим партициям сразу, каждая вставка создаёт кусок в каждой затронутой партиции — вставка «размазанная» по 30 партициям создаёт 30 кусков вместо одного.

Отсюда следствие: сортируйте данные перед вставкой так, чтобы одна пачка попадала в одну-две партиции.

async_insert: батчинг на стороне сервера

Собирать батчи в приложении не всегда удобно — особенно когда пишут много независимых источников. Для этого есть асинхронная вставка: клиент отправляет мелкие запросы, а ClickHouse сам буферизует их в памяти и записывает одним куском, когда накопится достаточно данных или истечёт таймаут.

SQL
SET async_insert = 1;
SET wait_for_async_insert = 1;
Нажмите, чтобы развернуть и увидеть больше

Ключевые настройки и их значения по умолчанию:

НастройкаПо умолчаниюСмысл
async_insert0Включает асинхронный режим
async_insert_max_data_size~100 МиБРазмер буфера, при котором сбрасывать
async_insert_busy_timeout_ms200 мс (в облаке 1000)Сброс по времени
async_insert_max_query_number450Сброс по числу накопленных запросов
wait_for_async_insert1Ждать ли реальной записи на диск

Про wait_for_async_insert стоит сказать отдельно. Значение 1 (по умолчанию) означает: клиент получает подтверждение только после того, как данные реально записаны. Значение 0 — это «отправил и забыл»: ответ приходит сразу, латентность минимальная, но гарантии записи нет — ошибки всплывут только при сбросе буфера, а данные в буфере при падении узла теряются.

Для продовых пайплайнов рекомендуется async_insert=1 вместе с wait_for_async_insert=1. Отключать ожидание стоит только там, где потеря отдельных записей допустима осознанно.

Альтернатива из прошлого — движок Buffer. Он живёт в памяти узла и теряет данные при перезапуске, поэтому для новых решений предпочтителен async_insert.

Куда вставлять в шардированном кластере

Два варианта, и у каждого своя цена:

В Distributed-таблицу. Просто: пишем в одну точку, ClickHouse сам раскладывает по шардам согласно ключу шардирования. Но по умолчанию узел складывает данные во временную очередь на диске и пересылает асинхронно — появляется промежуточное звено и дополнительная задержка. Обязательно включайте internal_replication = true в конфигурации кластера, иначе Distributed попытается писать во все реплики сам, вместо того чтобы отдать репликацию движку ReplicatedMergeTree (получите двойную работу и риск расхождений).

Напрямую в локальные таблицы шардов. Клиент сам выбирает шард (round-robin или по ключу) и пишет в ReplicatedMergeTree. Меньше звеньев, предсказуемее. Это предпочтительный способ для высоконагруженных пайплайнов, но требует, чтобы логика распределения жила в клиенте или в балансировщике.

Дедупликация

У реплицируемых таблиц есть встроенная защита от повторной вставки: ClickHouse запоминает хеши последних вставленных блоков в Keeper, и повторная вставка идентичного блока игнорируется. Это спасает при ретраях после сетевых ошибок.

Важные оговорки:

Полезно задавать insert_deduplication_token — тогда вы явно управляете идентичностью вставки, не завися от побайтового совпадения.

Потоки из Kafka

Три рабочих подхода:

1. Движок Kafka внутри ClickHouse. Таблица-консьюмер + материализованное представление, перекладывающее данные в MergeTree:

SQL
CREATE TABLE events_queue (raw String)
ENGINE = Kafka
SETTINGS kafka_broker_list = 'kafka:9092',
         kafka_topic_list = 'events',
         kafka_group_name = 'clickhouse',
         kafka_format = 'JSONAsString';

CREATE MATERIALIZED VIEW events_mv TO events AS
SELECT
    JSONExtractString(raw, 'event')       AS event_type,
    toDateTime(JSONExtractUInt(raw, 'ts')) AS ts,
    JSONExtractUInt(raw, 'user_id')        AS user_id
FROM events_queue;
Нажмите, чтобы развернуть и увидеть больше

Просто и без внешних компонентов, но потребление живёт внутри БД: проблемы с парсингом и отставанием придётся диагностировать через системные таблицы, а масштабирование потребления ограничено узлами кластера.

2. Внешний потребитель (свой сервис, Vector, Kafka Connect) — больше контроля над батчингом, ретраями и обработкой «плохих» сообщений. Для сложных пайплайнов обычно предпочтительнее.

3. ClickPipes/managed-интеграции — если используете облачное предложение.

Общее правило для любого варианта: батчить на стороне потребителя, а не полагаться на то, что «ClickHouse быстрый».

За чем следить

Три запроса, которые стоит держать под рукой (и вынести в алерты):

SQL
-- Не подбираемся ли к пределу по числу кусков
SELECT database, table, count() AS parts
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY parts DESC
LIMIT 10;

-- Не растёт ли очередь репликации
SELECT database, table, queue_size, absolute_delay
FROM system.replicas
WHERE queue_size > 0;

-- Что происходит с асинхронными вставками
SELECT * FROM system.asynchronous_inserts;
Нажмите, чтобы развернуть и увидеть больше

Дальше: Часть 7. Ресурсы и память: как не получить OOMKilled.

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша