ЯдроКодаподготовка к экзаменам
Научная библиотека

Загружаем научный разбор

Подготавливаем текст, источники и редакционные примечания без изменения разметки страницы.

Каталог статейМатериал и источники

Autothrottle: почему лимит CPU нужно связывать с задержкой приложения

Автор: · Обновлено

Два уровня управления CPU в Autothrottle, правильное чтение throttling и собственная модель выбора ресурсов при ограничении сквозной задержки.

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

Возникает задача управления: сколько CPU выделить каждому сервису, чтобы соблюдать цель по сквозной задержке и не держать лишний запас? Исследование Autothrottle интересно разделением этой задачи на два уровня. Разберём его постановку, затем решим собственную маленькую задачу распределения и выясним, какие предположения делают её намного проще реальной системы.

Что предложили авторы

В работе Wang и соавторов, NSDI 2024 верхний контроллер Tower использует обратную связь по задержке приложения и задаёт целевые показатели локальным контроллерам Captain. Последние регулируют CPU сервисов. Связующим показателем служит CPU throttle ratio. В реализации статьи его рассчитывают как прирост числа периодов CFS с исчерпанием квоты, делённый на число прошедших периодов; это не доля занятого CPU и не доля времени ожидания.

Оценка охватывает три микросервисных приложения, различные профили входной нагрузки и сравнения при заданных SLO для P99. Снижение выделенных ресурсов в этих экспериментах не обещает такого же выигрыша другой системе. Существенная деталь реализации: начальная исследовательская фаза обучаемого контроллера допускает нарушения SLO. Это необходимо учитывать при переносе подхода в эксплуатацию. Статья и окончательная версия PDF.

Что именно ограничивает квота

CPU-квота задаёт доступный объём процессорного времени в периоде. Если работа исчерпала этот объём раньше конца периода, ей может потребоваться ждать следующего. Среднее потребление за длинный интервал при этом способно выглядеть умеренным: короткий интенсивный всплеск и последующий простой усредняются. Параметры CPU в Docker описаны в официальной документации по ресурсным ограничениям.

Для диагностики полезно различать выделенный лимит, фактически использованное время и признаки ограничения. Если сервису дали больше CPU, но он ждёт базу данных, потребление не обязано вырасти. Если лимит слишком мал, наблюдаемое потребление может отражать потолок, а не истинную потребность. Наконец, высокий показатель throttling сам по себе не доказывает, что пользовательский SLO нарушен: значение зависит от роли сервиса и характера работы.

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

Собственная модель двух последовательных сервисов

Пусть запрос проходит сервис A, затем сервис B, после чего тратит ещё 10 мс на фиксированную работу. Вымышленная CPU-зависимая часть равна 20/cpu для A и 60/cpu для B. Такое обратное масштабирование — учебное допущение, а не закон микросервисов. Переберём три допустимых выделения и найдём самое дешёвое, которое укладывается в 120 мс. Код работает на Python 3 без внешних пакетов.

from itertools import product

choices = (0.5, 1.0, 2.0)
slo_ms = 120

def latency_ms(cpu_a, cpu_b):
    return 20 / cpu_a + 60 / cpu_b + 10

feasible = [(a + b, latency_ms(a, b), a, b)
            for a, b in product(choices, repeat=2)
            if latency_ms(a, b) <= slo_ms]
best = min(feasible)
uniform = min((2 * c, latency_ms(c, c), c, c)
              for c in choices if latency_ms(c, c) <= slo_ms)
assert best == (1.5, 110.0, 0.5, 1.0)
assert uniform == (2.0, 90.0, 1.0, 1.0)
assert latency_ms(1.0, 0.5) > slo_ms

before = {'periods': 100, 'throttled': 20}
after = {'periods': 110, 'throttled': 24}
ratio = ((after['throttled'] - before['throttled'])
         / (after['periods'] - before['periods']))
assert ratio == 0.4
print('best:', best, 'uniform:', uniform, 'period-ratio:', ratio)

Модель выбирает 0,5 CPU для A и 1 CPU для B, получая 110 мс при сумме 1,5 CPU. Одинаковые выделения обоим сервисам требуют в выбранной сетке 2 CPU и дают 90 мс. Обратное распределение той же суммы — 1 CPU для A и 0,5 для B — нарушает ограничение. Значит, имеет значение не только общий бюджет, но и его положение в цепочке.

Этот результат не воспроизводит Autothrottle: здесь нет обучения, контроллеров, очередей, квот Linux и реальных запросов. Мы знаем функцию задержки заранее и рассматриваем всего девять вариантов. Именно эти удобства позволяют найти точный ответ. В живой системе зависимость приходится оценивать по неполному и меняющемуся наблюдению.

Последняя часть программы показывает правильную арифметику счётчиков: четыре затронутых периода из десяти дают 0,4. Деление накопленных 24 на 110 ответило бы на другой вопрос — о всей истории с момента начала счёта. А число 0,4 не означает, что процесс 40% времени был остановлен. Длительность остановки внутри каждого периода может различаться. При перезапуске и сбросе счётчиков сначала проверяют непрерывность наблюдения, иначе разность станет бессмысленной.

Почему локальное улучшение не гарантирует глобальное

В примере B получает больше CPU, поскольку именно там дороже нехватка ресурса. В реальном приложении путь запроса может ветвиться. Одна ветка ждёт две независимые операции, другая выполняется только для части пользователей. При параллельных вызовах время определяется ожиданием завершения нужных ветвей, а не простым сложением средних задержек всех сервисов.

Похожая ловушка касается процентилей. P99 приложения нельзя в общем случае получить суммой P99 компонентов: медленные события могут происходить на разных запросах. Для пользовательского SLO нужна сквозная метрика с определённым началом, концом, окном и набором запросов. Локальные показатели помогают объяснять её, но не заменяют само измерение.

Рассмотрим отдельную гипотезу: задержка выросла из-за лимита CPU. Проверяем её изменением одного выделения на контролируемой нагрузке и наблюдением за throttling, очередью, сквозным временем и ошибками. Если throttling исчез, а задержка сохранилась, эта причина была недостаточной. Следующий кандидат может быть в блокировке или внешней зависимости. Постоянно увеличивать все лимиты без таких проверок дорого и малоинформативно.

Обратная связь тоже имеет задержку

Контроллер получает измерение за прошедшее окно. Пока оно собирается, нагрузка может уже измениться. После изменения CPU требуется время, чтобы обслужить очередь и увидеть эффект. Если принимать новые решения слишком быстро, можно несколько раз исправлять уже исчезнувшую проблему; если слишком медленно — долго держать очередь или лишний запас.

Для собственного пробного контроллера поэтому нужен явный шаг изменения, минимальный интервал наблюдения и способ отката. Стоимость ошибок асимметрична: случайное увеличение выделения расходует ресурс, а слишком сильное уменьшение способно нарушить пользовательскую цель. Эта асимметрия должна присутствовать в метрике качества эксперимента, а не только в описании намерений.

Отдельно проверяется взаимодействие с другими механизмами. Если один компонент меняет число реплик, а другой одновременно уменьшает CPU каждой реплике, наблюдаемый эффект смешивается. Для исследования сначала фиксируют остальные управляющие воздействия. Затем намеренно включают их совместную работу и ищут колебания, задержанные реакции и изменение распределения запросов.

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

Сначала определите SLO: например, конкретный процентиль времени ответа для определённого маршрута за заданное окно. Затем воспроизведите несколько профилей нагрузки: плавное изменение, краткий всплеск и устойчивый высокий поток. Одинаковые средние RPS не делают эти профили эквивалентными, поскольку очередь зависит от распределения поступлений во времени.

В результат включайте выделенные CPU во времени, фактическое потребление, нарушения SLO, долю ошибок и время восстановления после всплеска. Если измеряется экономия в ядро-секундах, сравнивайте одинаковую выполненную работу. Период обучения и неудачные попытки нельзя молча удалить из отчёта: они принадлежат стоимости метода. Запишите, какие знания о нагрузке получили baseline и новый контроллер до начала сравнения.

Упражнение с изменением ограничения

Уменьшите slo_ms до 80 и снова найдите лучший вариант. Затем добавьте фиксированную сетевую задержку 100 мс вместо 10. Для части целей допустимых вариантов не останется; обработайте этот случай явным сообщением. Объясните, почему увеличение CPU не может устранить нижнюю границу, созданную сетью. Следующий шаг — добавить в модель очередь и сравнить одинаковое среднее число запросов при равномерном и пакетном поступлении.

Прикладное продолжение — [трек Docker](/lessons/without-university/docker-containerization) и [урок Linux о планировщике и лимитах CPU](/lessons/without-university/linux-developer-foundations/linux-developer-26). После этого разбор Autothrottle можно читать как исследование связи между двумя наблюдаемыми уровнями: ресурсами процессов и опытом пользователя. Выбор лимита становится проверяемым решением по конкретному SLO, а не попыткой подобрать одинаковый красивый процент загрузки для всех контейнеров.

Источники

Формат и права

Формат
Авторский разбор

Атрибуция

Самостоятельный русскоязычный разбор ЯдроКода по указанным первичным источникам. Учебные модели, примеры и выводы редакции отделены от результатов исследований. Материал не является переводом или перепечаткой.

Код, данные и иллюстрации

Текст, учебные данные и Python-примеры созданы для этой публикации. Чужие программные реализации, таблицы, схемы и иллюстрации не воспроизводятся.