---
title: "ОС глазами бэкендера: собираем всё вместе"
description: "По курсу мы разбирали ОС по частям: процессы, память, потоки и локи, диски, файлы, контейнеры. В проде они работают вместе, и проблемы обычно возникают на стыках. Эта глава — про…"
canonical: https://graphlms.ru/os/book/os-for-backend
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

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

> Источник: https://graphlms.ru/os/book/os-for-backend — курс «Операционные системы», GraphLMS.

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

По курсу мы разбирали ОС по частям: процессы, память, потоки и локи, диски,
файлы, контейнеры. В проде они работают вместе, и проблемы обычно возникают на
стыках. Эта глава — про то, как по симптому в метриках назвать механизм ОС,
который виноват, и куда смотреть дальше.

Сначала два слова, которыми меряют сервис.

- **Задержка** (latency) — время от прихода запроса до ухода ответа. Её
  смотрят в перцентилях: p50 (медиана, типичный запрос) и p99 (хуже, чем у 99%
  запросов, «хвост»). Средняя задержка почти бесполезна: она прячет хвост.
- **Пропускная способность** (throughput) — сколько запросов в секунду
  обслуживает сервис (RPS).

Они часто тянут в разные стороны, как время оборота и время отклика у
[планировщика](/os/book/scheduling): групповой коммит повышает пропускную
способность ценой задержки, а большая очередь запросов — наоборот.

## Путь одного запроса

Пользователь вызвал HTTP-ручку, Go-сервис сходил в PostgreSQL и ответил.
Проследим запрос через все слои и отметим порядок времени на каждом.

```text
  [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` и дашборды делят время процессора на категории. Каждая
указывает на свой механизм.

```text
  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` — признак того, что
рабочее множество не помещается в память (см. [своп](/os/book/swapping)).

Кроме долей CPU, полезны ещё несколько чисел:

- **переключения контекста** — `vmstat` (колонка `cs`) по системе, `pidstat -w`
  по процессам. Резкий рост при прежнем RPS — борьба за локи, слишком много
  потоков, троттлинг;
- **major page faults** — `pidstat -r` (колонка `majflt/s`). Каждый —
  чтение с диска ради памяти: своп или первое обращение к странице
  mmap-файла;
- **троттлинг cgroup** — `nr_throttled` и `throttled_usec` в `cpu.stat` группы
  контейнера (см. [контейнеры](/os/book/containers-namespaces)).

## Четыре ресурса — четыре вопроса

| Ресурс | Что проверить | Чем грозит |
|---|---|---|
| Память | RSS растёт? Близко к `memory.max`? Есть ли своп? Стоит ли `GOMEMLIMIT` ниже лимита? | OOM kill (код 137), частая сборка мусора, своп |
| CPU | `us` или `sy`? Есть ли троттлинг? `GOMAXPROCS` соответствует лимиту? Много переключений? | хвост задержки, деградация под нагрузкой |
| Диск | случайный или последовательный доступ? `fsync` на каждый запрос? Холодный page cache? | высокий `wa`, p99 в миллисекундах |
| Сеть | сколько соединений, переиспользуются ли они, мелкие ли пакеты, какой RTT до зависимостей | фиксированная добавка к каждому запросу |

Память обычно убивает резко (OOM), CPU и диск портят хвост задержки
постепенно, сеть добавляет постоянную цену к каждому походу к зависимости.

Пояснения по каждому ресурсу:

- **Память.** Процесс в контейнере видит память всего хоста, но ограничен
  [cgroup](/os/book/containers-namespaces). Превысил `memory.max` — `SIGKILL`
  и код 137, даже если на хосте свободно. VIRT (обещанное адресное
  пространство) — не RSS (занятая физическая память), большой VIRT не утечка.
  Подробно — [память в проде](/os/book/memory-cloud).
- **CPU.** Превышение CPU-лимита не убивает, а останавливает всю группу до
  конца периода квоты. Отсюда «пила» на графике p99 при невысокой средней
  загрузке.
- **Диск.** Случайный доступ дороже последовательного (на HDD — в сотни раз),
  `fsync` ждёт устройство. Поэтому базы пишут
  [журнал](/os/book/journaling) последовательно и группируют коммиты.
- **Сеть.** Сервер не держит поток ОС на соединение: в Go это скрыто за
  горутинами и [netpoller](/os/book/event-based-concurrency). Но каждое новое
  соединение — это рукопожатие 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, данные читаются с диска | [память в проде](/os/book/memory-cloud) |
| высокий `sy` | много системных вызовов, мелкие операции без буфера | [цена системного вызова](/os/book/syscall-cost) |
| высокий `wa`, p99 скачет | упор в диск: случайный доступ или `fsync` | [HDD](/os/book/hdd), [SSD](/os/book/ssd), [fsync](/os/book/fsync-durability) |
| контейнер падает с кодом 137 | превышен `memory.max`, OOM killer | [контейнеры](/os/book/containers-namespaces), [память в проде](/os/book/memory-cloud) |
| p99 «пилит», средняя загрузка CPU низкая | троттлинг по квоте cgroup | [контейнеры](/os/book/containers-namespaces) |
| ненулевые `si`/`so` в `vmstat`, много major faults | рабочее множество не влезает в память, своп | [своп](/os/book/swapping) |
| много переключений контекста, CPU занят, RPS не растёт | борьба за локи, слишком много потоков | [локи](/os/book/locks-hardware), [конкурентные структуры](/os/book/concurrent-structures) |
| запросы зависают навсегда, CPU свободен | взаимоблокировка | [взаимоблокировки](/os/book/deadlock) |
| запись «потерялась» после сбоя питания | данные были в page cache, не было `fsync` | [fsync](/os/book/fsync-durability) |
| `too many open files` | кончились дескрипторы, часто утечка | [файлы и дескрипторы](/os/book/files-directories) |
| сборка мусора съедает CPU под нагрузкой | память близко к лимиту, нет `GOMEMLIMIT` или он слишком низкий | [память в проде](/os/book/memory-cloud) |

## Инструменты, которые стоит знать

Не нужно помнить все флаги, но стоит знать, что такие инструменты существуют
и на какой вопрос отвечают.

| Вопрос | 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` |

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/os-for-backend

## Частые ошибки

- Читать среднюю задержку вместо перцентилей и среднюю загрузку 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 (и холодные пулы соединений).

## Вопросы для самопроверки

**Квиз «ОС для бэкендера»**

1. Сервис тормозит, в top высокий показатель sy (system CPU). Что это вероятнее всего значит?
   - Слишком много системных вызовов: вход в ядро на каждый мелкий read/write (верно)
   - Процессор простаивает в ожидании диска
   - Код выполняет тяжёлые вычисления в user-space
   - Кончилась память и начался своп
   - Пояснение: Высокий sy — это много времени в ядре, типичный признак избытка syscall'ов (мелкие read/write вместо батчей, лог в файл на каждый запрос). Тяжёлые вычисления дали бы высокий us, ожидание диска — высокий wa.
2. В метриках высокий iowait (wa). Что из этого верно?
   - Это означает, что CPU перегружен вычислениями
   - Помогает добавить ядер CPU
   - CPU простаивает, ожидая завершения дисковых операций (верно)
   - Причиной может быть random I/O, fsync или холодный page cache (верно)
   - Пояснение: iowait — это простой CPU в ожидании диска, а не перегрузка вычислениями. Добавление CPU не поможет — узкое место в диске. Типичные причины: random I/O, синхронный fsync, чтение холодных данных с диска.
3. Какие операции на пути запроса измеряются в миллисекундах (а не нано/микросекундах)?
   - Обращение к данным при page cache HIT (из RAM)
   - fsync записи в WAL базы данных (верно)
   - Переключение контекста между горутинами
   - Чтение при page cache MISS (с диска) (верно)
   - Пояснение: fsync и чтение с диска (page cache miss) — это миллисекунды, обращение к носителю. Page cache hit (из RAM) — наносекунды, context switch — микросекунды. Латентность прячется там, где появляется 'ms'.
4. На сервере растёт p99, в vmstat у процесса стабильно ненулевые si/so (swap in/out), а в top почти весь RAM занят. Что происходит?
   - Рабочее множество не помещается в RAM: страницы постоянно уходят в swap и возвращаются обратно, то есть начался thrashing (верно)
   - Слишком много горутин, поэтому их мало на каждый поток
   - Сеть деградирует, и ответы приходят позже
   - Диск слишком быстрый для текущей нагрузки
   - Пояснение: si/so показывают, что система выгружает страницы на диск и читает их обратно. Каждое такое чтение на порядки медленнее обращения к RAM, поэтому хвост задержек (p99) взлетает первым. Лечат уменьшением потребления памяти или добавлением RAM. На многих боевых серверах swap отключают, чтобы вместо тихой деградации получить быстрый OOM и перезапуск.
5. Обработчик запроса делал 20 мелких write() в сокет. Вы добавили буфер, и теперь он делает один write() на запрос. Во сколько раз реже программа входит в ядро ради записи?
   - Ответ: 20
   - Пояснение: В 20 раз: было 20 входов в ядро на запрос, стал один. Каждый write() платит за trap, смену режима и проверки, поэтому батчинг (bufio.Writer, writev) один из самых дешёвых способов снизить sy в top.
6. Как один поток в современном сервере обслуживает десятки тысяч соединений одновременно?
   - Открывает один сокет за раз и обрабатывает последовательно
   - Создаёт по потоку ОС на каждое соединение
   - Использует epoll/netpoller: спит на epoll_wait и будится только по готовым сокетам (верно)
   - Опрашивает каждый сокет в бесконечном цикле (busy-poll)
   - Пояснение: Событийная модель: epoll позволяет одному потоку ждать события на тысячах сокетов и просыпаться только на готовых. В Go это спрятано за netpoller'ом — вы пишете блокирующий conn.Read, а горутина паркуется и будится по epoll-событию.
