概要
以前上の記事でGOMAXPROCS
はCPU数程度が適切に動くという話をしました。
これはこれで正しいのですが、一方でdockerのようにcgroupsでCPU Quotaを制限するケースではこれに当てはまらなくなります。
Kubernetesでいうと
Kubernetesのresource requests, limits - Carpe Diem
のようにlimitsを設定した場合ですね。
今回はその検証と対応方法についてまとめます。
環境
- go 1.14.2
- docker 19.03.8
CPU Quotaを制限してベンチマーク
以下の素数を算出するというCPUバウンドなプログラムを使います。
gotutorials/concprime at master · embano1/gotutorials · GitHub
PCのCPUは8コアで検証しています。
CPU CFS quota:制限なしの場合
GOMAXPROCS:指定なし(=8)
$ docker run --rm -it embano1/concprime Finding the first 10000 primes real 0m 2.32s user 0m 17.79s sys 0m 0.17s
user
は使用したCPU時間、real
は実際にかかった時間です。
CPUは8コアなので大体user / 8 = real
となっていますね。
GOMAXPROCS:1
$ docker run --rm -e GOMAXPROCS=1 -it embano1/concprime Finding the first 10000 primes real 0m 14.63s user 0m 14.41s sys 0m 0.20s
スケジューラにおけるG
, P
, M
のうち、P
が1つに制限されたため並行処理できずreal
とuser
が同値になっています。
GOMAXPROCS:2
$ docker run --rm -e GOMAXPROCS=2 -it embano1/concprime Finding the first 10000 primes real 0m 6.57s user 0m 12.83s sys 0m 0.20s
P
が2つになったので2並行まで処理可能になりuser / 2 = real
となりました。
CPU CFS quota:1の場合
次にCFS quotaで1コア分のCPU時間しか使えないようにします。
GOMAXPROCS:指定なし(=8)
$ docker run --cpus 1 --rm -it embano1/concprime Finding the first 10000 primes real 0m 24.96s user 0m 24.73s sys 0m 0.23s
real
は1コアしか無いのでuser
とほぼ同値になるのは分かりますが、user
自体が先程より長くなっています。
GOMAXPROCS:1
GOMAXPROCSを1にしてみます。
$ docker run --cpus 1 --rm -e GOMAXPROCS=1 -it embano1/concprime Finding the first 10000 primes real 0m 14.35s user 0m 14.05s sys 0m 0.19s
real
は1コアしか無いのでuser
とほぼ同値です。
一方user
が先程に比べて大きく下がりました。
GOMAXPROCS:2
$ docker run --cpus 1 --rm -e GOMAXPROCS=2 -it embano1/concprime Finding the first 10000 primes real 0m 15.38s user 0m 15.05s sys 0m 0.24s
先程より少しだけuser
が長くなりました。
GOMAXPROCS:4
$ docker run --cpus 1 --rm -e GOMAXPROCS=4 -it embano1/concprime Finding the first 10000 primes real 0m 19.53s user 0m 19.30s sys 0m 0.18s
先程よりさらにuser
が長くなりました。
CPU CFS quota:2の場合
GOMAXPROCS:指定なし(=8)
$ docker run --cpus 2 --rm -it embano1/concprime Finding the first 10000 primes real 0m 11.70s user 0m 23.12s sys 0m 0.24s
user / 2 = real
となっていますが、やはりuser
の時間が長いです。
GOMAXPROCS:1
$ docker run --cpus 2 --rm -e GOMAXPROCS=1 -it embano1/concprime Finding the first 10000 primes real 0m 15.13s user 0m 14.87s sys 0m 0.23s
2コアのCPU時間が使えますが、GOMAXPROCS
が1、つまりP
が1なので並行処理出来ずreal
はuser
と同値でした。
GOMAXPROCS:2
$ docker run --cpus 2 --rm -e GOMAXPROCS=2 -it embano1/concprime Finding the first 10000 primes real 0m 6.83s user 0m 13.30s sys 0m 0.21s
2コアのCPU時間が使え、GOMAXPROCS
が2のためuser / 2 = real
となりました。
GOMAXPROCS:4
$ docker run --cpus 2 --rm -e GOMAXPROCS=4 -it embano1/concprime Finding the first 10000 primes real 0m 8.84s user 0m 17.58s sys 0m 0.12s
user / 2 = real
ですが、user
自体は先程のGOMAXPROCS=2
よりも長くなりました。
考察
上記の検証から
- CPU CFS quotaを設定した状態で
GOMAXPROCS
が未指定(or多い)だとuser
が増えるGOMAXPROCS
分P
が増えM(=OSスレッド)
を活用するため、OSスレッドのコンテキストスイッチのコストが大きくなる
- CPU CFS quotaと
GOMAXPROCS
を合わせると良い結果になる
ということが分かります。
対応
Uberが出している以下のライブラリを使うと自動でCFSも考慮したGOMAXPROCS
を設定してくれます。
検証
README通りにblank importします。
import _ "go.uber.org/automaxprocs" func main() { // Your application logic here. }
先程の素数プログラムに↑を埋め込んだものを用意したので試してみます。
実際のコードは↓です。
CPU CFS quota:制限無し
$ docker run --rm -it jun06t/go-cfs 2020/05/19 19:23:04 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined Finding the first 10000 primes real 0m 2.60s user 0m 19.67s sys 0m 0.17s
GOMAXPROCS
が自動でCPU数の8
になりました。
CPU CFS quota:1
$ docker run --cpus 1 --rm -it jun06t/go-cfs 2020/05/19 19:23:15 maxprocs: Updating GOMAXPROCS=1: determined from CPU quota Finding the first 10000 primes real 0m 15.69s user 0m 15.36s sys 0m 0.21s
GOMAXPROCS
が自動でCPU CFS Quotaの1
になりました。
スコアも先程の検証で最も良いものと同程度です。
CPU CFS quota:2
$ docker run --cpus 2 --rm -it jun06t/go-cfs 2020/05/19 19:28:33 maxprocs: Updating GOMAXPROCS=2: determined from CPU quota Finding the first 10000 primes real 0m 7.74s user 0m 15.16s sys 0m 0.20s
GOMAXPROCS
が自動でCPU CFS Quotaの2
になりました。
スコアも先程の検証で最も良いものと同程度です。
まとめ
- CPUバウンドなサービスである
- CPU CFS Quotaを制限している(Kubernetesのlimitsを使用している等)
- ノードやインスタンスのCPU数と↑が大きく乖離している
これらの条件を満たす場合、デフォルトのGOMAXPROCS
では余計なオーバーヘッドが発生するので、automaxprocsを使うと良いでしょう。