Docker тормозит при свободном CPU: проверяем квоту контейнера

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

Посмотрите, какой бюджет получил процесс

На Linux с cgroup v2 лимит времени задаёт файл cpu.max. Два числа обозначают квоту и период в микросекундах. Например, 50000 100000 даёт группе 50 мс процессорного времени каждые 100 мс. Это бюджет примерно половины одного CPU при длительной нагрузке. Несколько потоков могут расходовать его одновременно на разных ядрах. Ограничение числа ядер и ограничение времени — разные механизмы, и в этом примере важен второй.

На Docker Engine сначала прочитайте сведения о контейнере. Замените report-api его именем. Команды ниже ничего не меняют.

docker info --format 'cgroup={{.CgroupVersion}}'
docker inspect --format 'pid={{.State.Pid}} nano={{.HostConfig.NanoCpus}} quota={{.HostConfig.CpuQuota}} period={{.HostConfig.CpuPeriod}}' report-api

NanoCpus может отражать настройку через --cpus, а CpuQuota и CpuPeriod — явные параметры квоты. Для фактического ограничения смотрите файлы ядра. Следующий пример рассчитан на Linux-хост, где cgroup v2 смонтирован в /sys/fs/cgroup. Выполняйте его в пространстве имён хоста, чтобы путь процесса совпал с деревом cgroup.

pid=$(docker inspect --format '{{.State.Pid}}' report-api)
if [ "$pid" -gt 0 ]; then
  relative=$(sed -n 's/^0:://p' "/proc/$pid/cgroup")
  if [ -n "$relative" ] && [ "$relative" != / ]; then
    cg="/sys/fs/cgroup$relative"
    printf 'cgroup: %s\n' "$cg"
    cat "$cg/cpu.max"
    grep -E '^(usage_usec|nr_periods|nr_throttled|throttled_usec) ' "$cg/cpu.stat"
  fi
fi

Если в cpu.max первым стоит max, в этой группе собственная квота не задана. Проверьте также родительские каталоги: ограничение может действовать выше, например для целого набора сервисов. Если контейнер успели пересоздать, найдите новый PID и снимите обе точки заново.

Сопоставьте задержку с приращением счётчиков

Один снимок cpu.stat показывает накопленную историю. Снимите его перед серией запросов и сразу после неё, запишите длительность интервала и задержки. Для примера представим, что nr_periods вырос со 180 до 200, а nr_throttled — с 22 до 34. В этом окне счётчик ограниченных периодов прибавил 12 при 20 учтённых периодах. Это 60% по отношению приращений, а не 60% потерянного времени. throttled_usec отдельно показывает накопленное время ограничения в микросекундах. Привязывайте прирост к тому же окну, где выросла задержка.

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

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

Откройте программу курса Docker и Docker Compose. В ней есть ограничения CPU и памяти, а в локальной практике вы будете сверять конфигурацию с состоянием контейнера и разбирать сбои приложения. С основами запуска поможет открытый урок о первом контейнере.

Продолжить на практике

  • Docker и Docker Compose: контейнеризация приложений ↗

    Практический курс Docker для разработчиков: упакуйте своё приложение, подключите PostgreSQL и запускайте окружение через Compose. Научитесь находить поломки, обновлять сервис и восстанавливать данные. 6 Dockerfile-задач с автопроверкой и 42 локальные лабораторные.