Learning Platform
Глоссарий Troubleshooting
Урок 13.03 · 22 мин
Средний
memorylow-memory-killerinternalsoperations

Low-memory killer и поведение кластера под давлением

Лимиты из прошлого урока ловят запросы, которые по отдельности слишком тяжелы. Но есть другой сценарий: каждый запрос в своих лимитах, а кластеру всё равно не хватает памяти, потому что их много одновременно. Десять запросов по 4 GB user memory на ноде с 30 GB доступного heap — каждый легитимен, сумма катастрофична.

Если ничего не предпринять, итог — настоящий OutOfMemoryError JVM: воркер падает, унося все запросы на ноде. Чтобы этого не случилось, у Trino есть механизм поведения под давлением. Его ядро — low-memory killer: компонент, который при нехватке памяти осознанно выбирает и убивает один запрос, чтобы спасти остальные. Разберём, как кластер реагирует на давление по шагам и по какой логике killer выбирает жертву.


Первая линия обороны: блокировка драйверов

Прежде чем кого-то убивать, Trino пытается переждать давление, не давая ему расти. Здесь работает то, что мы описали в уроке 1: оператор не аллоцирует память напрямую, он запрашивает резервирование у менеджера памяти.

Когда нода приближается к пределу доступной памяти, менеджер начинает отказывать в резервировании. Оператор, не получивший память, не падает — он блокируется. Драйвер, в котором этот оператор работает, ставится на паузу: он перестаёт потреблять CPU и перестаёт просить ещё память. Как только память освобождается (другой запрос завершился, кто-то спиллил revocable-состояние), резервирование удаётся — драйвер просыпается и продолжает.

Реакция на давление памяти — по нарастанию
Шаг 1. Блокировка драйверовМенеджер памяти отказывает в новых резервированиях. Драйверы, не получившие память, ставятся на паузу — не растут, ждут освобождения. Запросы живы, просто медленнее
если revocable-память есть
Шаг 2. Spill revocable-памятиSpillable-операторы по команде сбрасывают состояние на локальный диск, освобождая heap. Запросы выживают ценой замедления на disk I/O
если давление не спадает и спиллить нечего
Шаг 3. Low-memory killerКоординатор выбирает один запрос-жертву и убивает его, чтобы освободить память и спасти остальные. Крайняя мера

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


Когда блокировки недостаточно

Блокировка работает, только если давление временное — кто-то скоро освободит память. А если нет? Представь: на ноде десять запросов, каждый держит большую нерастворимую user memory (хэш-таблицы join), и каждый заблокирован, ожидая память. Никто не может продвинуться, потому что для продвижения нужна память, а память освободит только тот, кто продвинется и завершится. Классический дедлок: все ждут всех.

Если просто оставить кластер в таком состоянии, заблокированные запросы будут висеть бесконечно, а нода — простаивать с полным heap. Вот тут и нужен low-memory killer: кто-то должен разорвать дедлок, принудительно убрав одного участника.

WARNING

Дедлок памяти — это не «медленно», это «навсегда». Без low-memory killer кластер под памятным дедлоком встанет намертво: все драйверы заблокированы, никто не освобождает heap, новые запросы тоже не проходят. Killer — не «грубость», а необходимый размыкатель. Лучше потерять один запрос, чем заморозить весь кластер.


Как работает low-memory killer

Low-memory killer живёт на координаторе. Координатор периодически собирает с воркеров информацию о памяти — сколько занято, сколько свободно, какие запросы что держат на каждой ноде. Killer анализирует эту картину и решает: есть ли ситуация, требующая вмешательства.

Killer срабатывает не от первого же отказа в резервировании. Он ждёт, чтобы убедиться: давление устойчивое, а не мгновенный всплеск, который разрешится сам блокировкой драйверов. Для этого есть задержка — свойство task.low-memory-killer.high-memory-pool-utilization и связанная пауза: нода должна провести в состоянии нехватки памяти определённое время, прежде чем killer признает ситуацию дедлоком и начнёт действовать. Эта задержка отсеивает ложные срабатывания: дать блокировке драйверов шанс разрулить всё самостоятельно.

Поведением killer управляет свойство query.low-memory-killer.policy. Доступные политики:

Значение query.low-memory-killer.policyЧто делает
total-reservation-on-blocked-nodes(дефолт) Убивает запрос, занимающий больше всего памяти на тех нодах, что испытывают нехватку. Прицельно бьёт по виновнику давления
total-reservationУбивает запрос с наибольшим суммарным резервированием памяти по всему кластеру
noneKiller выключен. Не рекомендуется: при дедлоке кластер встанет
# etc/config.properties (координатор)
# Политика выбора жертвы low-memory killer
query.low-memory-killer.policy=total-reservation-on-blocked-nodes

Почему жертва — самый «большой» запрос

Логика выбора жертвы у дефолтной политики не случайна. Killer бьёт по запросу, который держит больше всего памяти на проблемных нодах, по двум причинам.

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

Во-вторых, справедливость: именно тяжёлый запрос с наибольшей вероятностью и есть причина давления. Убить его — значит наказать виновника, а не случайного соседа. Десять лёгких дашбордных запросов и один монструозный аналитический join: killer уберёт join, дашборды переживут.

Killer выбирает жертву на проблемной ноде
Запрос A — 2 GBЛёгкий дашбордный запрос. Killer его не трогает: освобождение мало и он почти наверняка не виновник давления
Запрос B — 3 GBСредний ad-hoc запрос. Тоже не первый кандидат на убийство
Запрос C — 22 GBТяжёлый аналитический join — держит больше всего памяти на проблемной ноде. ВЫБРАН killer'ом: убийство освобождает максимум heap и почти наверняка бьёт по виновнику

Убитый запрос завершается с ошибкой вида Query killed because the cluster is out of memory и кодом CLUSTER_OUT_OF_MEMORY. Это сигнал не «запрос плохой», а «кластеру в этот момент не хватило памяти на всех». Реакция инженера — посмотреть на конкуренцию: возможно, нужны resource groups (урок 5), чтобы тяжёлая аналитика не конкурировала с дашбордами, либо spilling (урок 4), чтобы запросы выживали за счёт диска, либо просто больше памяти на кластере.


Полная картина поведения под давлением

Соберём всё вместе. Когда на ноде растёт давление памяти, Trino проходит по ступеням:

  1. Менеджер памяти отказывает в новых резервированиях. Рост памяти на ноде останавливается.
  2. Драйверы, не получившие память, блокируются. Они на паузе, не потребляют CPU, ждут освобождения. Если давление временное — кто-то скоро завершится, заблокированные проснутся, инцидент исчерпан.
  3. Spillable-операторы отзывают revocable-память. По команде они сбрасывают состояние на диск, освобождая heap. Если revocable-память есть и её достаточно — давление спадает без жертв.
  4. Давление устойчиво, спиллить больше нечего, признаки дедлока. После выдержки задержки координатор признаёт ситуацию дедлоком.
  5. Low-memory killer выбирает жертву по политике (дефолт — самый «тяжёлый» запрос на проблемных нодах) и убивает её. Память освобождается, дедлок разомкнут, остальные запросы продолжают.
NOTE

Заметь порядок: убийство — последняя ступень, а не первая. Trino сначала тормозит рост, потом пытается обойтись spill, и только убедившись, что иначе кластер встанет, жертвует одним запросом. Если в твоём кластере killer срабатывает часто — это симптом: либо постоянная перегрузка по памяти, либо отсутствие изоляции нагрузки через resource groups, либо запросы, которым нужен включённый spilling.


Диагностика срабатываний killer

Когда killer убивает запрос, важно отличить «разовую невезуху» от «системной проблемы». Сигнал один и тот же — запрос завершился с CLUSTER_OUT_OF_MEMORY, — но реакция инженера зависит от того, как часто это происходит.

Где смотреть:

  • Текст ошибки запроса. Убитый killer’ом запрос завершается с сообщением вида Query killed because the cluster is out of memory и кодом CLUSTER_OUT_OF_MEMORY. Это явный маркер: запрос убил именно killer, а не per-query лимит (EXCEEDED_GLOBAL_MEMORY_LIMIT / EXCEEDED_LOCAL_MEMORY_LIMIT — это другое, превышение лимита одним запросом).
  • Web UI. В списке завершённых запросов видны failed-запросы с этим кодом. Если их единицы в день — кластер изредка попадает под пик. Если десятки — кластер хронически перегружен.
  • Метрики через jmx. Менеджер памяти и killer публикуют JMX-метрики; по ним строят алерт «частота срабатываний killer выросла».

Как читать частоту:

Spark: unified memory manager и давление памяти
Частота CLUSTER_OUT_OF_MEMORYЧто это значитРеакция
Единичные случаи, редкие пикиКластер изредка попадает под совпадение тяжёлых запросовОбычно терпимо; можно ничего не менять
Регулярно, в предсказуемые часыВ пиковые окна нагрузка стабильно превышает памятьResource groups для изоляции, расписание тяжёлых запросов, spilling
Постоянно, в разное времяКластер хронически недоразмерен по памятиДобавить воркеров или heap; пересмотреть лимиты
WARNING

Killer — это индикатор, а не решение. Само его срабатывание не «чинит» перегрузку, оно лишь не даёт кластеру встать. Если CLUSTER_OUT_OF_MEMORY сыплется регулярно, бороться надо не с killer (отключать его политикой none категорически нельзя — вернётся дедлок), а с причиной: изоляцией нагрузки, spilling или ёмкостью кластера.


Попробуй сам

Спровоцируй давление памяти на тестовом Trino и понаблюдай за реакцией.

  1. Возьми воркер с небольшим heap (например, контейнер с -Xmx2G через JAVA_TOOL_OPTIONS) и ужми query.max-memory так, чтобы один тяжёлый join всё же проходил, но запас был мал.
  2. Запусти параллельно несколько тяжёлых join’ов по tpch.sf100 — например, открой три CLI-сессии и стартуй запросы почти одновременно. Следи за Web UI: какие запросы переходят в состояние с заблокированными драйверами (растёт «Blocked» время), а какой получает ошибку CLUSTER_OUT_OF_MEMORY.
  3. Поменяй query.low-memory-killer.policy с дефолтного на total-reservation, повтори сценарий и подумай: изменился ли выбор жертвы и почему один запрос-обжора будет выбран обеими политиками одинаково.

Цель — увидеть своими глазами ступени «блокировка -> spill -> kill» и понять, что убийство запроса по памяти — это защита кластера, а не сбой.


Проверка знанийKnowledge check
Зачем Trino нужен low-memory killer, если уже есть блокировка драйверов при нехватке памяти? Почему одной блокировки недостаточно?
ОтветAnswer
Блокировка драйверов спасает только от временного давления: когда оператор не может зарезервировать память, его драйвер ставится на паузу и ждёт, пока память освободится — например, пока другой запрос завершит сортировку и отпустит heap. Это работает, если кто-то действительно скоро освободит память. Но возможна ситуация дедлока: на ноде много запросов, каждый держит большую нерастворимую user memory (хэш-таблицы join), и каждый заблокирован в ожидании памяти. Никто не может продвинуться, потому что для продвижения нужна память, а память освободит только тот, кто продвинётся и завершится — все ждут всех. Блокировка такой дедлок не разорвёт: кластер встанет навсегда, все драйверы заморожены, heap полон, новые запросы тоже не проходят. Low-memory killer — это размыкатель дедлока: координатор после выдержки задержки выбирает один запрос-жертву (по дефолтной политике — занимающий больше всего памяти на проблемных нодах) и убивает его. Освобождённая память даёт остальным запросам продвинуться. Лучше потерять один запрос, чем заморозить весь кластер.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что Trino делает ПЕРВЫМ, когда нода приближается к пределу доступной памяти?

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

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

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

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