ClickHouse по умолчанию считает, что машина принадлежит ему целиком. В Kubernetes это не так, и если не объяснить ему границы, вы получите либо OOMKilled посреди отчёта, либо необъяснимо медленные запросы.
Память: главный источник боли
Kubernetes убивает контейнер, превысивший limits.memory, без предупреждения и без изящной деградации. Задача — сделать так, чтобы раньше сработал внутренний лимит ClickHouse: он умеет аккуратно отменить запрос с внятной ошибкой.
Порядок величин должен выстраиваться так:
max_memory_usage (один запрос)
< max_server_memory_usage (весь сервер)
< limits.memory (контейнер)Ключевые настройки:
max_server_memory_usage_to_ram_ratio— доля памяти, которую сервер считает своей (типично 0.8–0.9). ClickHouse определяет доступную память из cgroup, но оставить запас всё равно нужно: вне учёта остаются кэши страниц, аллокатор и накладные расходы.max_memory_usage— потолок на один запрос. Без него один аналитик сGROUP BYпо высококардинальной колонке съест всю память узла.max_memory_usage_for_user— суммарный потолок на пользователя, защищает от «много запросов по чуть-чуть».
Три практических требования:
requests.memory=limits.memory. Класс QoSGuaranteedзащищает от вытеснения при нехватке памяти на ноде. Для базы данных бурстить памятью — плохая идея.- Оставьте запас 10–20% между лимитом контейнера и
max_server_memory_usage. - Разрешите слив на диск для тяжёлых операций, чтобы запрос замедлялся, а не падал:
SET max_bytes_before_external_group_by = '8G';
SET max_bytes_before_external_sort = '8G';Но тогда ограничьте и объём временных файлов (max_temporary_data_on_disk_size_for_query), иначе один запрос забьёт диск — см. часть 4.
CPU: лимиты чаще вредят
Здесь контринтуитивный момент. ClickHouse распараллеливает один запрос на много потоков, и жёсткий CPU-лимит в Kubernetes реализуется через CFS-квоту: когда квота на период исчерпана, процесс принудительно останавливается до начала следующего периода. Для многопоточного сканирования это выливается в рваную латентность и общее замедление, даже когда ядра на ноде свободны.
Разумный подход для выделенных под ClickHouse нод:
- ставить адекватный
requests.cpu(реальная потребность — это гарантия при планировании и доля в конкуренции); limits.cpuне задавать или задавать с большим запасом;- размещать ClickHouse на выделенных нодах (taints/tolerations), чтобы «не задавить соседей» перестало быть аргументом.
Если политика организации требует лимитов — согласуйте max_threads с числом доступных ядер, иначе ClickHouse будет плодить потоки, которые тут же упрутся в троттлинг.
Профили и квоты: защита от одного тяжёлого запроса
Одна из лучших практик, которую внедряют слишком поздно: разные профили настроек для разных потребителей. Дашборд не должен иметь права выполнить запрос, кладущий кластер.
<profiles>
<readonly_bi>
<max_memory_usage>10000000000</max_memory_usage>
<max_execution_time>30</max_execution_time>
<max_rows_to_read>1000000000</max_rows_to_read>
<readonly>1</readonly>
</readonly_bi>
<etl>
<max_memory_usage>60000000000</max_memory_usage>
<max_execution_time>3600</max_execution_time>
</etl>
</profiles>И квоты — ограничение на объём потребления за интервал (число запросов, прочитанных строк, времени выполнения). Профиль ограничивает один запрос, квота — аппетит пользователя за период.
Отдельно: max_concurrent_queries на сервере. Аналитическая СУБД не выигрывает от сотни параллельных тяжёлых запросов — она начинает конкурировать сама с собой за память и диск. Лучше очередь, чем деградация всех сразу.
Кэши
ClickHouse держит несколько кэшей, и их размеры входят в бюджет памяти:
mark_cache_size— кэш засечек (местоположение гранул). Небольшой, но важный: обращения к нему постоянны.uncompressed_cache_size— распакованные блоки. По умолчанию выключен; включать имеет смысл при повторяющихся точечных запросах, для сканирования больших объёмов пользы мало.- кэш для дисков объектного хранилища (см. часть 4).
Не раздувайте кэши «на всякий случай»: память полезнее отдать под выполнение запросов и страничный кэш ОС.
Фоновые задачи
Слияния конкурируют с запросами. Размеры фоновых пулов (background_pool_size и родственные) стоит согласовать с числом ядер: слишком мало — куски копятся и вы идёте к too many parts; слишком много — слияния съедают CPU, нужный запросам.
Дефолты в современных версиях адекватные — трогайте их осознанно, когда видите по метрикам конкретный перекос (system.merges, число активных кусков, утилизация CPU фоном).
Размер узла
Из части 1: ClickHouse эффективнее на меньшем числе крупных узлов. Практический ориентир для продового узла — от 16 ядер и 64 ГиБ памяти; много маленьких подов по 2 ядра дадут хуже и по скорости, и по эксплуатации.
Соотношение памяти к ядрам обычно берут около 4–8 ГиБ на ядро; для тяжёлых агрегаций и джойнов — ближе к верхней границе.
Диагностика
-- Кто ест память прямо сейчас
SELECT query_id, user, formatReadableSize(memory_usage) AS mem, elapsed, query
FROM system.processes
ORDER BY memory_usage DESC;
-- Топ запросов по памяти за сутки
SELECT query_id, user, formatReadableSize(memory_usage) AS mem, query_duration_ms, query
FROM system.query_log
WHERE type = 'QueryFinish' AND event_time > now() - INTERVAL 1 DAY
ORDER BY memory_usage DESC
LIMIT 20;
-- Запросы, упавшие по лимитам
SELECT event_time, user, exception, query
FROM system.query_log
WHERE type = 'ExceptionWhileProcessing'
AND event_time > now() - INTERVAL 1 DAY
ORDER BY event_time DESC
LIMIT 20;Ошибка MEMORY_LIMIT_EXCEEDED в логах — это хороший сценарий: сработала защита ClickHouse. Плохой — когда под просто исчезает с OOMKilled в событиях Kubernetes: значит, внутренние лимиты выставлены выше, чем позволяет контейнер.
Дальше: Часть 8. Наблюдаемость: system-таблицы, метрики и алерты.