Проблема
Представьте: вы деплоите Go-сервис в Kubernetes. В манифесте задаете лимит CPU = 2.
Kubernetes создает под на ноде с 128 ядрами CPU.
До Go 1.25 происходило вот что:
-
Go смотрит на ноду: «128 ядер!»
-
Устанавливает GOMAXPROCS=128
-
Создает 128 потоков ОС для выполнения горутин
Но контейнер имеет лимит = 2 CPU. Ядро Linux через cgroups разрешает использовать только 2 CPU.
Результат — катастрофа:
— Троттлинг CPU: ядро останавливает контейнер до конца периода (заморозка на 100 мс)
— Пики задержки: задержка P99 вырастает в десятки раз
— Проблемы GC: сборщик мусора запускает слишком много воркеров → мгновенный троттлинг
— Переключение контекста: ядро тратит процессорное время на переключение между сотнями потоков
Реальный случай из прода: под с лимитом CPU = 3, фактическое использование ~1 ядро, но троттлинг 40%.
Go 1.25 решает эту проблему автоматически. Рантайм научился читать лимиты CPU из cgroup и устанавливать правильный GOMAXPROCS без вашего участия. В некоторых случаях это дает улучшение задержки P99 в десятки раз.
1. Что такое GOMAXPROCS и как его обычно понимают
GOMAXPROCS — это переменная, которая определяет доступный рантайму Go уровень
распараллеливания. Технически: максимальное число потоков ОС, которые могут
одновременно выполнять Go-код.
Пример: если GOMAXPROCS=8 и у вас 1000 горутин, готовых к выполнению, Go будет использовать 8 потоков для запуска 8 горутин одновременно. Когда горутина блокируется, Go переключает тот же поток на другую горутину.
С Go 1.5 по Go 1.24 значение по умолчанию было простым: GOMAXPROCS = число логических ядер CPU на машине.
Это отлично работало на bare metal и в виртуальных машинах. Если у вас 8-ядерный сервер, Go видел 8 ядер и устанавливал GOMAXPROCS=8. Логично.
Но в контейнерах эта логика ломается.
Пример на коде:
go// Bare metal: нода с 8 ядрамиfunc main() { fmt.Println(runtime.GOMAXPROCS(0)) // → 8 ✓}// Контейнер: лимит CPU=2, но нода имеет 128 ядерfunc main() { fmt.Println(runtime.GOMAXPROCS(0)) // → 128 ✗}
Go использует runtime.NumCPU(), который возвращает число ядер на хосте, а не в контейнере.
Контейнеры не скрывают CPU от процесса. Внутри контейнера /proc/cpuinfo
показывает все 128 ядер хоста . Но cgroups ограничивают, сколько процессорного времени можно использовать за период — это ограничение пропускной способности, а не видимости.
2. Почему контейнерные лимиты ломали интуицию
2.1. Как работают CPU limits в Kubernetes
Когда вы пишете в спецификации пода:
yamlresources: limits: cpu: "2"
Kubernetes транслирует это в Linux cgroup CPU bandwidth limit. Есть два ключевых параметра:
-
cpu.cfs_period_us— период времени (обычно 100 000 микросекунд = 100 мс) -
cpu.cfs_quota_us— сколько процессорного времени можно использовать за период
Для CPU limit = 2:
|
quota = 2 × 100,000 = 200,000 микросекунд (200 мс) period = 100,000 микросекунд (100 мс) |
Это значит: контейнер может использовать 200 мс процессорного времени за каждые 100 мс реального времени.
Важно понимать, чем лимит CPU отличается от GOMAXPROCS. GOMAXPROCS ограничивает параллелизм — это максимум потоков, работающих одновременно. Лимит CPU ограничивает пропускную способность — общее процессорное время за период. Он не запрещает использовать много потоков параллельно, главное не превысить квоту за период.

2.2. Что такое throttling (Троттлинг)
Когда процесс пытается использовать больше процессорного времени, чем разрешено квотой за период, ядро применяет троттлинг.
Троттлинг — это грубый механизм ограничения: ядро не замедляет процесс плавно, а полностью останавливает выполнение всех потоков в контейнере до конца текущего периода.
Период обычно 100 мс. Значит заморозка может длиться до 100 мс. Это не «мягкое» замедление.
Это жесткая остановка: все потоки сняты с выполнения, приложение заморожено, запросы не обрабатываются.
Результат: значительное влияние на tail latency. Задержка P99 может вырасти многократно.
2.3. Четыре конкретные проблемы до Go 1.25
Эти проблемы детально описаны в GitHub Proposal #73193 от Michael Pratt (автор фичи).
CPU throttling. При GOMAXPROCS=64 и лимите CPU = 2 Go создает 64 потока ОС. Все они конкурируют за процессорное время. Даже если приложение не сильно нагружено, эти потоки существуют и планировщик переключается между ними. Каждое переключение потребляет процессорное время — 64 потока на 2 CPU означают, что ядро постоянно делает round-robin. Квота 200 мс заканчивается быстро, все потоки замораживаются до следующего периода.
GC спайки. Сборщик мусора Go ориентируется на GOMAXPROCS при выборе числа воркеров — цель 25%. При GOMAXPROCS=64 это 16 воркеров:
→ Старт GC: 16 воркеров + горутины приложения
→ Квота мгновенно превышена
→ Троттлинг
Хуже того: в какой-то момент может получиться, что все выполняющиеся потоки — это воркеры GC, а горутины приложения вообще не работают. Формально GC параллельный, но эффект похож на остановку мира.
Накладные расходы рантайма. GOMAXPROCS=64 вместо 2 — это не просто разные цифры. Рантайм держит внутренние кеши для каждого логического процессора: при 64 их в 32 раза больше, чем реально нужно. Плюс синхронизация и балансировка работы между 64 внутренними очередями вместо 2. Эти расходы оправданы, если у вас действительно 64 CPU. Если квота только на 2 — вы несете их вхолостую.
Переключения контекста. 64 потока на 2 CPU означают постоянные переключения: сохранение и восстановление регистров, очистка буфера TLB, инвалидация кешей. В проде оверхед переключения контекста съедал 20-30% от общего бюджета CPU. Дополнительные потоки не ускоряли работу — они ее замедляли.
2.4. Production данные
Проблема подробно описана в бенчмарках и разборах случаев из прода. Типичная картина: троттлинг CPU 40-70% времени, задержка P99 в сотни миллисекунд при нормальном P50, десятки тысяч переключений контекста в секунду — и при этом метрики использования CPU выглядят совершенно нормально (30-50%). После исправления GOMAXPROCS троттлинг падает ниже 5%, задержка P99 улучшается в 10-25 раз, переключения контекста снижаются в 15-20 раз. Использование CPU остается тем же.
Механизм прост, но контринтуитивен. При одинаковом общем использовании CPU троттлинг зависит от скорости расходования квоты.
Представьте два процесса, оба используют 200 мс процессорного времени за период 100 мс. Процесс A с GOMAXPROCS=64 запускает все 64 потока параллельно — квота 200 мс расходуется за 10-20 мс реального времени, остальные 80-90 мс приложение заморожено. Процесс B с GOMAXPROCS=2 расходует ту же квоту равномерно за ~100 мс — троттлинг минимален или отсутствует. Метрики использования CPU показывают одинаковые ~50% в обоих случаях.
Вот почему сервисы с «нормальным» использованием CPU в Prometheus всё равно испытывают пики задержки и троттлинг. Метрики утилизации не показывают главного — когда именно этот CPU использовался внутри периода.
Формула для проверки троттлинга:
bash# Читаем из cgroupcat /sys/fs/cgroup/cpu.stat# Считаем процентthrottling % = nr_throttled / nr_periods × 100
Где:
-
nr_throttled— число периодов с троттлингом -
nr_periods— общее число периодов
При одинаковом общем использовании CPU разница в троттлинге может достигать порядков величины просто из-за числа потоков. В задокументированных случаях: от 13% (64 потока) до 0,0005% (3 потока) при идентичном использовании CPU — разница в 26 000 раз.
Детальные бенчмарки и описание setup из прода:
3. Что изменилось в Go 1.25
Теперь, когда понятна суть проблемы, посмотрим что изменилось в Go 1.25.
3.1. Container-aware GOMAXPROCS
Go 1.25 приносит фундаментальное изменение: рантайм научился читать лимиты CPU из Linux cgroup.
Официальная цитата из Go Blog:
«If a Go process is running inside a container with a CPU limit, GOMAXPROCS will default to the CPU limit if it is less than the core count.»
Перевод:
«Если Go-процесс выполняется внутри контейнера с лимитом CPU, GOMAXPROCS по умолчанию будет равен лимиту CPU, если он меньше числа ядер.»
Поддержка:
-
Cgroup v1: читает
cpu.cfs_quota_us / cpu.cfs_period_us -
Cgroup v2: читает
cpu.max -
Mixed mode: поддерживает mixed v1/v2 controllers
Если переменная окружения GOMAXPROCS установлена — автоматика не включается, используется значение из переменной.
Если вы вызываете runtime.GOMAXPROCS(n) в коде — вы перезаписываете уже установленное автоматикой значение (автоматика отработала до вызова main()).
3.2. Как это работает
Рантайм Go 1.25 при старте приложения:
1. Проверяет: установлен ли GOMAXPROCS через переменную окружения
— Если да → не трогает, использует значение из переменной
— Если нет → идет дальше
2. Читает лимит CPU из cgroup:
— cgroup v1: cpu.cfs_quota_us / cpu.cfs_period_us
— cgroup v2: cpu.max
3. Если лимит найден:
— Округляет вверх до целого числа (2.5 → 3)
— Применяет минимум = 2
— Устанавливает GOMAXPROCS = это значение
Примеры:
|
Лимит CPU |
GOMAXPROCS |
Почему |
|
2 |
2 |
Прямое соответствие |
|
2.5 |
3 |
Округление вверх |
|
0.5 |
2 |
Применен минимум |
|
8 |
8 |
В пределах доступных ядер |
|
не задан |
= ядер на хосте |
Автоматика не сработала |
Почему округление вверх?
Если лимит CPU = 2,5, округление вниз до 2 означает, что 0,5 CPU из доступных остаются неиспользованными. Округление до 3 позволяет полностью утилизировать квоту.
Пример: квота = 250 мс за период 100 мс. С GOMAXPROCS=2 можно использовать максимум 200 мс. С GOMAXPROCS=3 можно использовать все 250 мс.
Почему минимум GOMAXPROCS = 2?
Три причины:
1. GOMAXPROCS=1 отключает параллелизм. Планировщик Go не может запустить горутины параллельно. Это создает странные эффекты в поведении приложения.
2. Воркеры GC создают «паузы». С одним потоком рантайм вынужден переключаться между горутинами приложения и воркером GC. Это создает эффект, похожий на «остановку мира» (stop the world), хотя формально GC работает параллельно.
3. Квота CPU <1 означает неравномерную нагрузку. Если у вас квота меньше одного CPU, ваша рабочая нагрузка по определению неравномерна (иначе вы бы постоянно упирались в лимит). Значит можно использовать всплески для оверхеда рантайма.
Исключение: если число логических CPU или CPU с affinity = 1, тогда GOMAXPROCS=1 (нет выбора).
3.3. Периодическое обновление
Рантайм Go 1.25 не просто читает лимиты CPU при запуске. Он периодически обновляет GOMAXPROCS.
Рантайм время от времени читает актуальные лимиты. Минимальный период обновления составляет 30 секунд, максимальный – 1 минуту.
Зачем? Kubernetes поддерживает изменение лимитов CPU на лету без перезапуска пода (in-place vertical scaling — постепенно становится стабильной фичей). Go 1.25 подстраивается автоматически.
Обновляется не только квота cgroup, но и маска CPU affinity. Если affinity изменился, GOMAXPROCS тоже обновится.
3.4. Новый API
Добавлена новая функция в runtime:
go// SetDefaultGOMAXPROCS updates GOMAXPROCS to runtime default,// ignoring GOMAXPROCS environment variablefunc SetDefaultGOMAXPROCS()
Пригодится, если вы вручную меняли GOMAXPROCS и хотите вернуть автоматическое значение, или если лимит CPU изменился и вы хотите обновить GOMAXPROCS принудительно, не дожидаясь периодического обновления.
GODEBUG для отката:
Если что-то пошло не так, можно откатиться к старому поведению:
|
export GODEBUG=cgroupgomaxprocs=0 |
Это полностью отключает container-aware логику. GOMAXPROCS вернется к числу CPUs на хосте.
GODEBUG совместим с версионированием языка: если в go.mod указана версия Go < 1.25, новое поведение не включается автоматически. Только при обновлении language version.
4. Как это влияет на throughput и latency
4.1. Устранение throttling
Go 1.25 устраняет фундаментальное несоответствие: до этого GOMAXPROCS отражал число ядер на хосте, а квота CPU ограничивала использование контейнера.
Механизм простой: меньше потоков — квота расходуется медленнее, троттлинг случается реже. Вместо десятков потоков, конкурирующих за квоту, рантайм создает ровно столько, сколько может эффективно использовать.
В проде это выглядит так: троттлинг падает на порядки, задержка P99 улучшается драматически, переключения контекста снижаются многократно — при том же использовании CPU.
Небольшой остаточный троттлинг остается из-за системных вызовов и пиков нагрузки — это нормально.
4.2. GC больше не создает спайки
Помимо устранения базового троттлинга, Go 1.25 решает еще одну проблему: сборщик мусора перестал создавать пики использования CPU.
Раньше при GOMAXPROCS=128 и лимите CPU = 2 GC целился в 32 воркера — 25% от 128. В момент старта сборки 32 воркера плюс горутины приложения моментально съедали квоту. Троттлинг был практически гарантирован при каждом цикле GC.
Теперь GOMAXPROCS=2, и цель GC — 0,5 воркера. Рантайм округляет это до 1 воркера, когда сборка нужна. Никакого пика, квота не превышается.
Простаивающие воркеры тоже перестали быть проблемой: рантайм управляет 2 внутренними очередями горутин, а не 128.
4.3. Трейдофф: неравномерная нагрузка
Есть один нестандартный сценарий, который стоит понимать.
Лимит CPU ограничивает общее процессорное время за период, но не число потоков одновременно. Можно использовать много ядер параллельно короткое время — главное не превысить квоту. GOMAXPROCS работает иначе: он жестко ограничивает параллелизм, даже если в квоте есть запас.
Абстрактный пример из GitHub Proposal:
Условия:
-
Машина: 10 логических CPU
-
Cgroup: квота = 200 мс, период = 100 мс (в среднем 2 CPU)
-
Рабочая нагрузка: 1 запрос каждые 100 мс, каждый требует 50 мс процессорного времени, идеально распараллеливается
БЕЗ Go 1.25 (GOMAXPROCS=10): Приходит запрос, работа на 50 мс распределяется на 10 горутин. Каждая горутина работает 5 мс. Задержка: 5 мс. Использовано процессорного времени: 50 мс (в пределах квоты 200 мс).
С Go 1.25 (GOMAXPROCS=2):
Приходит запрос, работа на 50 мс распределяется на 2 горутины. Каждая горутина работает 25 мс. Задержка: 25 мс. Использовано процессорного времени: 50 мс (все еще в пределах квоты). Задержка ухудшилась в 5 раз (5 мс → 25 мс), хотя троттлинг не происходит в обоих случаях.
Почему? Рабочая нагрузка сильно неравномерна. Приложение простаивает 50 мс из каждых 100 мс. Это создает запас: можно использовать >2 CPU параллельно в моменты всплесков, не превышая в среднем 2 CPU за период.
GOMAXPROCS=2 запрещает эти всплески, ограничивая параллелизм.
Цитата из Go Blog:
«Particularly spiky workloads may see a latency increase from this change due to GOMAXPROCS preventing short-lived spikes of additional threads beyond the CPU limit average.»
Перевод:
“Особенно неравномерные рабочие нагрузки могут столкнуться с ростом задержки из-за этого изменения, так как GOMAXPROCS предотвращает кратковременные всплески дополнительных потоков сверх среднего лимита CPU.”
Для большинства рабочих нагрузок это несущественно — устранение троттлинга перевешивает. Но если приложение большую часть времени простаивает, задержка отдельных запросов критична, а лимит CPU заметно выше среднего использования — стоит измерить задержку до и после. Возможно, в этом случае имеет смысл задать GOMAXPROCS вручную выше лимита CPU.
5. Когда все равно нужно задавать GOMAXPROCS вручную
Есть три основных случая, когда автоматика Go 1.25 не работает или не оптимальна.
5.1. Только CPU request (без limit)
yamlresources: requests: cpu: 2 # limits НЕТ!
CPU request в Kubernetes транслируется в cpu.shares (cgroup v1) или cpu.weight (cgroup v2). Это не квота, а относительный приоритет для планировщика. Go 1.25 его игнорирует — автоматика работает только с лимитами CPU.
yamlresources: limits: cpu: "2" # ✓ Go 1.25 использует это requests: cpu: "1" # ✗ Go игнорирует
Почему именно так: request — это минимальная гарантия, не верхний предел. Под может использовать больше, если на ноде есть простаивающие CPU. Если бы Go установил GOMAXPROCS = CPU request, вы бы не могли использовать эти простаивающие ресурсы. А ставить GOMAXPROCS > request — непонятно насколько: 2x? 4x? Четкого ответа нет.
Два варианта решения:
-
Добавить CPU limit (рекомендуется):
yamlresources: requests: cpu: 2 limits: cpu: 4
-
Go 1.25+: рантайм установит GOMAXPROCS автоматически
-
Go < 1.25: добавьте uber-go/automaxprocs — он сделает то же самое, но требует явного лимита
2.Установить GOMAXPROCS вручную через environment variable:
yamlenv: - name: GOMAXPROCS value: "2"
5.2. Неравномерная нагрузка
Как обсуждалось в разделе 4.3: если рабочая нагрузка сильно неравномерна и вы хотите использовать параллелизм всплесков, может быть выгоднее GOMAXPROCS > CPU limit.
Это имеет смысл рассматривать, когда среднее использование CPU заметно ниже лимита, нагрузка поступает всплесками — например, пакетная обработка — и задержка отдельных запросов критична.
Если всё это про вас: замерьте базовое состояние с автоматическим GOMAXPROCS, потом установите GOMAXPROCS = лимит × 1.5 или лимит × 2 и сравните задержку, троттлинг и пропускную способность. Если задержка улучшилась, а троттлинг остался ниже 10% — можно оставить. Трейдофф прямой: меньше задержка при всплесках, чуть больше троттлинга в остальное время.
5.3. Non-Linux окружения
Go 1.25 container-aware GOMAXPROCS работает только на Linux.
На Windows, macOS, BSD — автоматика не включается. GOMAXPROCS остается = число CPU на машине.
Если вы разрабатываете на macOS, но деплоите в Linux-контейнерах:
-
Локально:
GOMAXPROCSбудет = ядер на Mac (нормально для разработки) -
Продакшен (Linux): автоматика сработает
Проблема только если вы деплоите на Windows-контейнерах или других non-Linux платформах с лимитами CPU. Тогда нужно вручную:
yamlenv: - name: GOMAXPROCS value: "2"
6. Что проверить в Kubernetes
6.1. Проверить текущие лимиты CPU
Нужно узнать, какие лимиты установлены у ваших Go-сервисов.
bashkubectl get pods -n your-namespace \ -o custom-columns=POD:.metadata.name,CPU_LIMIT:.spec.containers[*].resources.limits.cpu
Обратите внимание:
-
Поды без лимита CPU → автоматика Go 1.25 не сработает
-
Дробные лимиты (500m, 1,5) → будут округлены вверх
6.2. Проверить троттлинг (до миграции на Go 1.25)
Если ваши сервисы уже запущены на Go 1.24 или ниже, проверьте текущий троттлинг.
bashkubectl exec -it your-pod -- cat /sys/fs/cgroup/cpu.stat
Ключевые метрики:
|
nr_periods 309 nr_throttled 164 |
Формула
|
throttling % = (nr_throttled / nr_periods) × 100 = (164 / 309) × 100 = 53% |
Троттлинг 53% — это очень плохо. Более половины времени приложение заморожено.
6.3. Проверить GOMAXPROCS в логах
Добавьте логирование GOMAXPROCS при старте:
gofunc main() { log.Printf("GOMAXPROCS=%d", runtime.GOMAXPROCS(0)) // ...}
После деплоя проверьте:
kubectl logs your-pod | grep GOMAXPROCS
должно быть: GOMAXPROCS=2 (если лимит CPU = 2)
6.4. Настройте мониторинг
После обновления на Go 1.25 отслеживайте ключевые метрики.
Prometheus-алерты:
Настройте два критичных алерта:
-
Несоответствие GOMAXPROCS — когда GOMAXPROCS значительно превышает лимит CPU:
|
# Используйте go_sched_gomaxprocs_threads из метрик рантайма или go_gomaxprocs > (container_spec_cpu_quota / container_spec_cpu_period) × 2 |
-
Высокий троттлинг — когда троттлинг >25% в течение 10 минут:
|
rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m]) > 0.25 |
Grafana дашборд:
Добавьте три панели для мониторинга:
-
Соотношение GOMAXPROCS к лимиту CPU (должно быть ~1.0):
|
go_gomaxprocs / (container_spec_cpu_quota / container_spec_cpu_period) |
-
Процент троттлинга (должно быть <5%):
|
rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m]) × 100 |
-
Переключения контекста (должно значительно снизиться):
|
rate(container_context_switches_total[5m]) |
Эти метрики покажут, работает ли автоматика Go 1.25 правильно.
7. Практический чек-лист для migration review
Перед апгрейдом на Go 1.25
Инвентаризация лимитов CPU
Найдите поды без лимитов CPU (см. команды в разделе 6.1).
Сервисы без лимита CPU — автоматика не сработает. Решите: добавить лимит или задать GOMAXPROCS вручную.
Базовые метрики
Соберите текущее состояние для сравнения после миграции:
-
Задержка: P50, P99, P99.9 из APM (Prometheus, Datadog, и т.д.)
-
Троттлинг: используйте команды из раздела 6.2
-
Переключения контекста: cat /proc/self/status | grep nonvoluntary_ctxt_switches
-
RPS / Пропускная способность: текущая нагрузка
Проверить ручные настройки GOMAXPROCS
bash# Поиск в deployment YAMLgrep -r "GOMAXPROCS" kubernetes/deployments/# Поиск в кодеgrep -r "runtime.GOMAXPROCS" cmd/ pkg/
Если найдены явные настройки — они отключат автоматику. Решите:
-
Убрать их (рекомендуется для большинства случаев)
-
Оставить только для специфичных случаев (неравномерная нагрузка)
После апгрейда на Go 1.25
Проверить GOMAXPROCS в логах
См. раздел 6.3 — добавьте логирование при старте.
Проверьте: GOMAXPROCS должен равняться лимиту CPU.
Сравнить троттлинг
Используйте команды из раздела 6.2.
Цель: троттлинг < 5% (раньше было 40-70%).
Если раньше был 40-70%, а стал 2-5% — отлично, автоматика работает.
Сравнить задержку
Проверьте APM-дашборды:
-
Задержка P99 должна значительно улучшиться (в случаях из прода: в 4-25 раз)
-
Задержка P50 может немного улучшиться или остаться стабильной
Если задержка ухудшилась — см. раздел «Если есть проблемы» ниже.
Проверить переключения контекста
bashcat /proc/self/status | grep nonvoluntary_ctxt_switches
Должно значительно снизиться (обычно на порядок).
Нагрузочный тест
Прогоните типичный нагрузочный тест:
-
RPS должен остаться стабильным или улучшиться
-
Использование CPU может немного снизиться (меньше оверхеда)
-
Задержка должна быть лучше или равна
Если есть проблемы
Неравномерная нагрузка с ухудшением задержки
Если задержка выросла (например, P99: 10 мс → 30 мс), троттлинг остался низким (<5%), а использование CPU заметно ниже лимита — скорее всего, нагрузка неравномерна и Go 1.25 ограничивает параллелизм всплесков. Установите GOMAXPROCS > лимита CPU вручную (см. раздел 5.2). Задержка должна улучшиться, троттлинг может немного вырасти — если перевалит за 25%, снижайте GOMAXPROCS.
Нет лимита CPU
Если GOMAXPROCS равен числу ядер на ноде и троттлинг высокий или нестабильный — добавьте лимит CPU в спецификацию пода (см. раздел 5.1) и передеплойте. Go 1.25 установит правильный GOMAXPROCS автоматически.
Non-Linux платформа
Если деплоите на Windows-контейнерах или других non-Linux платформах: установите GOMAXPROCS вручную через переменную окружения (автоматика работает только на Linux).
Откат (если критично)
Если после апгрейда на Go 1.25 возникли критические проблемы в проде (например, задержка ухудшилась на 50%+):
Быстрый откат:
yamlenv: - name: GODEBUG value: "cgroupgomaxprocs=0"
Повторный деплой вернет старое поведение. После этого стоит замерить задержку, троттлинг и использование CPU, проверить равномерность нагрузки и при необходимости задать GOMAXPROCS вручную.
Долгосрочно: откат не решает проблему троттлинга, он только возвращает к статус кво.
Вывод
Go 1.25 — это «Container-Native Release». Годами Go-сервисы в Kubernetes вели себя странно: использование CPU нормальное, но задержка через крышу, троттлинг 40-70%, переключения контекста — десятки тысяч в секунду. Причина простая — GOMAXPROCS устанавливался в число ядер на ноде, хотя контейнер имел квоту только на 2-4 CPU.
Go 1.25 решает это автоматически. Рантайм читает лимиты CPU из cgroup и устанавливает GOMAXPROCS = ceil(limit) с минимумом 2. Для большинства сервисов в Kubernetes это бесплатное улучшение: троттлинг падает на порядки, задержка P99 улучшается многократно, переключения контекста снижаются значительно. Просто обновляете Go 1.25 в go.mod → build → deploy → работает.
Нестандартные сценарии требуют внимания:
-
CPU request без лимита — автоматика не работает. Добавьте лимит или задайте GOMAXPROCS вручную.
-
Неравномерная нагрузка — может пострадать от ограничения параллелизма всплесков. Измерьте задержку.
-
Non-Linux — автоматика только на Linux. Windows/macOS-контейнеры требуют ручной настройки.
Перед деплоем достаточно сделать инвентаризацию лимитов CPU, собрать базовые метрики и после обновления сверить GOMAXPROCS в логах с лимитом в манифесте.
Ресурсы:
-
Go 1.25 Release Notes — официальная документация
-
Go Blog: Container-aware GOMAXPROCS — детальное объяснение от авторов
-
GitHub Proposal #73193 — technical proposal от Michael Pratt
-
uber-go/automaxprocs — библиотека, решавшая ту же проблему до Go 1.25
ссылка на оригинал статьи https://habr.com/ru/articles/1064568/