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

Почему CPU-лимиты вредят аналитической нагрузке, как согласовать лимиты памяти пода с внутренними лимитами ClickHouse, зачем нужны профили и квоты и что делать с тяжёлыми запросами.
Опубликовано:

ClickHouse по умолчанию считает, что машина принадлежит ему целиком. В Kubernetes это не так, и если не объяснить ему границы, вы получите либо OOMKilled посреди отчёта, либо необъяснимо медленные запросы.

Память: главный источник боли

Kubernetes убивает контейнер, превысивший limits.memory, без предупреждения и без изящной деградации. Задача — сделать так, чтобы раньше сработал внутренний лимит ClickHouse: он умеет аккуратно отменить запрос с внятной ошибкой.

Порядок величин должен выстраиваться так:

PLAINTEXT
max_memory_usage (один запрос)
    <  max_server_memory_usage (весь сервер)
        <  limits.memory (контейнер)
Нажмите, чтобы развернуть и увидеть больше

Ключевые настройки:

Три практических требования:

  1. requests.memory = limits.memory. Класс QoS Guaranteed защищает от вытеснения при нехватке памяти на ноде. Для базы данных бурстить памятью — плохая идея.
  2. Оставьте запас 10–20% между лимитом контейнера и max_server_memory_usage.
  3. Разрешите слив на диск для тяжёлых операций, чтобы запрос замедлялся, а не падал:
SQL
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 нод:

Если политика организации требует лимитов — согласуйте max_threads с числом доступных ядер, иначе ClickHouse будет плодить потоки, которые тут же упрутся в троттлинг.

Профили и квоты: защита от одного тяжёлого запроса

Одна из лучших практик, которую внедряют слишком поздно: разные профили настроек для разных потребителей. Дашборд не должен иметь права выполнить запрос, кладущий кластер.

XML
<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 держит несколько кэшей, и их размеры входят в бюджет памяти:

Не раздувайте кэши «на всякий случай»: память полезнее отдать под выполнение запросов и страничный кэш ОС.

Фоновые задачи

Слияния конкурируют с запросами. Размеры фоновых пулов (background_pool_size и родственные) стоит согласовать с числом ядер: слишком мало — куски копятся и вы идёте к too many parts; слишком много — слияния съедают CPU, нужный запросам.

Дефолты в современных версиях адекватные — трогайте их осознанно, когда видите по метрикам конкретный перекос (system.merges, число активных кусков, утилизация CPU фоном).

Размер узла

Из части 1: ClickHouse эффективнее на меньшем числе крупных узлов. Практический ориентир для продового узла — от 16 ядер и 64 ГиБ памяти; много маленьких подов по 2 ядра дадут хуже и по скорости, и по эксплуатации.

Соотношение памяти к ядрам обычно берут около 4–8 ГиБ на ядро; для тяжёлых агрегаций и джойнов — ближе к верхней границе.

Диагностика

SQL
-- Кто ест память прямо сейчас
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-таблицы, метрики и алерты.

Начать поиск

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

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