Carpe Diem

備忘録

CPU CFS Quotaを制限している場合の適切なGOMAXPROCS

概要

christina04.hatenablog.com

以前上の記事で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つに制限されたため並行処理できずrealuserが同値になっています。

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なので並行処理出来ずrealuserと同値でした。

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が増える
  • CPU CFS quotaとGOMAXPROCSを合わせると良い結果になる

ということが分かります。

対応

Uberが出している以下のライブラリを使うと自動でCFSも考慮したGOMAXPROCSを設定してくれます。

github.com

検証

README通りにblank importします。

import _ "go.uber.org/automaxprocs"

func main() {
  // Your application logic here.
}

先程の素数プログラムに↑を埋め込んだものを用意したので試してみます。
実際のコードは↓です。

github.com

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を使うと良いでしょう。

ソース