Learning Platform
Глоссарий Troubleshooting
Урок 13.05 · 24 мин
Средний
resource-groupsconfigurationoperationsisolation

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 vs с resource groups
Без групп: общий котёлВсе запросы в одной очереди, конкурируют без разбора. Тяжёлый ETL занимает все слоты, лёгкие дашборды ждут за ним
вводим resource groups
С группами: разделённая мощностьКаждая нагрузка в своей группе со своими лимитами слотов и памяти. ETL не залезет в долю дашбордов даже при свободном кластере

Дерево resource groups

Resource groups образуют дерево. Корень — весь кластер; внутренние узлы делят мощность дальше; листья — группы, в которые реально попадают запросы.

ClickHouse: query complexity и workload management У каждой группы свои лимиты, и дочерние группы делят ресурс родителя.

Типичная иерархия:

Дерево resource groups
global (корень)Корневая группа — весь кластер. Задаёт глобальный потолок: суммарно сколько запросов и памяти разрешено
etlВнутренний узел для batch-нагрузки. Получает свою долю кластера, делит её между дочерними группами
adhocВнутренний узел для интерактивной нагрузки: дашборды и ad-hoc аналитика. Своя доля кластера
adhoc.dashboardsЛист: дашборды. Высокий приоритет, малая память на запрос, много мелких параллельных запросов
adhoc.analystsЛист: ad-hoc запросы аналитиков. Средний приоритет, средние лимиты

Запрос всегда попадает в листовую группу (adhoc.dashboards, etl). Внутренние группы (global, adhoc) запросы не принимают — они только агрегируют лимиты и распределяют мощность ниже. Лимит дочерней группы не может превысить лимит родителя: adhoc.dashboards не получит больше слотов, чем выделено всей adhoc.


Лимиты группы: running, queued, память

Каждая группа задаёт несколько лимитов, и понимать разницу между ними критично.

Лимит группыЧто ограничивает
hardConcurrencyLimitСколько запросов группы исполняется одновременно (running)
maxQueuedСколько запросов группы может стоять в очереди сверх исполняемых
softMemoryLimitПорог памяти группы, после которого новые запросы группы начинают ставиться в очередь, а не запускаться
softCpuLimit / hardCpuLimitЛимиты CPU-времени группы за период (опционально)

Логика работы листовой группы при поступлении запроса:

  1. Число running-запросов группы меньше hardConcurrencyLimit и память группы ниже softMemoryLimit? -> запрос запускается немедленно.
  2. Слотов нет, но число queued-запросов меньше maxQueued? -> запрос встаёт в очередь и ждёт освобождения слота.
  3. И очередь полна (maxQueued достигнут)? -> запрос отклоняется сразу с ошибкой — кластер явно говорит «перегружен», вместо того чтобы копить бесконечную очередь.
NOTE

Очередь — это фича, а не баг. Когда дашбордная группа заполнила свои слоты, лучше подержать новый запрос несколько секунд в очереди, чем запустить его и замедлить все уже идущие. А maxQueued не даёт очереди расти бесконечно: при настоящей перегрузке запрос отклоняется быстро и честно, а не висит десять минут с растущей задержкой.


Распределение мощности: weight и приоритеты

Когда у внутренней группы несколько дочерних, нужно решить, кому отдать освободившийся слот. За это отвечает свойство schedulingPolicy группы и веса дочерних групп.

  • fair (дефолт) — слоты раздаются дочерним группам по очереди, поровну.
  • weighted — у каждой дочерней группы есть schedulingWeight; слоты раздаются пропорционально весам. Группа с весом 9 получит примерно в 9 раз больше слотов, чем группа с весом 1.
  • weighted_fair — взвешенный вариант с учётом текущей загрузки групп.
  • query_priority — учитывается приоритет, заданный в запросе сессией.

weighted — рабочая лошадка изоляции. Хочешь, чтобы дашборды почти всегда обгоняли ETL, — даёшь дашбордной ветке вес 9, а ETL вес 1: при конкуренции ETL получит лишь около 10% спорных слотов.

weighted: слоты пропорциональны весам
adhoc — weight 9При schedulingPolicy=weighted получает примерно 9/10 спорных слотов родителя. Интерактив почти всегда обгоняет batch
делят слоты родителя
etl — weight 1Получает примерно 1/10 спорных слотов. Batch не задушит интерактив, но и не остановится

Селекторы: как запрос попадает в группу

Дерево групп описывает «куда можно попасть». Селекторы описывают «кто куда попадает». Селектор — правило сопоставления: по атрибутам запроса (пользователь, 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. Последний селектор-без-условий — хорошая практика: гарантирует, что у любого запроса есть группа.

WARNING

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


Resource groups и память: как они связаны

Resource groups не заменяют лимиты памяти из уроков 2-3 — они работают поверх. query.max-memory по-прежнему ограничивает отдельный запрос. softMemoryLimit группы ограничивает сумму памяти всех запросов группы: когда etl-группа в сумме подошла к своему softMemoryLimit, новые ETL-запросы начинают ставиться в очередь, не давя на остальные группы.

Связка получается такой: resource groups делят кластер между нагрузками и не дают одной нагрузке задушить другую; лимиты памяти страхуют от запроса-обжоры внутри любой группы; low-memory killer остаётся последним предохранителем, если давление всё же случилось. Три механизма дополняют друг друга.


Попробуй сам

Настрой resource groups на тестовом Trino и увидь очереди.

  1. Создай etc/resource-groups.json с двумя листовыми группами — dashboards (большой hardConcurrencyLimit) и etl (маленький, например 2). Пропиши селекторы по source или по пользователю. Подключи через etc/resource-groups.properties, перезапусти Trino.
  2. Запусти много запросов, помеченных как ETL (trino --source etl ... или через свойство клиента), больше, чем hardConcurrencyLimit ETL-группы. В Web UI часть запросов окажется в состоянии QUEUED.
  3. Одновременно с забитой ETL-группой запусти дашбордный запрос (--source dashboard-bi) и убедись, что он стартует сразу — у его группы свои свободные слоты, очередь ETL ему не мешает.
  4. Поменяй schedulingPolicy родителя на weighted, задай дочерним группам разные schedulingWeight и понаблюдай, как меняется распределение слотов при конкуренции.

Цель — увидеть, что resource groups реально изолируют нагрузки: забитая ETL-очередь не задевает дашборды.


Проверка знанийKnowledge check
Чем отличается hardConcurrencyLimit от maxQueued, и что произойдёт с новым запросом группы, когда оба лимита достигнуты?
ОтветAnswer
hardConcurrencyLimit ограничивает число запросов группы, исполняемых ОДНОВРЕМЕННО (running) — это размер «активных слотов» группы. maxQueued ограничивает число запросов, стоящих В ОЧЕРЕДИ сверх исполняемых, — это размер буфера ожидания. Когда поступает новый запрос группы, Trino действует по ступеням: если число running меньше hardConcurrencyLimit и память группы ниже softMemoryLimit — запрос запускается немедленно; если активные слоты заняты, но число queued меньше maxQueued — запрос встаёт в очередь и ждёт освобождения слота; если же достигнуты ОБА лимита — все слоты заняты и очередь полна — запрос отклоняется сразу с ошибкой. Это сделано осознанно: очередь сглаживает временные всплески, подержав запрос несколько секунд вместо того чтобы замедлять все идущие; но maxQueued не даёт очереди расти бесконечно — при настоящей устойчивой перегрузке кластер честно и быстро говорит «перегружен» через отказ, а не накапливает очередь на десять минут с непрерывно растущей задержкой ответа.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какую задачу решают resource groups, которую лимиты памяти отдельного запроса решить не могут?

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

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

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

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