1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンテナ環境におけるGOMAXPROCSの設定方法 – コンテナ時代のGoアプリチューニング

Last updated at Posted at 2025-07-18

概要

Go1GOMAXPROCSは、同時に実行できる最大CPU数を設定し、設定前の設定値を返すruntimeパッケージの関数です。デフォルトでは、Goのランタイムがプロセス起動時にruntime.NumCPUを使用して現在のプロセスが利用可能な論理コア数を取得し、その値が自動で設定されます2。なので、GOMAXPROCSの設定値は論理コア数と等しくなります。

GOMAXPROCSは、Go1.5以前はデフォルトで1が設定されていたのですが、1.5以降はruntime.NumCPUの値が設定されるようになりました3。異なるスレッド間で2つのゴルーチンを切り替えるコストが改善し、ハードウェアのコア数が増え続ける中で性能の向上を狙ったためです4

デフォルトの設定がそうなっているように、CPUのコア数を最大限活用するという観点からも、GOMAXPROCSの設定値は論理コア数と同程度が望ましいです。GOMAXPROCSの値が論理コア数より多いと、OSスレッド間のコンテキストスイッチが増加しスイッチングコストが掛かってしまいます5

しかし、Dockerなどのコンテナ環境ではCPU帯域幅に制限がかかっている場合があり、必ずしもGOMAXPROCSの値をruntime.NumCPUで取得される論理コア数と合わせることが最適とは限りません。
本記事では、この問題がなぜ発生するのか、そしてGOMAXPROCSの適切な設定方法について解説します。

なお、ここでいうコンテナとは、Linux上で動くLinuxコンテナと、それを動かす技術Dockerのことを指します。

動作環境

動作確認は、すべてVM上のゲストOS(Ubuntu)で行っています。

  • ホストマシン: macOS Sequoia 15.5、Apple M2 Pro チップ(10コア)搭載
  • Linux VM: OrbStack 1.11.3
  • ゲストOS: Ubuntu 24.04.2 LTS
  • Docker: 28.2.2 (build e6534b4)
  • Golang: 1.24.4-alpine3.22
  • gcc: 13

GOMAXPROCSはDockerのCPU帯域幅制限を考慮しないことの確認

--cpus=0.56オプションを指定して、CPU 帯域幅を50%に制限した状態でコンテナを起動し、その中でGOMAXPROCSの値を確認してみます。

main.go
func main() {
	fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
}
Dockerfile
FROM golang:1.24.4-alpine3.22

COPY . .

RUN go build -o app

CMD ["./app"]
$ docker build -t test001 . && docker run --rm --cpus=0.5 test001
GOMAXPROCS: 10

上記のように、CPU帯域幅制限を50%にしているにも関わらず、GOMAXPROCSの設定値は10となっています。

ちなみに概要でも述べた通り、GOMAXPROCSの設定値が実際に利用可能なCPUリソースを上回ると、過剰なスレッド生成やコンテキストスイッチの増加により、かえってパフォーマンスが低下する可能性があります。

GOMAXPROCSに設定される値はどう決まるのか

概要で述べた通り、GOMAXPROCSにはruntime.NumCPUで取得した論理コア数が設定されます。NumCPUは現在のプロセスが利用可能な論理CPUの数を返します7。そのruntime.NumCPUgetproccount関数の戻り値です。それが具体的にどのような処理を行っているのかを見ていきます。
Goのruntime.NumCPUの実コード8を抜粋すると、以下のようになっています。

src/runtime/debug.go
// NumCPU returns the number of logical CPUs usable by the current process.
//
// The set of available CPUs is checked by querying the operating system
// at process startup. Changes to operating system CPU allocation after
// process startup are not reflected.
func NumCPU() int {
	return int(ncpu)
}

関数の戻り値として、ncpuを返していると思います。
ncpusrc/runtime/runtime2.goでグローバル変数として定義されています9
ncpuを何処で設定しているかというと、osinit関数内でgetproccountの結果を設定しています10

src/runtime/os_linux.go
func osinit() {
	ncpu = getproccount()
    // 以下省略

getproccountの実コード11は以下です。

src/runtime/os_linux.go
func getproccount() int32 {
	const maxCPUs = 64 * 1024
	var buf [maxCPUs / 8]byte
	r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
    // 以下省略

sched_getaffinityシステムコールが呼ばれています。sched_getaffinityは、スレッドのCPU アフィニティマスクを取得するシステムコールです。CPUアフィニティマスクとは、スレッドが実行可能なCPUの集合を決めるものです12

CPUアフィニティマスクから算出できるのは、利用可能なCPU論理コアの数である

こちら13を参考にして、sched_getaffinityシステムコールを使用して自分自身のスレッドのCPUアフィニティマスクを取得し、実行可能なCPUの数を数えて出力するコードを書いてみました。

cpu_count.c
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    cpu_set_t set;
    CPU_ZERO(&set);

    if (sched_getaffinity(0, sizeof(set), &set) != 0) {
        return 1;
    }
    printf("number of cpus: %d\n",CPU_COUNT(&set));
    return 0;
}
Dockerfile
FROM gcc:13

COPY . .

RUN gcc -o cpu_count cpu_count.c

CMD ["./cpu_count"]

以下は、ホストマシンに論理CPUコアが10個ある環境において、VM上で実行した場合の結果です。

VMのCPUリミットを無制限に設定した場合

$ docker build -t cpu_count . && docker run --rm cpu_count
number of cpus: 10

VMのCPUリミットを無制限にした場合、結果が10になりました。これは、ホストマシンの論理コア数が10であり、VMにも同等のCPUリソースが割り当てられていると考えられます。

VMのCPUリミットを200%に設定した場合

$ docker build -t cpu_count . && docker run --rm cpu_count
number of cpus: 2

VMのCPUリミットを200%にした場合、結果が2になりました。これは、VMに2論理コア分のCPUリソースが割り当てられていると考えられます。

以上により、CPUアフィニティマスクを通じて、現在のスレッドが利用可能な論理コアの数を取得できることが確認できました。

Goのgetproccountは、システムコールsched_getaffinityで取得したスレッドのアフィニティマスクに基づいて、利用可能なCPU論理コア数を返していると思われます。(※参考: 古いGoのバージョンに関する記事ですが、こちら14も参考になります)

概要で述べたように、GOMAXPROCSの設定値は論理コア数と同程度が望ましいため、getproccountで取得した利用可能な論理コアの数(runtime.NumCPUの結果)をGOMAXPROCSに設定するのは理にかなっています。

ただし、コンテナ環境ではCPU帯域幅に制限がかけられている場合があり、それも考慮する必要があります。

uber-go/automaxprocsを使うことで、CPU帯域幅制限を考慮したGOMAXPROCSの値を設定できる

CPU帯域幅制限を考慮してGOMAXPROCSを設定してくれるライブラリとして、uber-go/automaxprocs15が存在します。
使い方は、_ "go.uber.org/automaxprocs"でimportして、initを呼ぶだけです。

main.go
import (
	_ "go.uber.org/automaxprocs"
)

func main() {
    fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))
}

以下は、automaxprocsを使用した上記のコードを実行した場合の、GOMAXPROCSの設定結果です。

コンテナのCPU帯域幅制限を50%(=0.5コア)にした場合

このとき、automaxprocsによりGOMAXPROCSは1に設定されます(GOMAXPROCSは1未満にはなりません)。

$ docker build -t test002 . && docker run --rm --cpus=0.5 test002
GOMAXPROCS: 1
2025/06/24 11:32:02 maxprocs: Updating GOMAXPROCS=1: using minimum allowed GOMAXPROCS

コンテナのCPU帯域幅制限を200%(=2コア)にした場合

この場合、automaxprocsによりGOMAXPROCSは2に設定されます。

$ docker build -t test003 . && docker run --rm --cpus=2.0 test003
GOMAXPROCS: 2
2025/06/24 11:33:10 maxprocs: Updating GOMAXPROCS=2: determined from CPU quota

なぜautomaxprocsがCPUの帯域幅制限を考慮したGOMAXPROCSを設定できるのか

Dockerの公式ドキュメント6にも記載があるように、DockerコンテナをCPU帯域幅制限付きで実行することは、Linuxカーネルの機能のひとつであるcgroupを用いて、--cpu-period--cpu-quotaを設定してコンテナ内プロセスのCPU帯域幅を制限することと等しいです。

This is the equivalent of setting --cpu-period="100000" and --cpu-quota="150000".

automaxprocsがCPUの帯域幅制限を考慮したGOMAXPROCSを設定できる理由は、実行時にcgroupの情報を参照して値を決定しているためです。

automaxprocs内部のCPUQuotaToGOMAXPROCS関数16を追っていくことで、automaxprocscgroupの情報をもとにGOMAXPROCSを算出していることが確認できます。

cgroupとは

前提として、コンテナとは簡単にいうとホスト側や他コンテナのリソースへのアクセスが制限されたプロセスです。

アクセス制限はLinuxカーネルが持つケーパビリティNamespacescgroupなどの隔離技術を使用します。これらを使用して、プロセスとホストとの分離レベルを高めています。
cgroupはプロセスやスレッドをグループ化し、そのグループごとにCPUやメモリ、I/O帯域などのリソース使用を制御・制限する仕組みです。これにより、たとえば特定のプロセスグループに割り当てるCPUコア数やメモリ容量を制限できます17

cgroupcgroupfsと呼ばれるファイルシステムを通して操作をします。多くの場合/sys/fs/cgroupsにマウントされていますが、cgroupv1とv2では階層構造や設定方法が異なります。
ここではv2を前提に説明します。

cgroupでは管理するリソースのことをサブシステムと呼び、cgroup.controllersというファイルに利用できるサブシステムが書かれています。

$ cat /sys/fs/cgroup/cgroup.controllers 
cpuset cpu io memory pids

こちら18を参考にして、試しにプロセスのCPU帯域幅制限を20%に設定して、プロセスにCPU制限がかかっていることを確認しましょう。

$ sudo mkdir /sys/fs/cgroup/test
# 50ms(期間)ごとに最大10ms(CPU時間)だけ使用できる=20%を設定
$ echo "10000 50000" | sudo tee /sys/fs/cgroup/test/cpu.max
10000 50000
$ yes > /dev/null &
[1] 87343
# 直前にバックグラウンドで起動したプロセスのPIDをcgroup.procsに書き込む
$ echo $! | sudo tee /sys/fs/cgroup/test/cgroup.procs 
87343
$ top

top - 19:57:23 up 17:01,  0 user,  load average: 0.04, 0.03, 0.01
Tasks:  22 total,   2 running,  20 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  1.4 sy,  0.7 ni, 97.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st 
MiB Mem :   3977.7 total,   3481.8 free,    510.9 used,    122.2 buff/cache     
MiB Swap:   5001.7 total,   4886.7 free,    115.0 used.   3466.8 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                       
  87343 ryosh     25   5    8288   1528   1528 R  20.3   0.0   0:34.04 yes   

PID=87343のプロセスのCPU利用率が20%に制限されていることが確認できます。

他には、2コア分に相当する帯域幅制限を与えたい場合は、こちら19を参考に以下のように設定します。

# 500ms(期間)ごとに最大1000ms(CPU時間)だけ使用できる=2コア分のCPU時間を使える
$ echo "1000000 500000" | sudo tee /sys/fs/cgroup/test/cpu.max

まとめ

  • GOMAXPROCSは、同時に実行できる最大CPU数を決めるパラメータであり、デフォルトでは実行環境の論理コア数と同じ数になっている
  • Dockerコンテナ環境においては、CPU帯域幅制限がかかっている場合があり、GOMAXPROCSはそれを考慮しない。この場合、GOMAXPROCSの設定値が実際に利用可能なCPUリソースを上回り、過剰なスレッド生成やコンテキストスイッチの増加により、かえってパフォーマンスが低下する可能性がある
  • CPU帯域幅制限を考慮してGOMAXPROCSを設定してくれるライブラリとして、uber-go/automaxprocsがある
  • DockerコンテナをCPU帯域幅制限付きで実行することは、Linuxカーネルの機能のひとつであるcgroupを用いて、コンテナ内プロセスのCPU帯域幅を制限することに等しい
  • uber-go/automaxprocsは、cgroupの情報を参照してGOMAXPROCSを設定している
  1. https://go.dev/

  2. https://pkg.go.dev/runtime@go1.24.4#GOMAXPROCS

  3. https://go.dev/doc/go1.5#runtime

  4. https://docs.google.com/document/d/1At2Ls5_fhJQ59kDK2DFVhFu3g5mATSXqqV5QrxinasI/edit?tab=t.0

  5. https://christina04.hatenablog.com/entry/why-goroutine-is-good

  6. https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler 2

  7. https://pkg.go.dev/runtime@go1.24.4#NumCPU

  8. https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/runtime/debug.go;l=42

  9. https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/runtime/runtime2.go;l=1196

  10. https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/runtime/os_linux.go;l=355-356

  11. https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/runtime/os_linux.go;l=103

  12. https://manpages.ubuntu.com/manpages/noble/en/man2/sched_getaffinity.2.html

  13. https://qiita.com/masami256/items/47163fefed7c1e337dec

  14. https://qiita.com/kubo39/items/dec96d7c93a50a310d7e#golang

  15. https://github.com/uber-go/automaxprocs

  16. https://github.com/uber-go/automaxprocs/blob/1ea14c35ce47a73089b824e504d1c92eeb61a5a6/internal/runtime/cpu_quota_linux.go#L35-L54

  17. 森田 浩平. 基礎から学ぶコンテナセキュリティ――Dockerを通して理解するコンテナの攻撃例と対策. 技術評論社, 2023, 26p, 2.3章「コンテナとLinuxカーネルの機能」

  18. cgroup v2で設定する帯域幅制限

  19. https://docs.kernel.org/scheduler/sched-bwc.html#examples

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?