GraphLMS

ОС
Начать

ОС · ОС в проде · 15 мин

ОС глазами бэкендера: собираем всё вместе

Зачем эта глава

Весь курс мы по кусочкам разбирали ОС: процессы, потоки, память, диск, файлы, сеть. Каждый кусок жил отдельно. Но в реальном проде они работают вместе, и именно на стыках прячется то, ради чего вас наняли, — latency (задержка, сколько ждёт один запрос) и throughput (пропускная способность, сколько запросов в секунду тянет сервис).

Давайте определим эти два слова сразу, по-простому.

  • Latency (задержка) — время от «пришёл запрос» до «ушёл ответ» для одного запроса. Меряют в перцентилях: p50 (медиана, типичный запрос), p99 (худший из 100, «хвост»). Бытовая аналогия: сколько ты стоишь в очереди в кассу.
  • Throughput (пропускная) — сколько запросов сервис обслуживает за секунду (RPS). Аналогия: сколько человек касса пропускает за час.

Эти двое часто тянут в разные стороны — ровно как turnaround и response у планировщика. Хороший бэкендер не зубрит «как устроено ядро», он умеет по симптому в метриках назвать механизм ОС, который виноват. Этим и займёмся.

Жизнь одного запроса: полный путь через ОС

Возьмём типичный сценарий: пользователь дёрнул HTTP-ручку, сервис на Go сходил в базу и вернул ответ. Проследим запрос через все слои, что мы изучили, и отметим, где сколько времени теряется.

            ЖИЗНЬ ОДНОГО ЗАПРОСА (HTTP → БД → ответ)
 
  [1] Пакет из сети                              ~µs (сетевая карта)
       │  NIC принял байты, DMA кладёт их в RAM
       ▼     (DMA = карта пишет в память сама, без CPU)
  [2] Прерывание (IRQ)                           ~µs
       │  карта дёргает CPU: "данные готовы"
       ▼  ядро в обработчике прерывания, softirq
  [3] netpoller / epoll                          ~µs
       │  ядро: "сокет читаемый" → epoll_wait
       ▼  рантайм Go будит горутину, ждавшую сокет
  [4] Планировщик даёт CPU                        ~µs..ms
       │  горутина в runnable → шедулер Go на M:P
       ▼  если все P заняты — ждём в очереди (latency!)
  [5] Код парсит запрос, читает данные           ~ns..ms
       │  page cache HIT  → наносекунды (из RAM)
       │  page cache MISS → миллисекунды (с диска)

  [6] Запрос в БД: открыть сокет, послать         ~ms (сеть+БД)
       │  снова [1..4] но уже на стороне БД
       ▼  БД пишет WAL: fsync → ждём подтверждения диска
  [7] fsync / запись на диск                      ~ms (HDD) / ~100µs (SSD)
       │  durability: данные точно на носителе

  [8] Формируем ответ, write() в сокет            ~µs
       │  syscall → копирование в буфер ядра
       ▼  NIC отправляет пакеты
  [9] Ответ ушёл клиенту

Что нарисовано: один запрос проходит девять слоёв, и каждый — это отдельная глава курса. Обратите внимание на порядки времени справа: переключения и прерывания живут в микросекундах, page cache hit — в наносекундах, а вот диск (fsync) и поход по сети в БД — это миллисекунды, в тысячи раз дороже. Латентность почти всегда прячется там, где появляется буква «ms».

Где именно прячется латентность

Разложим тот же путь по «стоимости» и привяжем к главам, где механизм разобран подробно.

   ДЁШЕВО (ns)        СРЕДНЕ (µs)            ДОРОГО (ms)
  ┌───────────┐     ┌──────────────┐      ┌──────────────────┐
  │ page cache│     │ syscall      │      │ диск random I/O  │
  │   HIT     │     │ context      │      │ fsync (durability│
  │ обращение │     │  switch      │      │  WAL)            │
  │ к RAM     │     │ прерывание   │      │ page cache MISS  │
  │ TLB hit   │     │ epoll wakeup │      │ своп (page out)  │
  └───────────┘     └──────────────┘      │ сеть до БД       │
                                          └──────────────────┘
       ▲                  ▲                        ▲
   из RAM/кэша       вход в ядро            физический носитель
                                            или другая машина

Что нарисовано: грубая «лестница цен». Запомните правило большого пальца — RAM в ~100 000 раз быстрее диска, а SSD random-чтение в ~100 раз быстрее HDD. Поэтому первая интуиция при «тормозит»: мы случайно ушли из RAM на диск или в сеть?

Каждая ступенька — это глава:

Как читать метрики: симптом → причина

Самое полезное умение — посмотреть на top/htop/дашборд и назвать механизм. Разберём четыре главных столбца CPU и спутники.

        КАРТА СИМПТОМОВ В МЕТРИКАХ CPU
 
  high  us  (user)   → код реально считает на CPU.
                       Норма для CPU-bound. Если неожиданно
                       высоко — горячий цикл, сериализация,
                       регэкспы, GC.
 
  high  sy  (system) → много времени В ЯДРЕ.
                       Симптом: слишком много syscall'ов.
                       Причины: мелкие read/write вместо
                       батчей, много соединений, лог в файл
                       на каждый запрос.  → syscall-cost
 
  high  wa  (iowait) → CPU простаивает, ЖДЁТ диск.
                       Симптом: упёрлись в диск (random I/O,
                       fsync, холодный page cache, своп).
                       → hdd / ssd / fsync-durability
 
  high  si/hi(softirq)→ много сетевых прерываний.
                       Симптом: шторм пакетов, мелкие
                       пакеты, один CPU разгребает всю сеть.
 
  + cs (context switches) высокий → потоки/горутины дерутся
                       за CPU, частые блокировки на локах,
                       слишком много рантайм-потоков.
 
  + page faults (major) высокий → ходим на диск за памятью:
                       холодный mmap, своп.  → swapping

Что нарисовано: шпаргалка «какой столбец о чём кричит». Ключевое различие, которое любят на собесе: высокий us — это «мы много считаем» (часто ок), а высокий sy — это «мы много дёргаем ядро» (почти всегда подозрительно, см. стоимость syscall). А высокий wa (iowait) — это вообще не «CPU занят», это «CPU стоит без дела и ждёт диск», лечится совсем иначе.

Отдельно про major page fault (большой промах страницы): это когда процесс обратился к адресу, а нужной страницы нет в RAM — её надо тащить с диска (своп или ленивый mmap). Один major fault = миллисекунды. Если их много — вы либо в свопе, либо постоянно читаете «холодные» данные, см. свопинг.

Чеклист ОС-интуиции для бэкендера

Четыре ресурса, четыре вопроса. Держите в голове как чеклист при разборе инцидента.

  ┌──────────┬─────────────────────────────────────────────┐
  │ РЕСУРС    │ ЧТО ПРОВЕРИТЬ И ЧЕМ ГРОЗИТ                   │
  ├──────────┼─────────────────────────────────────────────┤
  │ ПАМЯТЬ    │ RSS растёт? упёрлись в cgroup limit?         │
  │           │ → OOM kill (exit 137). VIRT≠RSS.            │
  │           │ GOMEMLIMIT < лимита.  → memory-cloud         │
  ├──────────┼─────────────────────────────────────────────┤
  │ CPU       │ throttling по cgroup quota? GOMAXPROCS       │
  │           │ под лимит? много context switch?            │
  │           │ → хвост latency.  → containers-namespaces    │
  ├──────────┼─────────────────────────────────────────────┤
  │ ДИСК      │ random или sequential? fsync на каждый       │
  │           │ запрос? холодный page cache?                 │
  │           │ → iowait, p99.  → hdd / fsync-durability     │
  ├──────────┼─────────────────────────────────────────────┤
  │ СЕТЬ      │ epoll/netpoller, число соединений,           │
  │           │ мелкие пакеты, RTT до зависимостей.         │
  │           │ → latency.  → event-based-concurrency        │
  └──────────┴─────────────────────────────────────────────┘

Что нарисовано: четыре «оси», по которым бэкендер диагностирует прод. Память чаще всего убивает резко (OOM), CPU и диск — портят хвост latency постепенно, сеть — добавляет фиксированный «налог» на каждый поход к зависимости.

Разберём важные нюансы каждой оси.

Память. В контейнере процесс видит память всей ноды, но ограничен cgroup-лимитом. Превысил memory limit → OOM killer убивает процесс с SIGKILL, exit code = 128+9 = 137, даже если на хосте памяти полно. VIRT (виртуальное АП, «обещания») — не то же, что RSS (физическая RAM прямо сейчас), большой VIRT — не утечка. Подробно — память в проде.

CPU. В отличие от памяти, превышение CPU-лимита cgroup не убивает, а троттлит: ядро отбирает у процесса CPU в конце периода квоты, и горутины замирают на десятки миллисекунд — характерный «пилообразный» хвост p99. Лечится выравниванием GOMAXPROCS под CPU-лимит, см. контейнеры и namespaces и планировщик Go.

Диск. Главная мысль из глав про железо: для диска random I/O в разы дороже sequential, а fsync (форсированный сброс на носитель ради долговечности) — это синхронное ожидание физического устройства. Базы поэтому пишут WAL/журнал последовательно, а не разбрасывают записи.

Сеть. Современный сервер не держит поток на соединение — он использует epoll: один поток ждёт события на тысячах сокетов. В Go это спрятано за netpoller'ом: вы пишете «блокирующий» conn.Read, а под капотом горутина паркуется и будится по epoll-событию. Это связывает главу про горутины с механикой ОС.

Сводная таблица: симптом в проде → механизм ОС → глава

Симптом в проде Что происходит в ОС Куда смотреть
Первые запросы после деплоя медленные Холодный page cache, читаем с диска swapping, memory-cloud
Высокий sy (system CPU) Слишком много syscall'ов, мелкие read/write syscall-cost
Высокий wa (iowait), p99 скачет Упёрлись в диск: random I/O или fsync hdd, ssd, fsync-durability
Контейнер падает с exit 137 Превышен cgroup memory limit → OOM containers-namespaces, memory-cloud
p99 «пилит», CPU будто не загружен CPU-троттлинг по cgroup quota containers-namespaces
Много major page faults, тормоза Своп / ленивый mmap тащит страницы с диска swapping
Много context switches Драка за CPU, частые блокировки на локах scheduling, locks
Запись «потерялась» после краша Данные были в page cache, не было fsync fsync-durability, journaling
10k соединений, а потоков мало epoll/netpoller: событийная модель event-based-concurrency
GC агрессивно жрёт CPU под нагрузкой Давление на память близко к лимиту memory-cloud

Что в таблице: готовый «справочник дежурного» — увидел строку слева, назвал механизм по центру, пошёл углубляться по ссылке справа. Именно так выглядит зрелый разбор инцидента.

Как всё это связано: одна картинка

   ЗАПРОС


  ┌─────────┐  epoll/netpoller   ┌──────────────┐
  │  СЕТЬ    │ ─────────────────▶ │ ПЛАНИРОВЩИК   │ ← context switch
  │ (NIC,DMA)│                    │ (CPU, P/M/G)  │   = µs налог
  └─────────┘                    └──────┬───────┘
                                        │ syscall = вход в ядро

                                 ┌──────────────┐
                                 │   ПАМЯТЬ      │ ← page cache hit = ns
                                 │ (RAM, cgroup) │   miss = ms (диск)
                                 └──────┬───────┘   limit → OOM


                                 ┌──────────────┐
                                 │    ДИСК       │ ← fsync/WAL = ms
                                 │ (page cache,  │   random ≫ seq
                                 │  fsync)       │
                                 └──────────────┘

Что нарисовано: те же четыре ресурса из чеклиста, но как они передают запрос друг другу. Запрос «втекает» через сеть, планировщик даёт ему CPU, код трогает память (дёшево, если в кэше) и в конце упирается в диск (дорого, если нужна долговечность). Узкое место вашего сервиса — это всегда один из этих четырёх стыков, и ваша работа — по метрикам понять, какой.

Проверь себя· ОС для бэкендера

Сервис тормозит, в top высокий показатель sy (system CPU). Что это вероятнее всего значит?

В метриках высокий iowait (wa). Что из этого верно?

Какие операции на пути запроса измеряются в миллисекундах (а не нано/микросекундах)?

Контейнер в Kubernetes упал с exit code 137, при этом на ноде свободной памяти достаточно. Причина?

Чему равен exit code процесса, убитого OOM killer'ом (сигнал SIGKILL, номер 9)?

Как один поток в современном сервере обслуживает десятки тысяч соединений одновременно?

Что спрашивают на собесе

  • «Опиши путь HTTP-запроса от пакета до ответа». Сильный ответ идёт по слоям: NIC+DMA → прерывание → epoll/netpoller будит горутину → планировщик даёт CPU → код читает данные (page cache hit/miss) → запрос в БД с fsync/WAL → ответ. Покажите, что знаете, где наносекунды, а где миллисекунды.
  • «Высокий sys CPU — о чём это и чем отличается от user?» us — код считает (часто норма), sy — много времени в ядре, то есть слишком много syscall'ов (мелкие read/write вместо батчей). Лечится батчингом, буферизацией, переиспользованием соединений. См. syscall-cost.
  • «iowait высокий — что делать?» Это CPU простаивает, ждёт диск: упёрлись в random I/O, fsync или холодный page cache. Не лечится добавлением CPU — нужно смотреть на диск, паттерн доступа (random→sequential), кэш.
  • «Контейнер упал с exit code 137 при свободной памяти на ноде». cgroup memory limit + OOM killer внутри группы; 137 = 128+9 (SIGKILL). Хост здоров, лимит контейнера превышен. CPU-лимит, в отличие от memory, не убивает, а троттлит.
  • «Сервис медленный сразу после деплоя, через минуту быстрый — почему?» Холодный page cache: первые чтения идут на диск, потом данные оседают в RAM. Назвать именно page cache, а не только прогрев GC/JIT.
  • «Как один поток обслуживает 10k соединений?» epoll/netpoller: событийная модель, поток спит на epoll_wait и будится только на готовых сокетах; в Go это спрятано за горутинами и netpoller'ом.
Контейнеры: namespaces и cgroups