ОС в проде · 11 мин
Контент v0.12.0ОС глазами бэкендера: собираем всё вместе
Зачем эта глава
По курсу мы разбирали ОС по частям: процессы, память, потоки и локи, диски, файлы, контейнеры. В проде они работают вместе, и проблемы обычно возникают на стыках. Эта глава — про то, как по симптому в метриках назвать механизм ОС, который виноват, и куда смотреть дальше.
Сначала два слова, которыми меряют сервис.
- Задержка (latency) — время от прихода запроса до ухода ответа. Её смотрят в перцентилях: p50 (медиана, типичный запрос) и p99 (хуже, чем у 99% запросов, «хвост»). Средняя задержка почти бесполезна: она прячет хвост.
- Пропускная способность (throughput) — сколько запросов в секунду обслуживает сервис (RPS).
Они часто тянут в разные стороны, как время оборота и время отклика у планировщика: групповой коммит повышает пропускную способность ценой задержки, а большая очередь запросов — наоборот.
Путь одного запроса
Пользователь вызвал HTTP-ручку, Go-сервис сходил в PostgreSQL и ответил. Проследим запрос через все слои и отметим порядок времени на каждом.
[1] сетевая карта принимает пакеты, DMA кладёт их в RAM мкс
(устройства, DMA — глава io-devices)
[2] прерывание, softirq: сетевой стек ядра ОС разбирает пакеты мкс
и кладёт данные в буфер сокета
[3] netpoller: epoll_wait вернул сокет, горутина обработчика мкс
снова готова к запуску (глава event-based-concurrency)
[4] планировщик Go ставит горутину на поток ОС мкс…мс
если все P заняты или контейнер в троттлинге — ждём
[5] обработка: разбор JSON, логика, аллокации мкс…мс
чтение файла: page cache hit — мкс, miss — 0,1…10 мс
[6] запрос в PostgreSQL по сети: снова шаги 1–4 на стороне базы 0,1…1 мс
[7] COMMIT в базе: fsync журнала WAL 0,05…10 мс
(fsync-durability; зависит от диска)
[8] ответ: write в сокет, ядро ОС копирует данные в буфер, мкс
сетевая карта отправляет пакетыПочти всё время уходит туда, где появляются миллисекунды: ожидание в очереди на
CPU, промахи page cache, fsync, сетевые походы к зависимостям. Переходы в ядро
ОС, переключения и прерывания стоят микросекунды и становятся проблемой, только
когда их очень много.
Лестница цен
Порядки величин, которые полезно знать наизусть (приблизительно, для современного сервера):
| Операция | Время |
|---|---|
| обращение к кэшу L1 | ~1 нс |
| обращение к RAM | ~100 нс |
| системный вызов | ~100 нс … 0,5 мкс |
| переключение контекста потоков ОС | единицы мкс |
| чтение 4 КБ с NVMe | ~50–100 мкс |
| сетевой поход внутри дата-центра | ~0,1–0,5 мс |
fsync на NVMe с PLP / без PLP |
десятки мкс / единицы мс |
| чтение 4 КБ с HDD | ~10 мс |
Память быстрее NVMe примерно в тысячу раз, а NVMe быстрее HDD примерно в сто. Отсюда первая интуиция при «тормозит»: не ушли ли мы случайно из памяти на диск или в сеть, и не стоим ли мы в очереди.
Метрики CPU: что значат колонки top
top, vmstat и дашборды делят время процессора на категории. Каждая
указывает на свой механизм.
us (user) код процесса считает в пользовательском режиме.
Высоко и ожидаемо — CPU-bound нагрузка. Неожиданно высоко —
горячий цикл, сериализация, регулярные выражения, сборка мусора.
sy (system) время в ядре ОС от имени процессов.
Высоко — много системных вызовов (мелкие read/write без буфера,
лог на каждый запрос), много переключений, работа с памятью.
→ syscall-cost
wa (iowait) ядро процессора простаивает, а у системы есть незавершённый
дисковый ввод-вывод.
→ hdd, ssd, fsync-durability
si (softirq) обработка прерываний, в основном сетевых пакетов.
Высоко на одном ядре процессора — все прерывания сетевой
карты приходят туда. → io-devices
st (steal) виртуальная машина хотела работать, но гипервизор отдал
физическое ядро процессора соседям. Шумные соседи в облаке.Две детали, на которых ошибаются.
iowait — это простой, а не нагрузка. Ядро процессора ничего не делает, и
среди ожидающих есть дисковый ввод-вывод. На машине с 32 ядрами процессора
один поток, ждущий диск, даст небольшой wa, хотя для этого потока диск —
узкое место. А если те же ядра процессора займёт полезная работа, wa упадёт
до нуля при том же медленном диске. Ожидание сети в wa не попадает вообще.
Поэтому wa — подсказка, а не диагноз; про диск надёжнее смотреть iostat -x
(загрузка устройства, задержка операций).
Одни и те же буквы — разные вещи. В top колонка si — время softirq. В
vmstat колонки si и so — swap in и swap out, страницы, прочитанные из
свопа и записанные в него. Ненулевые si/so в vmstat — признак того, что
рабочее множество не помещается в память (см. своп).
Кроме долей CPU, полезны ещё несколько чисел:
- переключения контекста —
vmstat(колонкаcs) по системе,pidstat -wпо процессам. Резкий рост при прежнем RPS — борьба за локи, слишком много потоков, троттлинг; - major page faults —
pidstat -r(колонкаmajflt/s). Каждый — чтение с диска ради памяти: своп или первое обращение к странице mmap-файла; - троттлинг cgroup —
nr_throttledиthrottled_usecвcpu.statгруппы контейнера (см. контейнеры).
Четыре ресурса — четыре вопроса
| Ресурс | Что проверить | Чем грозит |
|---|---|---|
| Память | RSS растёт? Близко к memory.max? Есть ли своп? Стоит ли GOMEMLIMIT ниже лимита? |
OOM kill (код 137), частая сборка мусора, своп |
| CPU | us или sy? Есть ли троттлинг? GOMAXPROCS соответствует лимиту? Много переключений? |
хвост задержки, деградация под нагрузкой |
| Диск | случайный или последовательный доступ? fsync на каждый запрос? Холодный page cache? |
высокий wa, p99 в миллисекундах |
| Сеть | сколько соединений, переиспользуются ли они, мелкие ли пакеты, какой RTT до зависимостей | фиксированная добавка к каждому запросу |
Память обычно убивает резко (OOM), CPU и диск портят хвост задержки постепенно, сеть добавляет постоянную цену к каждому походу к зависимости.
Пояснения по каждому ресурсу:
- Память. Процесс в контейнере видит память всего хоста, но ограничен
cgroup. Превысил
memory.max—SIGKILLи код 137, даже если на хосте свободно. VIRT (обещанное адресное пространство) — не RSS (занятая физическая память), большой VIRT не утечка. Подробно — память в проде. - CPU. Превышение CPU-лимита не убивает, а останавливает всю группу до конца периода квоты. Отсюда «пила» на графике p99 при невысокой средней загрузке.
- Диск. Случайный доступ дороже последовательного (на HDD — в сотни раз),
fsyncждёт устройство. Поэтому базы пишут журнал последовательно и группируют коммиты. - Сеть. Сервер не держит поток ОС на соединение: в Go это скрыто за
горутинами и netpoller. Но каждое новое
соединение — это рукопожатие TCP (и TLS), поэтому пулы соединений к базе и
http.Clientс keep-alive важнее, чем кажется.
Пример разбора: p99 вырос после переезда в Kubernetes
Сервис на Go переехал с виртуальных машин в Kubernetes. Средняя задержка не изменилась, а p99 вырос с 15 до 120 мс. Загрузка CPU пода по графику — 40% лимита. Разберём по шагам.
- Память? Перезапусков нет,
OOMKilledнет, RSS стабилен. Не она. - Диск? Сервис почти не трогает локальный диск.
waоколо нуля. Не он. - CPU. 40% средней загрузки ни о чём не говорит: квота считается по
периодам в 100 мс. Смотрим
cpu.statконтейнера:cat /sys/fs/cgroup/cpu.statвнутри пода.nr_throttledрастёт на десятки в минуту,throttled_usecвелико. Значит, группа регулярно выбирает квоту и стоит. - Почему? Лимит
cpu: 2, а узлы кластера — по 48 ядер процессора. Образ собран Go 1.22,GOMAXPROCS= 48. Под всплеском запросов 48 потоков одновременно выедают 200 мс квоты за ~4 мс и стоят оставшиеся ~96 мс. - Лечение. Задать
GOMAXPROCS=2через переменную окружения (или пересобрать на Go 1.25 сgo 1.25вgo.mod, тогда рантайм возьмёт лимит сам). Дополнительно обсудить, нужен ли вообще CPU-лимит или достаточноrequests. - Проверка.
nr_throttledперестал расти, p99 вернулся к 15 мс.
Обратите внимание на ход рассуждения: от симптома (хвост задержки при низкой
средней загрузке) к механизму (квота cgroup считается за период и по всем
потокам) и к метрике, которая его подтверждает (cpu.stat).
Справочник: симптом → механизм → глава
| Симптом | Что происходит в ОС | Куда смотреть |
|---|---|---|
| первые минуты после деплоя медленно | холодный page cache, данные читаются с диска | память в проде |
высокий sy |
много системных вызовов, мелкие операции без буфера | цена системного вызова |
высокий wa, p99 скачет |
упор в диск: случайный доступ или fsync |
HDD, SSD, fsync |
| контейнер падает с кодом 137 | превышен memory.max, OOM killer |
контейнеры, память в проде |
| p99 «пилит», средняя загрузка CPU низкая | троттлинг по квоте cgroup | контейнеры |
ненулевые si/so в vmstat, много major faults |
рабочее множество не влезает в память, своп | своп |
| много переключений контекста, CPU занят, RPS не растёт | борьба за локи, слишком много потоков | локи, конкурентные структуры |
| запросы зависают навсегда, CPU свободен | взаимоблокировка | взаимоблокировки |
| запись «потерялась» после сбоя питания | данные были в page cache, не было fsync |
fsync |
too many open files |
кончились дескрипторы, часто утечка | файлы и дескрипторы |
| сборка мусора съедает CPU под нагрузкой | память близко к лимиту, нет GOMEMLIMIT или он слишком низкий |
память в проде |
Инструменты, которые стоит знать
Не нужно помнить все флаги, но стоит знать, что такие инструменты существуют и на какой вопрос отвечают.
| Вопрос | Linux | Go |
|---|---|---|
| куда уходит CPU | top, pidstat -u, perf top |
pprof CPU-профиль |
| сколько системных вызовов и каких | strace -c (тормозит процесс), perf trace |
— |
| где ждут локи | — | pprof mutex- и block-профили |
| что с диском | iostat -x, pidstat -d |
— |
| что с памятью | free, vmstat, /sys/fs/cgroup/memory.* |
GODEBUG=gctrace=1, heap-профиль, runtime/metrics |
| троттлинг | /sys/fs/cgroup/cpu.stat |
— |
| что делают горутины | — | /debug/pprof/goroutine?debug=2, SIGQUIT |
| сколько горутина ждала планировщик | — | runtime/metrics: /sched/latencies:seconds |
Сервис тормозит, в top высокий показатель sy (system CPU). Что это вероятнее всего значит?
В метриках высокий iowait (wa). Что из этого верно?
Какие операции на пути запроса измеряются в миллисекундах (а не нано/микросекундах)?
На сервере растёт p99, в vmstat у процесса стабильно ненулевые si/so (swap in/out), а в top почти весь RAM занят. Что происходит?
Обработчик запроса делал 20 мелких write() в сокет. Вы добавили буфер, и теперь он делает один write() на запрос. Во сколько раз реже программа входит в ядро ради записи?
Как один поток в современном сервере обслуживает десятки тысяч соединений одновременно?
Частые ошибки
- Читать среднюю задержку вместо перцентилей и среднюю загрузку CPU вместо троттлинга.
- Лечить высокий
waдобавлением ядер процессора. Узкое место — диск или паттерн доступа к нему. - Считать высокий
waнадёжным признаком проблем с диском и низкий — признаком их отсутствия. - Путать
siвtop(softirq) иsiвvmstat(swap in). - Видеть код 137 и сразу увеличивать память, не проверив, был ли это OOM.
- Объяснять медленные первые минуты после деплоя только прогревом JIT или сборщика мусора, забыв про холодный page cache.
Проверьте себя
- У сервиса высокий
syи низкийus. Какие две гипотезы проверите первыми и каким инструментом? wa= 2% на 64-ядерной машине. Можно ли сделать вывод, что диск — не узкое место?- В
vmstatколонкиsiиsoстабильно ненулевые, p99 вырос. Что происходит и почему страдает именно хвост? - Под с лимитом
cpu: 1и Go 1.22 запущен на 32-ядерном узле. Какое значениеGOMAXPROCSбудет по умолчанию и чем это грозит?
Показать ответы
Ответы. (1) Много мелких системных вызовов (небуферизованная запись, лог на
каждый запрос) и много переключений контекста; strace -c или perf trace для
вызовов, pidstat -w для переключений. (2) Нельзя: на многоядерной машине
несколько потоков, ждущих диск, дают маленький wa; смотреть задержки
устройства в iostat -x и время ожидания в профиле. (3) Рабочее множество не
помещается в память, страницы уходят в своп и читаются обратно; медленными
становятся отдельные обращения, которые попали на выгруженную страницу, — это и
есть хвост. (4) 32: рантайм до 1.25 не видит квоту; потоки выбирают 100 мс
квоты за единицы миллисекунд и стоят до конца периода, p99 растёт.
Что спрашивают на собесе
- Опишите путь HTTP-запроса через ОС. Сетевая карта и DMA → прерывание и
сетевой стек → netpoller будит горутину → планировщик Go → обработка (page
cache hit или miss) → поход в базу с
fsyncжурнала → запись в сокет. Важно назвать, где микросекунды, а где миллисекунды. - Высокий
syпротив высокогоus?us— код считает, часто норма.sy— много времени в ядре ОС: системные вызовы, переключения. Лечится батчингом, буферизацией, переиспользованием соединений. - Высокий iowait — что делать? Смотреть на диск (
iostat -x) и паттерн доступа: случайный ввод-вывод,fsync, холодный кэш. Добавление CPU не поможет. - Контейнер падает с кодом 137 при свободной памяти на узле. Лимит
memory.maxи OOM killer группы; подтвердитьOOMKilled. - p99 вырос, а CPU загружен на 40%. Проверить троттлинг в
cpu.statиGOMAXPROCSотносительно лимита. - Сервис медленный после деплоя и ускоряется через минуту. Холодный page cache (и холодные пулы соединений).