Learning Platform
Глоссарий Troubleshooting
Урок 13.02 · 22 мин
Средний
memoryconfigurationlimits

Лимиты памяти: max-memory, max-memory-per-node, max-total-memory

В прошлом уроке мы разобрали категории памяти: user, revocable, system. Теперь — конкретные рычаги. Trino даёт три основных свойства лимитов памяти, и они защищают кластер с двух разных сторон: один лимит ограничивает общий по кластеру расход запроса, другой — расход на одной ноде, третий накрывает то же сверху, включая отзываемую память.

Перепутать их легко, а цена ошибки высока: слишком жёсткий лимит убивает легитимные тяжёлые запросы, слишком мягкий — пускает запрос-обжору положить ноду. Разберём каждое свойство: что именно оно считает, какой у него дефолт и как они работают в связке.


Кластерный лимит: query.max-memory

query.max-memory ограничивает суммарную распределённую user memory одного запроса по всему кластеру. Это потолок «сколько данных запросу позволено держать в памяти на всех воркерах вместе взятых».

Дефолт — 20 GB. Если у вас 10 воркеров и запрос на каждом держит по 2.5 GB user memory под хэш-таблицы join, суммарно это 25 GB — превышение дефолтного лимита, и запрос будет убит с ошибкой EXCEEDED_GLOBAL_MEMORY_LIMIT.

Важные свойства этого лимита:

  • Считается только user memory — нерастворимая память (join hash-таблицы, GROUP BY, sort без spill). Revocable-память сюда не входит.
  • Считается сумма по всему кластеру, а не по одной ноде. Запрос может занять 2 GB на каждом из 10 воркеров — итого 20 GB, на грани.
  • Превышение -> запрос убивается, потому что user memory нечем ужать.
# etc/config.properties (на координаторе)
# Кластерный потолок user memory одного запроса
query.max-memory=20GB
NOTE

20 GB — это дефолт, рассчитанный на скромный кластер. На продакшен-кластере из мощных воркеров с большим heap его обычно поднимают: тяжёлая аналитика по lakehouse легко требует сотни GB суммарной user memory. Поднимать стоит осознанно — лимит существует, чтобы один монструозный запрос не выел всю память кластера и не заморил конкурентные запросы.


Лимит на ноду: query.max-memory-per-node

query.max-memory-per-node ограничивает user memory одного запроса на одном воркере. Это не «сколько всего на ноде», а «сколько на ноде позволено занять одному конкретному запросу».

Дефолт — 30% максимального heap ноды. Дефолт не абсолютная цифра, а доля от -Xmx. На воркере с -Xmx100G это 30 GB; на воркере с -Xmx48G — 14.4 GB. Это удобно: один и тот же config.properties корректно масштабируется на воркеры разного размера.

Зачем нужен отдельный per-node лимит, если уже есть кластерный? Потому что данные распределяются по нодам неравномерно. Из-за перекоса (data skew) запрос может уложиться в кластерные 20 GB, но при этом 18 GB из них окажутся на одной несчастливой ноде — и именно эта нода свалится в OOM, хотя «по кластеру всё хорошо». Per-node лимит ловит такой перекос: как только запрос на любой ноде превышает свой per-node потолок, он убивается с EXCEEDED_LOCAL_MEMORY_LIMIT, не успев положить ноду.

Кластерный лимит не видит перекоса по нодам
query.max-memory = 20GB — суммарно по кластеруСчитает сумму user memory запроса по всем воркерам. Не знает, как она распределена между нодами
но на нодах перекос
query.max-memory-per-node = 30% heap — на каждой нодеЛовит перекос: даже при соблюдённом кластерном лимите одна нода может перегрузиться из-за data skew
# etc/config.properties
# Потолок user memory ОДНОГО запроса на ОДНОЙ ноде
# 30% максимального heap воркера (дефолт)
query.max-memory-per-node=30%

Лимит сверху: query.max-total-memory

query.max-total-memory ограничивает всю память запроса по кластеру — user плюс revocable.

Дефолт — query.max-memory x 2. При дефолтном query.max-memory=20GB это означает query.max-total-memory=40GB.

Логика разделения такая. query.max-memory следит за «нерастворимой» частью — той, что нельзя отозвать. query.max-total-memory следит за полным отпечатком запроса в памяти, потому что revocable-память, пока не спилилась, тоже реально занимает heap прямо сейчас. Запас в два раза даёт spillable-операторам место накопить revocable-состояние, прежде чем оно сбросится на диск. Без total-лимита запрос с интенсивным spill мог бы держать гигантское revocable-состояние, и сумма user + revocable незаметно перегрузила бы кластер.

Превышение total-лимита -> запрос убивается. Аналогично есть и per-node вариант, query.max-total-memory-per-node, ограничивающий user + revocable на одной ноде.

Что считает каждый из трёх лимитов
max-memory-per-nodeUser memory одного запроса на одной ноде. Дефолт 30% heap ноды. Ловит data skew
max-memoryСуммарная user memory запроса по всему кластеру. Дефолт 20GB. Ограничивает общую тяжесть запроса
max-total-memoryВся память запроса по кластеру: user + revocable. Дефолт = max-memory x 2 = 40GB. Накрывает и отзываемую память

Точные дефолты — сводка

Запомни эти значения наизусть: это самый частый предмет вопросов и самый частый источник ошибок конфигурации.

СвойствоЧто лимитируетДефолтОшибка при превышении
query.max-memoryuser memory, сумма по кластеру20GBEXCEEDED_GLOBAL_MEMORY_LIMIT
query.max-memory-per-nodeuser memory, одна нода30% максимального heap нодыEXCEEDED_LOCAL_MEMORY_LIMIT
query.max-total-memoryuser + revocable, сумма по кластеруquery.max-memory x 2EXCEEDED_GLOBAL_MEMORY_LIMIT
memory.heap-headroom-per-nodeрезерв heap под system-аллокации30% максимального heap ноды(не лимит запроса — резерв ноды)

Обрати внимание: дефолт query.max-total-memory задан относительно query.max-memory. Поднимешь query.max-memory до 100 GB, не трогая total, — query.max-total-memory автоматически станет 200 GB. Это удобно, но об этом легко забыть.


Как лимиты работают вместе — пример

Кластер: координатор плюс 8 воркеров, на каждом воркере -Xmx80G. Оставляем все свойства памяти дефолтными. Что получаем:

  • query.max-memory-per-node = 30% от 80 GB = 24 GB — потолок user memory одного запроса на одном воркере.
  • memory.heap-headroom-per-node = 30% от 80 GB = 24 GB — резерв под system-аллокации.
  • Проверка правила старта: 24 GB (per-node) + 24 GB (headroom) = 48 GB < 80 GB (-Xmx) — конфигурация валидна, остаётся 32 GB «дыхания» под прочие конкурентные запросы.
  • query.max-memory = 20 GB — суммарный кластерный потолок user memory запроса.
  • query.max-total-memory = 20 GB x 2 = 40 GB — потолок user + revocable по кластеру.

Тонкий момент: query.max-memory (20 GB) меньше, чем query.max-memory-per-node x число воркеров (24 GB x 8 = 192 GB). Это нормально и осознанно. Per-node лимит — это страховка от перекоса на отдельной ноде, а не «бюджет, который надо набрать со всех нод». Реальным кластерным потолком user memory работает именно query.max-memory: запрос упрётся в 20 GB задолго до того, как приблизится к per-node лимитам.

WARNING

Частая ошибка при тюнинге: поднять query.max-memory до огромного значения, забыв про query.max-memory-per-node и про правило per-node + headroom < -Xmx. Кластерный лимит станет щедрым, но запрос всё равно упрётся в per-node потолок на первой же ноде с перекосом. Поднимать лимиты надо согласованно, держа в голове все четыре свойства разом.

Превышение любого лимита даёт информативную ошибку. Пример вывода в CLI:

trino> SELECT l.*, o.* FROM tpch.sf1000.lineitem l
    -> JOIN tpch.sf1000.orders o ON l.orderkey = o.orderkey;

Query 20260520_101500_00012_a3k9p failed:
Query exceeded distributed user memory limit of 20GB
[EXCEEDED_GLOBAL_MEMORY_LIMIT]

Сообщение прямо называет нарушенный лимит и его значение. Дальше у инженера три пути: переписать запрос (фильтры, уменьшить join), включить spilling (перевести часть нагрузки в revocable + диск), либо осознанно поднять лимит, если кластер реально потянет.

ClickHouse: настройка лимитов памяти Spark: broadcast hint и борьба с нехваткой памяти на join

Глобальные лимиты против сессионных override

Свойства из config.properties — это глобальные лимиты: они действуют на весь кластер и меняются только перезапуском. Но иногда нужен потолок не «для кластера», а «для конкретного запроса или пользователя». Для этого у части memory-свойств есть сессионные аналоги, задаваемые через SET SESSION без перезапуска кластера.

Сессионное свойство query_max_memory_per_node — пример такого override. Оно соответствует глобальному query.max-memory-per-node, но действует только в рамках текущей сессии:

-- Понизить per-node лимит только для своей сессии
SET SESSION query_max_memory_per_node = '8GB';

-- Посмотреть, что действует сейчас в сессии
SHOW SESSION LIKE 'query_max_memory%';

Важная асимметрия: сессионный override может лимит только понизить относительно глобального, но не поднять выше. Логика очевидна — иначе любой пользователь обходил бы защиту кластера, выставив себе гигантский лимит одной строкой SQL. Глобальное свойство задаёт жёсткий потолок, сессионное — позволяет отдельному запросу быть скромнее этого потолка.

Зачем это на практике. Аналитик знает, что его конкретный запрос тяжёлый и рискует выесть память в ущерб соседям, — он сам понижает себе лимит. Или администратор через resource groups и сессионные свойства даёт «дешёвой» группе запросов более строгие лимиты, чем кластерный дефолт. Сессионные override — это инструмент тонкой настройки поверх грубого глобального потолка.

NOTE

Не все memory-свойства имеют сессионный аналог, и набор сессионных свойств меняется между версиями Trino. Полный актуальный список смотри командой SHOW SESSION на своём кластере или на странице свойств своей версии. Принцип же неизменен: сессионный override уточняет глобальный лимит в сторону строгости, но не ослабляет защиту кластера.


Наблюдение за памятью запроса

Лимиты бессмысленно настраивать вслепую — нужно видеть, сколько памяти запросы реально потребляют. У Trino два основных окна наблюдения.

Web UI координатора. В списке запросов и в деталях каждого запроса видны показатели памяти — текущая и пиковая user memory, total memory. Сравнив пиковую память типичных запросов с лимитами, понимаешь, есть ли запас или кластер работает на грани.

Коннектор jmx. Менеджер памяти публикует свои счётчики как JMX-метрики, а коннектор jmx даёт запрашивать их обычным SQL:

-- Состояние учёта памяти на нодах кластера
SELECT node, freebytes, reservedbytes, maxbytes
FROM jmx.current."trino.memory:name=general,type=memorypool";
        node          |  freebytes   | reservedbytes |   maxbytes
----------------------+--------------+---------------+--------------
 trino-worker-1       |  48318382080 |   12884901888 |  61203283968
 trino-worker-2       |  55834574848 |    5368709120 |  61203283968

reservedbytes — это и есть бухгалтерия менеджера памяти из урока 1: сколько байт сейчас числится занятыми. Регулярно глядя на эти метрики под нагрузкой, видишь, насколько близко кластер подходит к лимитам и пора ли их пересматривать или добавлять воркеров.


Попробуй сам

Запусти Trino и проверь лимиты на практике.

  1. Посмотри текущие действующие значения через системную таблицу:

    SELECT * FROM system.metadata.materialized_views;  -- пример системной схемы

    Точнее лимиты видны через коннектор jmx — найди MBean’ы менеджера памяти и сверь их с тем, что ожидаешь по дефолтам.

  2. Временно ужми лимит в etc/config.properties, например query.max-memory=1GB, перезапусти Trino и запусти намеренно тяжёлый join по tpch.sf100. Поймай ошибку EXCEEDED_GLOBAL_MEMORY_LIMIT и прочитай её текст целиком.

  3. Для своего реального или гипотетического воркера с конкретным -Xmx посчитай все четыре значения из таблицы дефолтов и проверь правило query.max-memory-per-node + memory.heap-headroom-per-node < -Xmx. Если оно нарушено — какие свойства и как ты поменяешь?

Цель — уметь по -Xmx за минуту восстановить всю раскладку лимитов и заметить опасную конфигурацию.


Проверка знанийKnowledge check
Запрос укладывается в кластерный лимит query.max-memory, но всё равно падает с ошибкой EXCEEDED_LOCAL_MEMORY_LIMIT. Как такое возможно и какой лимит здесь сработал?
ОтветAnswer
Сработал query.max-memory-per-node — лимит user memory одного запроса на одной ноде, по умолчанию 30% максимального heap воркера. query.max-memory считает суммарную user memory запроса по всему кластеру и не знает, как она распределена между нодами. Из-за перекоса данных (data skew) запрос может уложиться в кластерный потолок — например, 20 GB суммарно — но при этом непропорционально много памяти осесть на одной несчастливой ноде: скажем, один join-ключ встречается в разы чаще остальных, и его хэш-бакет целиком попадает на один воркер. Когда user memory запроса на этой ноде превышает per-node потолок, запрос убивается с EXCEEDED_LOCAL_MEMORY_LIMIT, не дожидаясь, пока нода свалится в реальный OOM. Именно для этого per-node лимит и существует отдельно от кластерного: кластерный ограничивает общую тяжесть запроса, а per-node ловит перекос на конкретной ноде.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Каков дефолт query.max-memory и что именно это свойство ограничивает?

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

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

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

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