Три недели ловили тормоза, а мы просто стояли в очереди за чужим CPU
Три недели мы гонялись за призраком: API периодически отвечал не за 50-80 мс, а за секунду-две, без всякой видимой причины. Профайлер приложения молчал, база не жаловалась на медленные запросы, а top внутри VPS показывал вполне мирную загрузку. Разгадка нашлась не в коде и не в конфиге сервиса, а в одной колонке вывода vmstat, на которую до этого никто не смотрел — %st, steal time. Сервер не тормозил сам — он стоял в очереди за физическим CPU, который гипервизор в этот момент отдавал соседям по хосту.
Содержание
- Три недели случайных тормозов без видимой причины
- Что проверили в первую очередь — и что оказалось ни при чём
- Зацепка, которую все пролистывали: колонка %st
- sar и mpstat: ловим окна воровства CPU по часам
- Почему это «чужой» CPU: как overselling выглядит изнутри гостя
- Что изменили: выделенные ресурсы и постоянный мониторинг steal time
Три недели случайных тормозов без видимой причины
История началась с жалоб от пользователей: часть запросов к API изредка «подвисала». Не все и не постоянно — где-то одна заявка из полусотни вдруг растягивалась на 700-1500 мс вместо обычных 50-80. Паттерна по времени суток или дню недели поначалу не просматривалось: могло стрельнуть в 11 утра, могло в три ночи, могло два часа подряд не быть вообще ни одного всплеска.
Первая реакция была стандартной — посмотреть на графики самого приложения. Grafana с метриками из Prometheus показывала: CPU процесса — 20-35%, память стабильна, количество активных соединений с БД в пределах нормы, очередь фоновых задач не растёт. Ничего похожего на перегрузку. При этом трейсы (мы используем OpenTelemetry) в момент инцидента показывали аномалию не внутри обработки запроса, а до неё — время между «сокет принят» и «первая строка кода обработчика начала выполняться» иногда вырастало в 10-20 раз против обычного. То есть тормозило что-то до того, как наш код вообще начинал работать.
Это был первый настоящий след, но тогда мы его не распознали и потратили ещё полторы недели на проверку версий попроще.
Что проверили в первую очередь — и что оказалось ни при чём
По порядку отбросили гипотезы, которые обычно и объясняют такие вещи:
- Паузы сборщика мусора. Сервис на Go, GC там относительно предсказуемый, но на всякий случай включили
GODEBUG=gctrace=1и сопоставили лог пауз с моментами инцидентов. Совпадений не нашли — паузы GC были в единицах миллисекунд, а не в секундах. - Блокировки в базе. Смотрели
pg_stat_activity, искали долгие транзакции и ожидания на локах во время всплесков задержки. База была чистой,wait_eventпустой у большинства сессий. - Диск.
iostat -x 1не показывал ни высокогоawait, ни насыщения очереди (%utilв норме). Диск явно был не при чём — узкое место было где-то ещё до операций ввода-вывода. - Сеть. Прогнали
mtrи повесили постоянныйpingс соседнего сервера — джиттер укладывался в обычные значения, потерь пакетов не было. - Память и своп.
free -mпоказывал достаточно свободной памяти, своп не использовался (swapon --show— пусто). Значит, не подкачка на диск в моменты пиков. - Пул потоков и исчерпание воркеров. Сняли несколько
goroutine dumpчерезpprofв момент всплеска — горутины не «висели» на блокирующих операциях, они просто не успевали выполниться. Это звучит как мелкая деталь, но на самом деле это и была подсказка: не «ждём чего-то», а «не дали времени выполниться».
Все проверки заняли добрую часть трёх недель, потому что инцидент был нерегулярным: приходилось ждать следующего всплеска, снимать снапшоты состояния и разбирать их постфактум. К концу второй недели стало ясно: внутри гостевой системы буквально нечего чинить — по всем метрикам приложение и ОС ведут себя штатно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЗацепка, которую все пролистывали: колонка %st
Ключевой момент случился, когда один из инженеров запустил vmstat 1 и оставил его открытым на весь рабочий день вместо разовой проверки:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 812340 65200 985600 0 0 2 8 310 480 8 3 87 0 2
2 0 0 808120 65200 985800 0 0 0 12 295 460 6 2 88 0 4
6 0 0 795400 65200 986100 0 0 4 20 340 610 9 4 46 0 41
5 0 0 793880 65200 986300 0 0 0 8 320 590 7 3 50 0 40
1 0 0 796200 65200 986400 0 0 0 10 300 470 8 2 88 0 2
Столбец st — это steal time: доля времени, когда виртуальный CPU был готов исполнять инструкции, но физическое ядро в этот момент отдали другой виртуальной машине на том же гипервизоре. В обычном top эта же цифра прячется в строке %Cpu(s) последним значением после wa, и по умолчанию на неё почти никто не смотрит — она нулевая 95% времени, поэтому глаз её игнорирует.
В нашем случае в спокойные периоды st действительно был в районе 1-4%, что для виртуализации нормально и никак не ощущается. Но в моменты жалоб пользователей он подскакивал до 35-45%, а id (простой CPU) одновременно проседал — то есть гостевая система не была загружена своей работой, она просто физически не получала процессорное время. Это объясняло сразу всё: почему приложение «спокойное» по своим метрикам, почему трейсы показывают задержку до входа в код, и почему GC/диск/сеть ни при чём — они и правда были в порядке, просто у процесса отбирали время до того, как он вообще мог что-либо сделать.
sar и mpstat: ловим окна воровства CPU по часам
Чтобы не сидеть с открытым терминалом, поставили sysstat и включили сбор истории:
apt install sysstat
# в /etc/default/sysstat: ENABLED="true"
systemctl enable --now sysstat
Дальше несколько дней просто смотрели накопленную статистику:
sar -u ALL -f /var/log/sysstat/saXX
и параллельно держали разбивку по ядрам:
mpstat -P ALL 1 60
Через несколько дней собранных данных вырисовалась картина: всплески %steal не были случайными в строгом смысле — они группировались в получасовые-часовые окна, которые повторялись нерегулярно, но заметно чаще в определённые интервалы суток. Это было похоже на поведение соседей по хосту, которые запускают у себя что-то тяжёлое (батчевые джобы, рендеринг, майнинг, бэкапы — со стороны не видно, что именно, гостевая система в принципе не может это знать). Наша нагрузка была ни при чём: она колебалась в обычных пределах и до, и во время, и после всплесков st.
Отдельно проверили важный нюанс: high steal time — это не то же самое, что троттлинг по cgroup-квоте, когда ваш собственный контейнер упирается в лимит cpu.cfs_quota_us и его насильно тормозит planировщик хоста по вашей же настройке. Там счётчик другой — throttled periods, а не steal. В нашем случае квот не было вообще, и лимит %st показывал именно чужую активность, а не наш собственный потолок.
Почему это «чужой» CPU: как overselling выглядит изнутри гостя
У большинства бюджетных VPS-тарифов виртуальные ядра — это не выделенные физические ядра, а доля времени на общем пуле CPU хост-сервера, распределяемая планировщиком гипервизора (у KVM это в конечном счёте cgroups + CFS на самом хосте, у других гипервизоров — свой шедулер). Если провайдер продаёт больше виртуальных vCPU, чем есть реальных потоков на хосте, — а такая перепродажа (overselling) в бюджетном сегменте вполне обычная бизнес-модель, — в моменты, когда несколько арендаторов одновременно требуют CPU, гипервизор физически не может выдать всем сразу. Кто-то ждёт в очереди. Гостевая ОС в этот момент видит именно steal time: «я готов работать, но мне не дали ядро».
Важно: это не поломка и не обязательно недобросовестность провайдера — умеренный оверселлинг допустим, потому что рабочие нагрузки редко утилизируют CPU на 100% одновременно. Проблема начинается, когда коэффициент перепродажи слишком высокий или на хосте оказался один сосед с постоянной тяжёлой нагрузкой. Для арендатора это выглядит как необъяснимые тормоза, потому что метрики внутри гостя показывают лишь то, что видно изнутри виртуальной машины, — а конкуренция происходит уровнем ниже, на хосте, куда у гостя нет доступа.
Что изменили: выделенные ресурсы и постоянный мониторинг steal time
По итогам разбора сделали три вещи.
Во-первых, добавили node_exporter c метрикой node_cpu_seconds_total{mode="steal"} в постоянный мониторинг и завели алерт в Prometheus/Alertmanager, чтобы не полагаться на память инженера, который однажды догадался открыть vmstat:
- alert: HighCPUSteal
expr: rate(node_cpu_seconds_total{mode="steal"}[5m]) > 0.15
for: 10m
labels:
severity: warning
annotations:
summary: "Высокий steal time на {{ $labels.instance }}"
Порог 15% — не универсальная константа, это ориентир, который сработал для нашего профиля нагрузки; для сервиса с более жёсткими требованиями к задержке имеет смысл ставить порог ниже и смотреть не только на среднее, но и на всплески.
Во-вторых, для этого конкретного сервиса, где задержка напрямую бьёт по деньгам, переехали на тариф с гарантированными, а не разделяемыми ядрами — по сути, ушли от «виртуального CPU из общего пула» к конфигурации, где ядра закреплены за инстансом и не участвуют в чужом планировании. Для кого-то разумнее шагнуть ещё дальше и взять выделенный сервер вместо VPS — там steal time как класс отсутствует, потому что CPU физически не делится с чужими виртуалками.
В-третьих, договорились с командой о простом правиле на будущее: если задержка «плавает» без корреляции с собственными метриками CPU/памяти/диска приложения — первым делом смотреть %st, а не гоняться за экзотическими гипотезами в коде. Это тот случай, когда дешёвая пятиминутная проверка могла сэкономить три недели разбирательств, если бы мы знали, куда смотреть с самого начала.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Чем steal time отличается от обычной загрузки CPU?
Обычная загрузка (us/sy в top) — это время, которое ваш процесс реально работал на ядре. Steal time (st) — это время, когда виртуальный CPU был готов работать, но физическое ядро в этот момент занял гипервизор для другой задачи или соседней ВМ. Высокий st при низком us/sy — верный признак конкуренции за CPU на хосте, а не проблемы в вашем приложении.
Можно ли увидеть steal time без установки дополнительных пакетов?
Да, top показывает его в строке %Cpu(s) последним значением после wa без всякой установки, vmstat 1 — в колонке st. Просто по умолчанию никто на них не смотрит, пока не столкнётся с похожим инцидентом.
Небольшой steal time — это всегда плохо?
Нет. Пара процентов st время от времени — нормальное явление почти в любой виртуализированной среде, включая честные, не переподписанные тарифы: гипервизору всегда нужно какое-то время на собственные служебные задачи. Тревогу стоит бить, когда st держится двузначным числом продолжительное время или регулярно подскакивает синхронно с жалобами пользователей.
Это можно вылечить настройками внутри своей VPS, не переезжая?
Изнутри гостевой системы — нет: у вас просто нет контроля над планировщиком хоста и над тем, что делают соседи. Единственные рабочие пути — тариф с гарантированными ядрами, миграция на менее нагруженный хост (иногда провайдер может перенести ВМ по заявке) или переход на выделенный сервер, где эта проблема отсутствует по конструкции.
Как отличить steal time от троттлинга cgroup у себя же в контейнере?
Смотрите на источник счётчика: троттлинг cgroup виден изнутри как nr_throttled/throttled_time в /sys/fs/cgroup/.../cpu.stat — это ваш собственный лимит, который вы сами (или оркестратор) выставили контейнеру. Steal time — это метрика гипервизора уровня выше вашей ОС, и никакого cgroup-лимита в игре может не быть вовсе, как и было в нашем случае.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →