CPU-лимиты, query.max-cpu-time и тайм-ауты запросов
Память — не единственный исчерпаемый ресурс кластера. Запрос может уложиться во все лимиты памяти, но при этом сжигать процессорное время часами: декартово произведение по недосмотру, regexp по миллиардам строк, забытый аналитический запрос. Один такой запрос отбирает CPU у всех остальных и тихо деградирует кластер.
Trino даёт два класса защиты по времени. Первый — CPU-лимиты: потолок процессорного времени, которое запросу позволено сжечь. Второй — тайм-ауты: предельная продолжительность разных фаз жизни запроса по «настенным часам». Это разные вещи: CPU-время и реальное время — не одно и то же. Разберём оба и почему по умолчанию они почти не ограничивают.
CPU-время против настенного времени
Сначала — фундаментальное различие, без которого CPU-лимиты не понять.
Настенное время (wall-clock time) — реальное время от старта до финиша запроса по часам на стене. Если запрос шёл 5 минут — это 5 минут настенного времени.
CPU-время — суммарное процессорное время, потраченное на запрос на всех ядрах всех воркеров. Запрос распараллелен: за 5 минут настенного времени он мог занять 200 ядер, и тогда CPU-время — около 1000 минут (200 ядер x 5 минут).
Почему лимитировать именно CPU-время? Потому что CPU-время и есть реальная цена запроса для кластера. Запрос, который висит 2 часа, ожидая медленный внешний источник, почти не потребляет CPU — он не вредит соседям, он просто ждёт. А запрос, который за 10 минут сжёг 5000 ядро-минут на декартовом произведении, отобрал процессор у всех остальных. query.max-cpu-time ловит именно второй случай — прожорливость по процессору, а не долгое ожидание.
query.max-cpu-time и почему дефолт — практическая бесконечность
query.max-cpu-time ограничивает суммарное CPU-время одного запроса по всему кластеру.
Дефолт — практически бесконечность. Внутри это значение 1_000_000_000d — на практике запрос никогда столько не накопит. То есть по умолчанию CPU-время запроса не ограничено вообще.
Почему так? Потому что разумный потолок CPU-времени невозможно задать универсально. Для интерактивного кластера 10 ядро-минут — уже подозрительно много. Для batch-кластера, который ночами пережёвывает терабайты, 5000 ядро-минут на один ETL-запрос — норма. Дефолт «бесконечность» означает: Trino не угадывает за тебя, ограничение CPU-времени — осознанное решение администратора под конкретный кластер.
# etc/config.properties (координатор)
# Потолок суммарного CPU-времени одного запроса по кластеру.
# Дефолт практически бесконечен — задаётся осознанно под кластер.
# Пример для интерактивного кластера:
query.max-cpu-time=2h
Здесь 2h — это два часа CPU-времени, а не настенного. На кластере с сотнями ядер такой запрос исчерпает лимит за считанные минуты настенного времени, если действительно грузит процессор на полную.
Превышение query.max-cpu-time -> запрос убивается с ошибкой EXCEEDED_CPU_LIMIT. Текст ошибки прямо называет лимит:
Query 20260520_143012_00031_b7m2q failed:
Query exceeded maximum CPU time limit of 2.00h
[EXCEEDED_CPU_LIMIT]
CPU-лимит существует на двух уровнях. query.max-cpu-time — глобальный потолок для всех запросов кластера. А внутри resource groups (урок 5) есть softCpuLimit и hardCpuLimit — лимиты CPU-времени для конкретной группы за период. Глобальный лимит ловит совсем уж аномальные запросы; групповые лимиты дают тонкую настройку под нагрузки. Их можно и нужно использовать вместе.
Тайм-ауты: ограничение по настенному времени
CPU-лимит ловит прожорливость. Но запрос может вредить и иначе — просто слишком долго жить: висеть полдня, занимая слот resource group, держа память, мешая планированию. Против этого — тайм-ауты по настенному времени.
Ключевые тайм-ауты Trino:
| Свойство | Что ограничивает |
|---|---|
query.max-run-time | Полную продолжительность запроса от submit до завершения (настенное время) |
query.max-execution-time | Время именно фазы исполнения (без учёта ожидания клиента за результатом) |
query.client.timeout | Сколько запрос живёт, не получая обращений от клиента, прежде чем будет прерван |
query.min-expire-age | Сколько информация о завершённом запросе хранится для UI/API |
Разберём смысл главных.
query.max-run-time — общий потолок жизни запроса. Запрос, идущий дольше, убивается, как бы он ни был занят. По умолчанию величина большая (порядка суток) — Trino не обрывает долгие легитимные batch-запросы без явного указания.
query.max-execution-time — тоньше. Жизнь запроса включает не только исполнение, но и ожидание, пока клиент заберёт результат. Trino отдаёт результат страницами: клиент опрашивает nextUri и забирает страницы. Если клиент медленный или приостановился, запрос формально «жив», но не исполняется — он ждёт клиента. query.max-execution-time ограничивает именно исполнительную часть, не наказывая запрос за медлительность клиента.
query.client.timeout решает проблему брошенных запросов. Клиент Trino обязан периодически опрашивать координатор (poll по nextUri). Если клиент исчез — упал, потерял сеть, пользователь закрыл ноутбук, — запрос остался бы висеть, занимая ресурсы зря. query.client.timeout задаёт, как долго запрос терпит молчание клиента: не дождавшись обращений за этот срок, координатор прерывает запрос и освобождает ресурсы.
Зачем тайм-аутов так много
Может показаться избыточным иметь четыре разных тайм-аута. Но каждый закрывает свой класс проблем, и подменять их друг другом нельзя.
| Проблема | Какой механизм её ловит |
|---|---|
| Запрос сжигает слишком много процессора | query.max-cpu-time (CPU-время) |
| Запрос в принципе живёт слишком долго | query.max-run-time (настенное время, вся жизнь) |
| Запрос слишком долго именно считается | query.max-execution-time (настенное время, только исполнение) |
| Клиент исчез, запрос-сирота висит | query.client.timeout (молчание клиента) |
| Незавершённая инфо о запросах копится в памяти координатора | query.min-expire-age (срок хранения метаданных) |
Разные оси: CPU-время против настенного, вся жизнь против фазы исполнения, активность против молчания клиента. Один тайм-аут все эти случаи не покроет — потому их и несколько.
Точные имена и дефолты тайм-аутов между релизами Trino меняются — список свойств в admin/properties для версии 481 не обязан совпадать с более старыми гайдами. Перед настройкой продакшен-кластера всегда сверяй имена и значения со страницей свойств той версии, которую разворачиваешь, а не полагайся на статью трёхлетней давности.
Как это сочетается с остальной защитой ресурсов
Соберём всю картину управления ресурсами из модуля воедино. У Trino получается несколько слоёв, и каждый закрывает свою ось:
- Лимиты памяти (
query.max-memoryи соседи) — сколько данных запрос держит в RAM. - Spilling — аварийный клапан: при нехватке RAM выгрузить состояние на диск.
- Low-memory killer — последний предохранитель: разорвать памятный дедлок, убив одну жертву.
- Resource groups — изоляция нагрузок: поделить слоты, память и CPU между категориями запросов.
- CPU-лимиты (
query.max-cpu-time,softCpuLimit/hardCpuLimitгрупп) — сколько процессора запрос вправе сжечь. - Тайм-ауты — сколько по настенному времени запрос вправе жить и ждать.
Память, процессор, время, изоляция — четыре независимые оси. Грамотно настроенный кластер использует все слои: тогда ни один отдельный запрос — будь он прожорлив по памяти, по CPU или просто бесконечно долгий — не способен деградировать кластер для остальных.
Попробуй сам
Поэкспериментируй с CPU-лимитом и тайм-аутами на тестовом Trino.
- Задай в
etc/config.propertiesмаленькийquery.max-cpu-time, например30s, перезапусти Trino. Запусти заведомо тяжёлый по CPU запрос — крупное соединение без условия (фактически cross join) поtpch.sf100— и поймай ошибкуEXCEEDED_CPU_LIMIT. Сравни в Web UI настенное время этого запроса и его CPU-время: они будут сильно различаться. - Задай небольшой
query.max-run-timeи запусти долгий запрос — посмотри, как он обрывается по настенному времени, даже будучи занятым. - Поразмышляй: у тебя интерактивный дашбордный кластер и отдельный ночной batch-кластер. Какие значения
query.max-cpu-timeи тайм-аутов ты задашь каждому и почему они должны различаться?
Цель — закрепить, что CPU-время и настенное время — разные величины, и подобрать осмысленные лимиты под профиль кластера.