ОС в проде · 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% лимита. Разберём по шагам.

  1. Память? Перезапусков нет, OOMKilled нет, RSS стабилен. Не она.
  2. Диск? Сервис почти не трогает локальный диск. wa около нуля. Не он.
  3. CPU. 40% средней загрузки ни о чём не говорит: квота считается по периодам в 100 мс. Смотрим cpu.stat контейнера: cat /sys/fs/cgroup/cpu.stat внутри пода. nr_throttled растёт на десятки в минуту, throttled_usec велико. Значит, группа регулярно выбирает квоту и стоит.
  4. Почему? Лимит cpu: 2, а узлы кластера — по 48 ядер процессора. Образ собран Go 1.22, GOMAXPROCS = 48. Под всплеском запросов 48 потоков одновременно выедают 200 мс квоты за ~4 мс и стоят оставшиеся ~96 мс.
  5. Лечение. Задать GOMAXPROCS=2 через переменную окружения (или пересобрать на Go 1.25 с go 1.25 в go.mod, тогда рантайм возьмёт лимит сам). Дополнительно обсудить, нужен ли вообще CPU-лимит или достаточно requests.
  6. Проверка. 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.

Проверьте себя

  1. У сервиса высокий sy и низкий us. Какие две гипотезы проверите первыми и каким инструментом?
  2. wa = 2% на 64-ядерной машине. Можно ли сделать вывод, что диск — не узкое место?
  3. В vmstat колонки si и so стабильно ненулевые, p99 вырос. Что происходит и почему страдает именно хвост?
  4. Под с лимитом 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 (и холодные пулы соединений).

Продолжить