Resource groups: иерархия, очереди, приоритеты, изоляция нагрузки
Лимиты памяти и spill из прошлых уроков работают на уровне отдельного запроса: они не дают одному запросу перегрузить кластер. Но есть проблема, которую они не решают — конкуренция разных нагрузок. Дашборды требуют ответа за секунды. Тяжёлый ночной ETL крутится часами. ad-hoc запросы аналитиков непредсказуемы. Если все они летят в кластер вперемешку, один тяжёлый ETL легко забьёт все слоты и заставит дашборды стоять в очереди — притом каждый запрос по отдельности в своих лимитах.
Resource groups — механизм Trino для решения именно этой задачи: разделить мощность кластера между категориями нагрузки, задать каждой свои лимиты и приоритеты, изолировать их друг от друга. Разберём дерево групп, лимиты, очереди и то, как запрос попадает в нужную группу.
Зачем нужна изоляция нагрузки
Без resource groups кластер — общий котёл: запросы исполняются по мере поступления, конкурируя за слоты и память без разбора. Это плохо предсказуемо.
Сценарий: в 02:00 стартует ночной ETL — двадцать тяжёлых запросов. В 02:05 аналитик из другого часового пояса открывает дашборд. Его лёгкий запрос встаёт в общую очередь за двадцатью тяжёлыми ETL-запросами и ждёт. Дашборд «висит». Формально всё корректно — просwho first, served first, — но бизнес-результат плохой: интерактивная нагрузка задушена batch-нагрузкой.
Resource groups вводят категории нагрузки и гарантируют каждой её долю. ETL получает свой кусок кластера, дашборды — свой; ETL не может занять больше своего куска, даже если кластер наполовину пуст в этот момент — слоты дашбордов остаются дашбордам.
Дерево resource groups
Resource groups образуют дерево. Корень — весь кластер; внутренние узлы делят мощность дальше; листья — группы, в которые реально попадают запросы.
ClickHouse: query complexity и workload management У каждой группы свои лимиты, и дочерние группы делят ресурс родителя.
Типичная иерархия:
Запрос всегда попадает в листовую группу (adhoc.dashboards, etl). Внутренние группы (global, adhoc) запросы не принимают — они только агрегируют лимиты и распределяют мощность ниже. Лимит дочерней группы не может превысить лимит родителя: adhoc.dashboards не получит больше слотов, чем выделено всей adhoc.
Лимиты группы: running, queued, память
Каждая группа задаёт несколько лимитов, и понимать разницу между ними критично.
| Лимит группы | Что ограничивает |
|---|---|
hardConcurrencyLimit | Сколько запросов группы исполняется одновременно (running) |
maxQueued | Сколько запросов группы может стоять в очереди сверх исполняемых |
softMemoryLimit | Порог памяти группы, после которого новые запросы группы начинают ставиться в очередь, а не запускаться |
softCpuLimit / hardCpuLimit | Лимиты CPU-времени группы за период (опционально) |
Логика работы листовой группы при поступлении запроса:
- Число running-запросов группы меньше
hardConcurrencyLimitи память группы нижеsoftMemoryLimit? -> запрос запускается немедленно. - Слотов нет, но число queued-запросов меньше
maxQueued? -> запрос встаёт в очередь и ждёт освобождения слота. - И очередь полна (
maxQueuedдостигнут)? -> запрос отклоняется сразу с ошибкой — кластер явно говорит «перегружен», вместо того чтобы копить бесконечную очередь.
Очередь — это фича, а не баг. Когда дашбордная группа заполнила свои слоты, лучше подержать новый запрос несколько секунд в очереди, чем запустить его и замедлить все уже идущие. А maxQueued не даёт очереди расти бесконечно: при настоящей перегрузке запрос отклоняется быстро и честно, а не висит десять минут с растущей задержкой.
Распределение мощности: weight и приоритеты
Когда у внутренней группы несколько дочерних, нужно решить, кому отдать освободившийся слот. За это отвечает свойство schedulingPolicy группы и веса дочерних групп.
fair(дефолт) — слоты раздаются дочерним группам по очереди, поровну.weighted— у каждой дочерней группы естьschedulingWeight; слоты раздаются пропорционально весам. Группа с весом 9 получит примерно в 9 раз больше слотов, чем группа с весом 1.weighted_fair— взвешенный вариант с учётом текущей загрузки групп.query_priority— учитывается приоритет, заданный в запросе сессией.
weighted — рабочая лошадка изоляции. Хочешь, чтобы дашборды почти всегда обгоняли ETL, — даёшь дашбордной ветке вес 9, а ETL вес 1: при конкуренции ETL получит лишь около 10% спорных слотов.
Селекторы: как запрос попадает в группу
Дерево групп описывает «куда можно попасть». Селекторы описывают «кто куда попадает». Селектор — правило сопоставления: по атрибутам запроса (пользователь, source, клиентские теги, тип запроса) он выбирает целевую группу.
Конфигурация resource groups — JSON-файл (file-based resource group manager); альтернатива — database-based, когда правила хранятся в БД. Минимальный file-based пример:
{
"rootGroups": [
{
"name": "global",
"softMemoryLimit": "80%",
"hardConcurrencyLimit": 100,
"maxQueued": 1000,
"schedulingPolicy": "weighted",
"subGroups": [
{
"name": "etl",
"softMemoryLimit": "40%",
"hardConcurrencyLimit": 10,
"maxQueued": 200,
"schedulingWeight": 1
},
{
"name": "adhoc",
"softMemoryLimit": "60%",
"hardConcurrencyLimit": 50,
"maxQueued": 500,
"schedulingWeight": 9,
"schedulingPolicy": "weighted",
"subGroups": [
{
"name": "dashboards",
"softMemoryLimit": "20%",
"hardConcurrencyLimit": 40,
"maxQueued": 400,
"schedulingWeight": 8
},
{
"name": "analysts",
"softMemoryLimit": "40%",
"hardConcurrencyLimit": 20,
"maxQueued": 200,
"schedulingWeight": 2
}
]
}
]
}
],
"selectors": [
{ "user": "etl_service", "group": "global.etl" },
{ "source": "dashboard-bi", "group": "global.adhoc.dashboards" },
{ "group": "global.adhoc.analysts" }
]
}
Подключается через etc/resource-groups.properties:
resource-groups.configuration-manager=file
resource-groups.config-file=etc/resource-groups.json
Селекторы проверяются сверху вниз, побеждает первый совпавший. В примере выше: запросы пользователя etl_service уходят в global.etl; запросы с source=dashboard-bi — в global.adhoc.dashboards; всё остальное ловит последний селектор без условий и направляет в global.adhoc.analysts. Последний селектор-без-условий — хорошая практика: гарантирует, что у любого запроса есть группа.
Порядок селекторов значим. Если поставить общий селектор без условий первым, он перехватит вообще все запросы, и специфичные правила ниже никогда не сработают. Самые узкие правила — наверх, самое общее — в самый низ.
Resource groups и память: как они связаны
Resource groups не заменяют лимиты памяти из уроков 2-3 — они работают поверх. query.max-memory по-прежнему ограничивает отдельный запрос. softMemoryLimit группы ограничивает сумму памяти всех запросов группы: когда etl-группа в сумме подошла к своему softMemoryLimit, новые ETL-запросы начинают ставиться в очередь, не давя на остальные группы.
Связка получается такой: resource groups делят кластер между нагрузками и не дают одной нагрузке задушить другую; лимиты памяти страхуют от запроса-обжоры внутри любой группы; low-memory killer остаётся последним предохранителем, если давление всё же случилось. Три механизма дополняют друг друга.
Попробуй сам
Настрой resource groups на тестовом Trino и увидь очереди.
- Создай
etc/resource-groups.jsonс двумя листовыми группами —dashboards(большойhardConcurrencyLimit) иetl(маленький, например 2). Пропиши селекторы поsourceили по пользователю. Подключи черезetc/resource-groups.properties, перезапусти Trino. - Запусти много запросов, помеченных как ETL (
trino --source etl ...или через свойство клиента), больше, чемhardConcurrencyLimitETL-группы. В Web UI часть запросов окажется в состоянии QUEUED. - Одновременно с забитой ETL-группой запусти дашбордный запрос (
--source dashboard-bi) и убедись, что он стартует сразу — у его группы свои свободные слоты, очередь ETL ему не мешает. - Поменяй
schedulingPolicyродителя наweighted, задай дочерним группам разныеschedulingWeightи понаблюдай, как меняется распределение слотов при конкуренции.
Цель — увидеть, что resource groups реально изолируют нагрузки: забитая ETL-очередь не задевает дашборды.