結論として、Dockerコンテナが消費するCPUリソースはホストマシンのcgroup
によって制御されています。
今回はあえてcgroup
の設定ファイルを直接操作することでDockerのリソース制御の仕組みを検証していきます。
簡略化のために、無限ループ処理するだけのプログラムをコンテナ内で動かした状態で検証を進めていきます。
検証に使用するコンテナの設定
FROM ruby:3.2.2-slim-bullseye
WORKDIR /usr/src/app
COPY . .
version: "3.9"
services:
ruby:
build: .
container_name: ruby
volumes:
- .:/usr/src/app
tty: true
stdin_open: true
検証
まずdocker-compose up -d
でコンテナを立ち上げた状態で、docker-compose exec ruby bash
でコンテナのシェルに入り、top
でリソースを確認してみます。
$ top
(一部省略)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 102396 23212 7328 S 0.0 0.2 0:00.15 irb
7 root 20 0 6048 3756 3316 S 0.0 0.0 0:00.01 bash
13 root 20 0 8880 3540 3072 R 0.0 0.0 0:00.00 top
当然CPUには十分空きがあります。
ではCPUリソースを消費するために、無限ループするだけの以下のプログラムを実行してみましょう。
while true
end
ruby test.rb &
でバックグラウンド実行した状態で再度top
でリソースを確認すると以下のようにtest.rb
がCPUを100%消費していることがわかります。
$ ruby test.rb &
# [1] 14
# PIDを指定して無限ループのプロセスだけ表示
$ top -p 14
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14 root 20 0 95820 16408 6936 R 100.0 0.1 0:03.11 ruby
Dockerコンテナはデフォルトでは消費できるリソースに制限がかかっていないので、このように闇雲にCPUを消費することができてしまいます。
ではcgroup
によってコンテナが消費できるCPUリソースを制限してみましょう。
まずは、当該コンテナがどのコントロールグループに属しているか、systemd-cgtop
で確認します。
Control Group Procs %CPU Memory Input/s Output/s
/ - 107.4 8.8G - -
/docker - 99.7 27.5M - -
/docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8 - 99.7 26.7M - -
/podruntime - 1.5 6.1G - -
/podruntime/docker - 1.5 6.1G - -
/volume-contents - 0.1 25.4M - -
~~~ (省略) ~~~
docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8
というコントロールグループに属しているようなので、実際にcgroup
の設定ファイルを確認しましょう。
$ cat /sys/fs/cgroup/cpu/docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8/cpu.cfs_period_us
# 100000
$ cat /sys/fs/cgroup/cpu/docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8/cpu.cfs_quota_us
# -1
以上の設定ファイルの内容の意味は、100ミリ秒あたり無制限にCPUを使えるという意味です。
(単位がマイクロ秒なので、100,000マイクロ秒 = 100ミリ秒となります。)
検証(リソース制限あり)
では、100ミリ秒あたり70ミリ秒までしか使えないように制限してみましょう。
$ echo 70000 | sudo tee /sys/fs/cgroup/cpu/docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8/cpu.cfs_quota_us
# 70000
すると以下のように、無限ループを行っているプログラムのCPU使用率が約70%となりました。
$ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14 root 20 0 95820 16408 6936 R 69.7 0.1 4:08.84 ruby
複数プロセスの場合
では無限ループプログラムをもう1つバックグラウンドで起動し、CPUコアを2つ使うようにするとどうなるでしょうか。
$ ruby test.rb &
# [2] 19
$ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14 root 20 0 95820 16408 6936 R 31.4 0.1 6:59.76 ruby
19 root 20 0 95804 16404 6936 R 38.0 0.1 0:19.69 ruby
ruby test.rb
のプロセスが2つ実行されており、合計でCPUを約70%使用しています。
3つ4つとプロセスを追加で実行しても、合計のCPU使用率は70%までに制限されます。
なぜなら、あるコンテナの中で複数のプロセスが実行されていても、Dockerホストのcgroup
によってコンテナ単位でCPUリソースが制御されるからです。
2プロセスまではCPUを100%利用できるようにするには、コンテナが100ミリ秒あたり200ミリ秒CPUを利用できるようにcpu.cfs_quota_us
に200000
を書き込みます。
$ echo 200000 | sudo tee /sys/fs/cgroup/cpu/docker/f1af8ea44304a30abe393bc4937e76de3fe8f8c749a8fea297620db709aaf3a8/c
pu.cfs_quota_us
# 200000
$ top
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14 root 20 0 95820 16408 6936 R 100.0 0.1 9:05.90 ruby
19 root 20 0 95804 16404 6936 R 100.0 0.1 2:25.83 ruby
各プロセスが100%CPUを利用できています。
100ミリ秒あたり200ミリ秒というと矛盾しているように感じますが、1プロセス100ミリ秒 * 2プロセス = 200ミリ秒と考えるとわかりやすいです。
この状態でdocker stats
でコンテナのCPU使用率を確認すると合計で約200%となっています。
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f1af8ea44304 ruby 199.52% 35.92MiB / 12.43GiB 0.28% 1.66kB / 0B 0B / 0B 3
リソース制御をDockerの仕組みを使って行う
cgroup
によるコンテナのリソース制御を理解したところで、Dockerが提供している機能で同じことをより簡単に行ってみましょう。
version: "3.9"
services:
ruby:
build: .
container_name: ruby
volumes:
- .:/usr/src/app
tty: true
stdin_open: true
cpu_quota: 70000 # この行を追加
ここまでの検証でこの意味は簡単に理解できますね。
CPUリソースの消費を100ミリ秒あたり70ミリ秒に制限するという意味です。
ではこの設定を反映した新しいコンテナを立ち上げて検証してみましょう。
$ docker-compose down
$ docker-compose up -d
$ docker-compose exec ruby bash
$ ruby test.rb &
# [1] 13
$ top -p 13
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
13 root 20 0 95812 16304 6840 R 69.7 0.1 14:36.17 ruby
cgroup
で直接操作した場合と同様にCPU使用率が約70%に制限されています。
実際にcgroup
の設定ファイルにも反映されているか確認しましょう。
$ systemd-cgtop
Control Group Procs %CPU Memory Input/s Output/s
/ - 70.9 8.8G - -
/docker - 69.8 28.2M - -
/docker/f1f9e2f477364022641ecb5520aea8a7f2447e0b615c34f73b9363d1d15dfa3a - 69.8 27.4M - -
$ cat /sys/fs/cgroup/cpu/docker/f1f9e2f477364022641ecb5520aea8a7f2447e0b615c34f73b9363d1d15dfa3a/cpu.cfs_quota_us
# 70000
cpu.cfs_quota_us
に反映されています。
つまりDockerはリソース制御をcgroup
で行っているということです。