Настройка памяти и MemoryTracker
RAM — главный ресурс для выполнения аналитических запросов в ClickHouse. GROUP BY на больших таблицах, сортировки по большим результирующим наборам, сложные JOIN — всё это требует оперативной памяти. Ошибка MEMORY_LIMIT_EXCEEDED — одна из самых распространённых в production-системах. ClickHouse предоставляет гибкий набор настроек для управления потреблением памяти: от жёстких лимитов до механизма spill-to-disk, который позволяет завершить тяжёлый запрос без нехватки RAM.
max_memory_usage: лимит памяти на запрос
max_memory_usage ограничивает потребление оперативной памяти для одного запроса. При превышении лимита ClickHouse немедленно прерывает запрос с исключением MEMORY_LIMIT_EXCEEDED.
-- Установить лимит 10 GB для текущего запроса
SET max_memory_usage = 10000000000;
-- Запрос будет прерван при превышении 10 GB
SELECT
user_id,
count() AS events,
sum(revenue) AS total_revenue
FROM large_events_table
GROUP BY user_id;
Значение 0 означает отсутствие лимита (поведение по умолчанию). Лимит задаётся в байтах.
Для ограничения на уровне пользователя (не отдельного запроса) используйте max_memory_usage_for_user:
-- Ограничить суммарное потребление памяти пользователем analyst
-- (применяется ко всем его одновременным запросам)
SET max_memory_usage_for_user = 8000000000; -- 8 GB
Внешняя агрегация: max_bytes_before_external_group_by
По умолчанию GROUP BY операции выполняются в памяти. Если таблица слишком велика, GROUP BY завершится ошибкой. Настройка max_bytes_before_external_group_by включает механизм spill-to-disk: при превышении порога промежуточные данные агрегации выгружаются на диск, и запрос завершается без нехватки RAM.
-- Разрешить spill на диск при накоплении 10 GB промежуточных данных
SET max_bytes_before_external_group_by = 10000000000;
-- Теперь GROUP BY на большой таблице завершится, а не упадёт с OOM
SELECT
user_id,
count() AS session_count,
sum(duration_sec) AS total_duration
FROM user_sessions_50gb
GROUP BY user_id;
Spill-to-disk делает запрос медленнее (операции с диском), но позволяет выполнить его при ограниченной RAM.
Если max_bytes_before_external_group_by установлен, рекомендуется задать max_memory_usage как минимум в два раза больше этого значения. ClickHouse может использовать до 2x указанного порога для финальной агрегации прочитанных из диска данных. Например: если max_bytes_before_external_group_by = 10G, то max_memory_usage должен быть не менее 20G.
Внешняя сортировка: max_bytes_before_external_sort
Аналогичный механизм существует для операций ORDER BY. По умолчанию значение 0 (отключено). При включении промежуточные данные сортировки выгружаются на диск.
-- Разрешить spill на диск для ORDER BY при превышении 10 GB
SET max_bytes_before_external_sort = 10000000000;
-- ORDER BY на большом результирующем наборе не упадёт с OOM
SELECT *
FROM large_log_table
WHERE date >= today() - 30
ORDER BY event_time DESC;
max_bytes_before_external_sort по умолчанию равен 0 — внешняя сортировка отключена. В отличие от внешней агрегации, она не включается автоматически. Запрос с большим ORDER BY упадёт с MEMORY_LIMIT_EXCEEDED, если явно не установить этот параметр.
MemoryTracker: мониторинг потребления памяти
ClickHouse имеет встроенный MemoryTracker, отслеживающий потребление памяти на уровне запросов, пользователей и всего сервера. Данные доступны через системные таблицы.
Текущее суммарное потребление памяти
-- Текущее суммарное потребление памяти сервером
SELECT metric, value
FROM system.metrics
WHERE metric = 'MemoryTracking';
Развёрнутая статистика через asynchronous_metrics
-- Все метрики памяти с human-readable значениями
SELECT
metric,
formatReadableSize(value) AS readable_value
FROM system.asynchronous_metrics
WHERE metric LIKE '%Mem%'
ORDER BY value DESC;
Пример вывода:
metric | readable_value
------------------------------|---------------
MemoryCode | 256.00 MiB
MemoryDataAndStack | 14.32 GiB
OSMemoryTotal | 64.00 GiB
OSMemoryAvailable | 38.45 GiB
OSMemoryFreePlusCached | 41.20 GiB
Функция formatReadableSize() переводит байты в удобочитаемый формат (MiB, GiB).
Потребление памяти по завершённым запросам
-- Топ-10 запросов по потреблению памяти за последний час
SELECT
initial_query_id,
formatReadableSize(memory_usage) AS memory,
query_duration_ms,
substring(query, 1, 80) AS query_preview
FROM system.query_log
WHERE type = 'QueryFinish'
AND event_time >= now() - INTERVAL 1 HOUR
ORDER BY memory_usage DESC
LIMIT 10;
Сводная таблица настроек
| Настройка | Назначение | По умолчанию | Поведение при превышении |
|---|---|---|---|
max_memory_usage | Лимит RAM на один запрос | 0 (нет лимита) | MEMORY_LIMIT_EXCEEDED |
max_memory_usage_for_user | Суммарный лимит RAM на пользователя | 0 (нет лимита) | MEMORY_LIMIT_EXCEEDED |
max_bytes_before_external_group_by | Порог spill для GROUP BY | 0 (отключено) | Spill промежуточных данных на диск |
max_bytes_before_external_sort | Порог spill для ORDER BY | 0 (отключено) | Spill промежуточных данных на диск |
Практические рекомендации
Для многопользовательских аналитических систем рекомендуется следующая стартовая конфигурация:
-- Пример для сервера с 64 GB RAM, 8 concurrent пользователей:
-- Лимит на одного пользователя: 80% RAM / 8 пользователей = 6.4 GB
-- Используем консервативное значение 6 GB
SET max_memory_usage_for_user = 6000000000;
-- Лимит на один тяжёлый запрос: половина пользовательского лимита
SET max_memory_usage = 3000000000;
-- Порог external GROUP BY: max_memory_usage / 2 как стартовое значение
SET max_bytes_before_external_group_by = 1500000000;
Формула для расчёта max_memory_usage_for_user:
max_memory_usage_for_user = (общий RAM * 0.8) / ожидаемое_число_concurrent_пользователей
Ключевые выводы
max_memory_usageограничивает RAM на уровне запроса — при превышении запрос прерывается сMEMORY_LIMIT_EXCEEDED. Значение0означает отсутствие лимита.max_bytes_before_external_group_byвключает spill-to-disk для GROUP BY. Запрос выполнится медленнее, но не упадёт с OOM. Установитеmax_memory_usageв 2x от этого значения.max_bytes_before_external_sort— аналогичный механизм для ORDER BY. По умолчанию отключён (значение 0).- MemoryTracker доступен через
system.metrics(текущее состояние) иsystem.asynchronous_metrics(расширенная статистика). ФункцияformatReadableSize()переводит байты в читаемый формат. - Стартовое значение
max_memory_usage_for_user: (80% RAM) / число_concurrent_пользователей.