Learning Platform
Глоссарий Troubleshooting
Урок 13.04 · 23 мин
Средний
memoryspillingconfigurationinternals

Spilling на диск: aggregations, joins, sort, window

К этому моменту мы знаем: если запросу не хватает памяти, он либо ждёт (блокировка драйверов), либо его убивают (low-memory killer). Но есть третий путь — spilling: выгрузить часть состояния на локальный диск, освободить heap и довести запрос до конца, заплатив замедлением вместо смерти.

Spilling превращает «запрос упал по памяти» в «запрос отработал медленнее». Это компромисс: диск на порядки медленнее RAM, поэтому spill — не бесплатное расширение памяти, а аварийный клапан. Разберём, какие операторы умеют спиллить, как это устроено механически, как настраивается и почему за spill всегда платят дисковым I/O.


Что такое spill и какая память спиллится

В уроке 1 мы ввели revocable memory — память, которую запрос может вернуть. Spill — это и есть механизм возврата: spillable-оператор берёт часть своего состояния из heap, сериализует его на локальный диск воркера, освобождает heap-память, а позже, когда состояние снова понадобится, читает его обратно с диска.

Спиллится именно revocable-память — и в этом весь смысл деления памяти на категории. User memory нельзя ужать: хэш-таблица join обязана быть в памяти, чтобы по ней искать. А revocable-память spillable-оператора — можно: оператор спроектирован так, что умеет работать, имея часть состояния на диске.

Spill поддержан для четырёх классов операторов:

  • Hash aggregationGROUP BY. Спиллится хэш-таблица групп.
  • Hash join — как inner, так и outer. Спиллится build-сторона.
  • SortORDER BY. Спиллятся отсортированные сегменты (runs).
  • Window functions — спиллятся буферы партиций окна.
DuckDB: buffer manager и larger-than-memory Spark: управление памятью и spill
Spill: revocable-состояние уходит на диск и возвращается
Оператор в heapSpillable-оператор: hash aggregation, hash join, sort или window. Часть его состояния — revocable-память
spill: сериализация
Spill-файлы на локальном дискеСериализованные страницы на локальном диске воркера. Каталог задаётся spiller-spill-path. Сжимаются и опционально шифруются
unspill: чтение обратно
Финальная обработкаОператор дочитывает спилленное состояние с диска и завершает работу: досчитывает агрегацию, домерживает join, выдаёт отсортированный поток

Механика по операторам

Spill — это не «скинули кусок RAM на диск и забыли». Каждый класс операторов спиллит по-своему, потому что должен уметь досчитать правильный результат, имея часть данных на диске.

Hash aggregation. Хэш-таблица групп при давлении сбрасывается на диск частями. Когда входные данные кончились, оператор не может просто отдать результат: одни и те же группы могли попасть и в память, и в несколько спилл-файлов. Он считывает спилленные сегменты и сливает аккумуляторы одинаковых ключей (merge) — только после слияния агрегат по каждой группе верен.

Hash join. Спиллится build-сторона. Чтобы сохранить корректность, build и probe партиционируются по join-ключу на согласованные партиции. Если build-партиция спиллилась, соответствующая probe-партиция тоже придерживается, и пара досоединяется отдельным проходом после чтения build-партиции с диска.

Sort. Классический external merge sort. Оператор набирает данные в памяти, сортирует этот фрагмент, сбрасывает на диск как отсортированный run. Так накапливается несколько отсортированных runs на диске. В конце оператор сливает runs (k-way merge), выдавая единый отсортированный поток.

DuckDB: external sort и larger-than-memory sort

Window. Window-функция работает над партициями окна; буферы партиций спиллятся, а при вычислении функции дочитываются с диска.

NOTE

Общий узор всех четырёх случаев — spill, а затем merge. Спилленное состояние нельзя просто «забыть»: оператор обязан собрать корректный результат из памяти плюс диска. Поэтому spill стоит не только записи на диск, но и дополнительного чтения и слияния — это часть цены.


Цена spill: почему это аварийный клапан

Spill спасает запрос от гибели, но даётся дорого. Узкое место — disk I/O.

Локальный SSD выдаёт порядки гигабайт в секунду, RAM — десятки и сотни гигабайт в секунду; латентность доступа отличается ещё драматичнее. Когда оператор спиллит, он:

  1. сериализует страницы (CPU на сериализацию и сжатие);
  2. пишет их на диск (disk write I/O);
  3. позже читает обратно (disk read I/O);
  4. десериализует и сливает (CPU на merge).

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

Без spill и со spill — компромисс
Без spillВсё состояние в RAM. Максимальная скорость. Но при нехватке памяти запрос убивается low-memory killer
включаем spill
Со spillЧасть состояния уходит на локальный диск. Запрос выживает на объёмах больше RAM, но платит за это disk I/O на запись, чтение и merge

Конфигурация spill

Spill настраивается на воркерах (это они держат состояние и диски), в etc/config.properties.

# etc/config.properties (на каждом воркере)

# Включить spill глобально
spill-enabled=true

# Каталог(и) для spill-файлов. Можно несколько через запятую —
# Trino распределит нагрузку по разным физическим дискам
spiller-spill-path=/mnt/spill1,/mnt/spill2

# Потолок суммарного места под spill на ноду — для ВСЕХ запросов сразу
max-spill-per-node=100GB

# Потолок места под spill на ноду для ОДНОГО запроса
query-max-spill-per-node=20GB

# Кодек сжатия spill-страниц (экономит I/O ценой CPU)
spill-compression-codec=LZ4

# Шифрование spill-файлов: на каждый файл — свой случайный ключ
spill-encryption-enabled=true

Точные имена и дефолты свойств сверяй со страницей admin/spill той версии Trino, которую разворачиваешь, — набор и значения иногда меняются между релизами. Что важно понять про каждый рычаг:

  • spiller-spill-path — список каталогов через запятую. Несколько каталогов на разных дисках складывают I/O-пропускную способность дисков. Это самый эффективный способ ускорить spill.
  • max-spill-per-node — общий потолок места под spill на ноде. Не даёт spill-файлам забить диск полностью.
  • query-max-spill-per-node — потолок на один запрос, чтобы один запрос не выел весь spill-бюджет ноды.
  • spill-compression-codec — сжатие страниц перед записью. Меньше байт на диск ценой CPU; обычно сжатие выгодно, потому что disk I/O — более дефицитный ресурс, чем CPU.
  • spill-encryption-enabled — на каждый spill-файл генерируется случайный ключ. Промежуточные данные запроса временно лежат на диске в открытом виде без шифрования — для чувствительных данных это включают.
DANGER

Два правила выбора диска под spill. Первое: НЕ спилль на системный диск ОС — забитый spill’ом системный диск способен подвесить всю ноду. Второе: НЕ спилль на тот же диск, куда пишутся логи JVM, — spill и логирование начнут конкурировать за I/O и мешать друг другу. Под spill выделяй отдельные диски (/mnt/spill1, /mnt/spill2), а не корневой раздел.


Когда spill включать, а когда нет

Spill — не «включил и забыл». Решение зависит от профиля нагрузки кластера.

Профиль нагрузкиSpillПочему
Интерактивные дашборды, низкая латентность критичнаСкорее выключитьSpill превращает быстрый запрос в медленный; лучше отклонить тяжёлый запрос, чем замедлить все
Тяжёлая batch-аналитика, важнее завершить, чем быстроСкорее включитьЛучше медленный успешный запрос, чем убитый по памяти
Смешанная нагрузкаSpill + resource groupsИзолировать batch от интерактива (урок 5), spill — для batch-группы

Идея в том, что spill меняет приоритет с «быстро» на «дойти до конца». Для batch-ETL это правильный размен. Для дашборда, который пользователь ждёт на экране, — почти всегда нет: медленный дашборд воспринимается как сломанный.


Попробуй сам

Включи spill на тестовом Trino и поймай его срабатывание.

  1. В etc/config.properties воркера задай spill-enabled=true и spiller-spill-path на отдельный каталог (например, /tmp/trino-spill). Перезапусти Trino.
  2. Возьми воркер с небольшим heap и запусти крупную агрегацию с высокой кардинальностью группировки или сортировку большого набора — что-то, что точно не уместится в RAM, например SELECT ..., count(*) FROM tpch.sf100.lineitem GROUP BY <много колонок>.
  3. Во время выполнения посмотри в каталог spill — там появятся файлы. После завершения запроса они исчезнут. В Web UI в деталях запроса найди показатели spill (объём спилленного).
  4. Засеки время того же запроса со spill-enabled=true и spill-enabled=false (если без spill он не падает) — оцени, во сколько раз spill замедлил выполнение.

Цель — увидеть spill-файлы вживую и прочувствовать, что spill спасает запрос ценой реального замедления.


Проверка знанийKnowledge check
Почему spill поддержан только для hash aggregation, hash join, sort и window, но не, например, для произвольного оператора? И почему spill всегда сопровождается фазой merge?
ОтветAnswer
Spill работает только там, где оператор спроектирован так, чтобы давать корректный результат, имея часть состояния на диске. У этих четырёх классов операторов есть промежуточное состояние, которое можно разбить на части и собрать обратно: хэш-таблица групп в aggregation, build-сторона в join, отсортированные сегменты в sort, буферы партиций в window. Произвольный оператор такого свойства не имеет — нельзя просто «выгрузить кусок RAM на диск» и ожидать, что результат останется верным. Фаза merge обязательна именно потому, что спилленное состояние нельзя забыть: после spill данные оказываются разбросаны между памятью и несколькими спилл-файлами. В hash aggregation одни и те же группы могут попасть и в RAM, и в разные файлы — нужно слить аккумуляторы одинаковых ключей. В sort на диске лежит несколько отсортированных runs — нужен k-way merge в единый поток. В hash join спилленная build-партиция досоединяется отдельным проходом с придержанной probe-партицией. Без merge результат был бы неполным или неверным. Поэтому spill стоит не только записи на диск, но и дополнительного чтения с диска и слияния — это часть его цены, из-за которой spill остаётся аварийным клапаном, а не бесплатным расширением памяти.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какая память спиллится на диск и для каких операторов поддержан spill?

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

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

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

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