⚙️

GOMAXPROCS 自動調整が Go 1.25 でついに本格化 – 過去の課題と運用メリットを解説

に公開

はじめに

Go の並列実行上限を司る GOMAXPROCS。コンテナランタイムが普及するにつれ CPU クォータを無視してホスト全 CPU を使用してしまう 問題が顕在化し、開発・運用の現場では 手動設定外部ライブラリ (uber‑go/automaxprocs など) に頼る状況が続いてきました。Go 1.25 ではランタイムが cgroup の CPU 帯域変更を継続監視 し、GOMAXPROCS自動的かつ動的 に調整する仕組みが正式採用されます。

本記事では以下の3つのポイントを整理します。

  • これまで発生していた課題と代表的ワークアラウンド
  • Go 1.25 の新実装が内部で何を行うのか
  • 自動調整がもたらすメリットとベストプラクティス

1. これまでの課題

1‑1. CPU クォータと GOMAXPROCS の乖離

Docker / Kubernetes などのコンテナ環境では cgroup v1/v2 により CPU 時間の上限が設定されますが、Go ランタイムは長らく ホストのvCPU 数 をそのまま GOMAXPROCS のデフォルト値に採用していました。結果として

  • クォータ 0.5 CPU しか与えられていない Pod でも 8 スレッド生成 → スケジューラ競合が激増
  • CPU サイクルを奪い合い スループット低下・レイテンシ上昇 などのパフォーマンス低下を引き起こす。

といったトラブルが頻発していました。

1‑2. 代表的ワークアラウンド

上記の問題を解決するために、例えば uber‑go/automaxprocs を使って、アプリケーションの起動時に cgroup を読み取り GOMAXPROCS の値を調整していました。また、この手法では “起動時に 1 回だけ” クォータを読み取る点で共通しており、ランタイム中の動的な CPU 割り当て変化 には対応できませんでした。


2. Go 1.25 の「自動 & 動的」調整メカニズム

2‑1. 実装ハイライト

  • クロスプラットフォーム対応: Linux/macOS/Windows いずれも対応 (CPU hot‑plug も対象)

  • 定期スキャン: デフォルトで数秒おきに論理 CPU 数と cgroup CPU 帯域を再評価

  • min(limit, NumCPU) アルゴリズム: クォータが 2.3 CPU なら 3 に切り上げ (最低 2)

  • 無停止更新: runtime.procresize によりスレッドプールサイズをライブ変更し、実行中 goroutine への影響を最小化

  • オプトアウト可能:

    • 環境変数 GOMAXPROCS を明示すると自動調整は無効化
    • GODEBUG=updatemaxprocs=0,containermaxprocs=0 で手動停止

2‑2. 既存コードへの影響

従来の automaxprocs を利用していた場合は ライブラリを外しても挙動が等価 となるため移行は容易。ただし GOMAXPROCS をハードコードしているアプリは 自動調整が無効 になる点に注意してください。


3. 自動調整がもたらすメリット

  1. 運用コスト削減
    uber‑go/automaxprocs などの外部ライブラリへの依存がなくなり運用コスト削減に繋がります。
  2. 動的リソース拡張への即応
    Kubernetes の VPA / Node hot‑plug / CPUSet 再割り当て にリアルタイム順応し、リスタートせずに性能を最適化することができます。
  3. レイテンシの安定化
    OS スケジューラ競合が減り、p99/p999 のテイルレイテンシ の改善に繋がるかもしれません。

5. 実験

以下のコードを使って GOMAXPROCS が cgroup を参照し、かつ、 GOMAXPROCS が動的に変更されることを確認します。

// app/main.go
package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // 初回の状態を出力
    fmt.Printf("NumCPU:		%v", runtime.NumCPU())
    fmt.Printf("GOMAXPROCS:	%v", runtime.GOMAXPROCS(0))

    // GOMAXPROCS の動的変化を監視し、変化があれば出力
    prev := runtime.GOMAXPROCS(0)
    for range time.Tick(2 * time.Second) {
        cur := runtime.GOMAXPROCS(0)
        if cur != prev {
            fmt.Printf("Dynamic update: GOMAXPROCS => %d", cur)
            prev = cur
        }
    }
}
# ホスト 8CPU、コンテナ側 1.5 CPU 制限で実行
$ docker run -v "$(pwd)/app:/app" --cpus=1.5 golang:1.25rc1-alpine go run /app/main.go
NumCPU:        8
GOMAXPROCS:    2    # ← 1.5 を切り上げて 2

# 別ターミナルにて、動的に 4 CPU へ拡張
$ docker update --cpus=4 <container id>
# 数秒後にGOMAXPROCSが変わる
Dynamic update: GOMAXPROCS => 4

まとめ

Go 1.25 の GOMAXPROCS 自動調整 は、これまで手作業やサードパーティに委ねてきた “CPU リミット適応” をランタイム標準機能として内包します。運用負荷の削減一貫したパフォーマンス を同時に実現できる本アップデートを活用し、Go サービスのコンテナパフォーマンスを次のレベルへ引き上げましょう。

Discussion