Планирование CPU и переключение контекста

Планировщик ОС выбирает, какая runnable-задача получит процессорное время. Переключение контекста сохраняет состояние текущей задачи и восстанавливает состояние следующей. Это необходимо для многозадачности, но не бесплатно: растут накладные расходы, теряется locality кэшей и увеличивается время ожидания в очереди.

Зачем это на интервью

Тема проверяет умение отличить CPU saturation от блокировки I/O и не лечить p99 бесконтрольным увеличением параллелизма. Для backend-сервиса важны не только средняя утилизация CPU, но и очередь runnable-задач, CPU quota контейнера, контеншен и хвост задержки.

Минимум для E4

  • Объяснять состояния running, runnable и waiting/blocking.
  • Понимать, что runnable-задача ждёт CPU, а waiting-задача ждёт событие, I/O или синхронизацию.
  • Называть причины context switch: вытеснение, блокирующий системный вызов, ожидание lock, пробуждение более приоритетной задачи.
  • Отличать увеличение throughput от простого роста числа goroutine и потоков.

На каждом логическом CPU в Linux есть очередь задач. Планировщик учитывает policy и приоритет; для обычных задач это fair scheduling, для real-time — отдельные политики. Задача может выполняться, быть готовой к выполнению или спать в ожидании. Если runnable-задач больше доступных CPU, они конкурируют за квант времени и задержка растёт даже без изменения полезной работы.

Углубление для E5/Senior

Context switch включает работу ядра с регистрами, состоянием планировщика и адресным пространством. Стоимость зависит от архитектуры и рабочей нагрузки; особенно заметен косвенный эффект — холодные L1/L2/TLB и миграция задачи между CPU. Поэтому «стоимость одного переключения» нельзя использовать как универсальную константу: измеряйте конкретную нагрузку.

В контейнере доступность CPU определяется не только числом видимых CPU. CFS quota может периодически throttle группу: приложение наблюдает паузы, хотя node не загружен на 100%. Для Go GOMAXPROCS следует сопоставлять с выделенной квотой. Излишнее число runnable goroutine повышает конкуренцию; полезнее ограничить параллелизм, размер очереди и число одновременных внешних запросов.

Старший инженер формулирует гипотезу и проверяет её: schedstat/PSI показывают давление на CPU, pidstat -w — добровольные и недобровольные переключения, perf sched и trace — задержку планирования. Результат оценивают по p95/p99, throughput, error rate и CPU throttling, а не по одной метрике context switches.

Ключевые понятия

  • Runnable — задача готова выполняться, но может ждать свободный CPU; running — фактически исполняется; sleeping/waiting — ждёт события.
  • Run queue — очередь runnable-задач, конкурирующих за CPU. Длинная очередь при стабильной нагрузке — сигнал дефицита CPU или избыточного параллелизма.
  • Time slice — доля времени до пересмотра решения планировщика; точная длительность не должна быть частью ответа без контекста policy и версии ядра.
  • Voluntary context switch происходит, когда задача сама блокируется; involuntary — когда её вытесняет планировщик.
  • CPU affinity ограничивает набор CPU для задачи; migration переносит её между CPU и может ухудшить cache locality.
  • CPU throttling — принудительная пауза группы при исчерпании квоты cgroup, не равная обычной высокой загрузке CPU.

Типовые вопросы

  1. Что происходит при context switch?
    • Ядро сохраняет исполняемый контекст текущей задачи, выбирает следующую runnable-задачу и восстанавливает её состояние. Помимо прямых затрат возможны потери кэш-локальности и TLB.
  2. Чем runnable отличается от waiting?
    • Runnable уже может работать и ждёт CPU; waiting не может продолжить до события: данных из сети, освобождения mutex, таймера или I/O. Лечение различается.
  3. Почему больше потоков может замедлить CPU-bound сервис?
    • Потоков больше, чем CPU, создают конкуренцию, переключения и кэш-промахи. Ограничение числа активных задач часто улучшает p99 без потери throughput.
  4. Почему CPU usage не всегда объясняет latency в контейнере?
    • Процесс может быть throttled по cgroup quota или ждать CPU на перегруженном CPU, а агрегированная метрика node/контейнера скрывает это. Нужны метрики quota/throttled time и давление CPU.
  5. Что означает много voluntary context switches?
    • Часто это ожидание I/O, каналов, mutex или sleep. Это гипотеза, а не диагноз: надо сопоставить с профилем блокировок и трассировкой.
  6. Как соотносится планировщик Go с планировщиком Linux?
    • Go распределяет goroutine по потокам M и P; Linux распределяет сами M среди CPU. Проблемы возможны на любом уровне, поэтому trace Go и системные метрики дополняют друг друга.

Практика

  • Сравните CPU-bound программу с числом workers 1, равным числу выделенных CPU и в четыре раза больше. Соберите throughput, p99 и pidstat -w 1 -p <pid>; сформулируйте точку насыщения.
  • Создайте workload с mutex contention. Подтвердите ожидание через mutex profile Go и сопоставьте его с voluntary context switches, не объявляя переключения первопричиной без профиля.
  • В тестовом контейнере задайте CPU quota, затем запустите постоянную нагрузку. Снимите показатели cpu.stat cgroup и покажите, как throttling меняет latency при том же коде.
  • Сделайте trace через go tool trace и найдите goroutine, которая runnable дольше целевого порога. Проверьте, связан ли интервал с потоками и goroutine или с внешним ожиданием.

Частые ошибки и ловушки

  • Приравнивать количество context switches к проблеме: они нормальны; важны причина, частота относительно baseline и влияние на latency.
  • Оценивать CPU только средним значением, не рассматривая p99, run queue, PSI и throttled time.
  • Увеличивать GOMAXPROCS или worker pool сверх CPU quota без контрольного измерения.
  • Считать, что waiting-задача «нагружает CPU», либо что низкий CPU исключает дефицит CPU для конкретной очереди.
  • Фиксировать affinity без причины: это может ухудшить балансировку и работу GC.

Связанные темы

Операционные системы · Процессы, потоки и goroutine · Виртуальная память · Go · Наблюдаемость

Источники

  • Linux kernel documentation: Completely Fair Scheduler и документация scheduler.
  • Linux kernel documentation: Control Group v2 и файл cpu.stat.
  • man 7 sched, man 2 sched_setaffinity, man 1 perf-sched, man 1 pidstat.
  • Brendan Gregg, Systems Performance, главы о CPU saturation и планировании.
  • Документация Go: Execution Tracer и Package runtime.