Dockerはどのようにリソース制限しているか
Dockerを利用してコンテナを動かすと、様々なリソースをLinuxのnamespaceを利用してプロセスを独立させている。 コンテナを立ち上げる際にDockerではリソース使用量の制限を行うことができるようになっている。
例えばdocker run実行時に--memory=256mb
を指定すればメモリ使用量は256MBに制限されるし、--cpuset-cpus="0-3,15-19"
と指定すれば指定したcpu上で動作するように制限することができる。
これはLinuxのcgroupの機能を利用したもので、デフォルトではOCIに準基したruncがsystemdを介して設定をしているものになる。
今回はLinux上でDockerを利用してコンテナを動かした際に、どのようにcgroupが扱われているか確認する。
コンテナ動作時にcgroupの確認
実際にDockerでコンテナを動かしてリソースの制限をした際にcgroupがどのように機能しているか確認してみる。 今回は手持のテスト用イメージを利用したので、手元で試す際は適当なCentOSなどのイメージで必要なパッケージをインストールすると良い。
なお実行時のdocker関連のパッケージのバージョンは以下の通り。
$ rpm -qa |grep docker docker-ce-cli-20.10.22-3.el7.x86_64 docker-ce-20.10.22-3.el7.x86_64 docker-scan-plugin-0.23.0-3.el7.x86_64 docker-ce-rootless-extras-20.10.22-3.el7.x86_64
$ sudo docker run --cpuset-cpus=""0-2,4-6" --name cpu-limit -d TEST-IMAGE tail -f /dev/null
この状態で起動したコンテナ内でstressを実行してホスト上で各CPUの使用状況を確認してみると、docker run
実行時に指定したCPUの負荷のみ上昇していることが確認できる。
$ sudo docker exec -it cpu-limit bash [root@0e48eee6ab06 /]# stress -c 10 stress: info: [21] dispatching hogs: 10 cpu, 0 io, 0 vm, 0 hdd # 別窓でコンテナ中にて実行 $ sudo docker exec -it cpu-limit bash $ htop
この状態でコンテナ内からcgroupを参照すると、以下のようにdocker run実行時に指定したCPUでのみ動作するように制限されていることがわかる。
$ cat /sys/fs/cgroup/cpuset/cpuset.cpus 0-2,4-6
なお上記の挙動はcgroup v1が動作するカーネルでの実行例で、cgroup v2カーネルではこの様なパスになる
$ cat /sys/fs/cgroup/cpuset.cpus 0-2,4-6
このようにdockerに渡したリソース制限に関するオプションは、最終的にはcgroupを利用して実現している。
ホスト側の視点
ホスト側ではコンテナはいくつかのnamespaceを分離したプロセスとして見えている。
ここからはホストでマウントしているcgroupのバージョンによって挙動が異なるのでバージョンを明確にする。
下記のカーネル3系の結果を見てみる。/proc/${pid}/ns
配下を参照すると該当プロセスがどのnamepsaceに所属しているかを確認することができる。
また/proc/${pid}/cgroup
を参照すると所属cgroupを確認することができる。実行結果かからcgroupの各サブシステムにおいて/system.slice/docker-${CONTAINER_IO}.scope
に所属していることが確認できる。
カーネル 3.10
# docker runで tail -f しているのでpidofでpidを特定 $ pidof tail 1652 $ pid=1652 $ sudo -E ls -l /proc/${pid}/ns/ 合計 0 dr-x--x--x. 2 root root 0 1月 4 00:22 . dr-xr-xr-x. 9 root root 0 1月 4 00:22 .. lrwxrwxrwx. 1 root root 0 1月 4 00:24 ipc -> ipc:[4026532129] lrwxrwxrwx. 1 root root 0 1月 4 00:24 mnt -> mnt:[4026532127] lrwxrwxrwx. 1 root root 0 1月 4 00:22 net -> net:[4026532132] lrwxrwxrwx. 1 root root 0 1月 4 00:24 pid -> pid:[4026532130] lrwxrwxrwx. 1 root root 0 1月 4 00:24 user -> user:[4026531837] lrwxrwxrwx. 1 root root 0 1月 4 00:24 uts -> uts:[4026532128] $ cat /proc/${pid}/cgroup 11:cpuset:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 10:memory:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 9:devices:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 8:freezer:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 7:perf_event:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 6:pids:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 5:hugetlb:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 4:cpuacct,cpu:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 3:blkio:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 2:net_prio,net_cls:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 1:name=systemd:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope
カーネル5系ではnamesaceの種類が増えている。またcgroup v2がマウントされている状態のため、subsystemごとではなくプロセスが所属しているcgroupは1つとなっている。
カーネル 5.17
$ pidof tail 1264 $ pid=1264 $ sudo -E ls -l /proc/${pid}/ns/ total 0 lrwxrwxrwx. 1 root root 0 Jan 4 01:07 cgroup -> 'cgroup:[4026532240]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 ipc -> 'ipc:[4026532172]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 mnt -> 'mnt:[4026532170]' lrwxrwxrwx. 1 root root 0 Jan 4 00:22 net -> 'net:[4026532174]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 pid -> 'pid:[4026532173]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 pid_for_children -> 'pid:[4026532173]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 time -> 'time:[4026531834]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 time_for_children -> 'time:[4026531834]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 user -> 'user:[4026531837]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 uts -> 'uts:[4026532171]' $ cat /proc/${pid}/cgroup 0::/system.slice/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope
systemd経由のtransient cgroup
上記でホスト側からのcgroupのバージョン差によるcgroupの見え方の違いを確認したが、その時のグループはいずれも/system.slice/docker-${CONTAINER_IO}.scope
の形式となっていた。
これはsystemd経由で作成されたものである。またsystemctlによってunit fileして出力をgrepするとtransient unitとして登録されていることが確認できる。
# バージョンによってはunit名が省略して表示されるがcgroupで確認したものと同じ $ systemctl list-unit-files |grep docker docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope transient - docker.service enabled disabled docker.socket disabled enabled $ systemctl cat docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope # Warning: docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope changed on disk, the version systemd has loaded is outdated. # This output shows the current version of the unit's original fragment and drop-in files. # If fragments or drop-ins were added or removed, they are not properly reflected in this output. # Run 'systemctl daemon-reload' to reload units. # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope # This is a transient unit file, created programmatically via the systemd API. Do not edit. [Unit] Description=libcontainer container 0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a [Scope] Slice=system.slice Delegate=yes MemoryAccounting=yes CPUAccounting=yes IOAccounting=yes TasksAccounting=yes [Unit] DefaultDependencies=no # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-DevicePolicy.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] DevicePolicy=strict # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-DeviceAllow.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] DeviceAllow= DeviceAllow=char-pts rwm DeviceAllow=/dev/char/5:2 rwm DeviceAllow=/dev/char/5:1 rwm DeviceAllow=/dev/char/5:0 rwm DeviceAllow=/dev/char/1:9 rwm DeviceAllow=/dev/char/1:8 rwm DeviceAllow=/dev/char/1:7 rwm DeviceAllow=/dev/char/1:5 rwm DeviceAllow=/dev/char/1:3 rwm DeviceAllow=char-* m DeviceAllow=block-* m # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-AllowedCPUs.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] AllowedCPUs=0-2 4-6 $ cat /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-AllowedCPUs.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] AllowedCPUs=0-2 4-6
Dcokerにおけるcgroupによるリソース制御に仕組みはDcoker run
実行時に引数として渡した値が起点となっている。その後近年のバージョンではCRIインタフェースを実装しているcontainerdによって処理される。
docker daemonのexec-optsでcgroupを自分で作成するようにするか、systemdの管理とするかを選択できるようになっている。(そのためこれまでに確認した内容はあくまでデフォルトのsystemdが指定されている場合の動作) - https://docs.docker.com/engine/reference/commandline/dockerd/#options-for-the-runtime
cat /etc/docker/daemon.json | grep cgroup "exec-opts": ["native.cgroupdriver=systemd"],
ここで指定された後にcontainerdによって処理された後、低レイヤーランタイム(デフォルトではrunc)によって実際に反映されることになる。
runcに渡される際には--systemd-cgroup
オプションによってsystemdによる管理としている。
- https://github.com/opencontainers/runc/blob/v1.1.4/docs/systemd.md
containerd内部ではruncを操作する際はgo-runcというライブラリが使われている。
コードを確認するとDockerからのオプションに応じて内部で--systemd-cgroup
を渡していることが確認できる。
- https://github.com/containerd/go-runc/blob/42adae7462d417ea5178c9f8b24a55968ddada14/runc.go#L755-L757
func (r *Runc) args() (out []string) { if r.Root != "" { out = append(out, "--root", r.Root) } if r.Debug { out = append(out, "--debug") } if r.Log != "" { out = append(out, "--log", r.Log) } if r.LogFormat != bash { out = append(out, "--log-format", string(r.LogFormat)) } if r.SystemdCgroup { out = append(out, "--systemd-cgroup") } if r.Rootless != nil { // nil stands for "auto" (differs from explicit "false") out = append(out, "--rootless="+strconv.FormatBool(*r.Rootless)) } if len(r.ExtraArgs) > 0 { out = append(out, r.ExtraArgs...) } return out }
Cgroup Namespaceaによるコンテナからの見え方の差
Linuxにはいくつかのnamespaceが実装されているが、カーネル4.6から搭載されるcgroup namespaceの有無によってコンテナ内でのcgroupの見え方が異なる。
先程試した2種類のバージョンで比較する。
まずdocker execでコンテナ内からpid 1(tail -f /dev/null
)のcgroup情報を見てみる。
カーネル 3.10
/sys/fs/cgroup
を見る限りはコンテナ内では対処プロセスは/(root)
グループに所属しているように見える。
# ホスト上でcgroup v1が利用されていることを確認 $ findmnt -T /sys/fs/cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup tmpfs tmpfs ro,nosuid,nodev,noexec,seclabel,mode=755 $ sudo docker exec -it df9be6fb5e25 bash # 以下コンテナ内 # /sys/fs/cgroup/cpu配下にグループ(ディレクトリ)は存在せずtasksを確認するとコンテナ内のプロセスはすべてルートに所属している。 $ cat /sys/fs/cgroup/cpu/tasks 1 7 61
しかしコンテナ内からホストのcgroup情報を参照する手段は存在する。
コンテナ内から/proc/1/cgroup
を参照するとcgroupの各subsystemに関連づいているグループが表示される。
これは先程ホスト側で見えていたcgroupと全く同じ情報である。
$ cat /proc/1/cgroup 11:cpuset:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 10:memory:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 9:devices:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 8:freezer:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 7:perf_event:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 6:pids:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 5:hugetlb:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 4:cpuacct,cpu:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 3:blkio:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 2:net_prio,net_cls:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 1:name=systemd:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope $ findmnt -t cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup/systemd cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd /sys/fs/cgroup/net_cls,net_prio cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls /sys/fs/cgroup/blkio cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,blkio /sys/fs/cgroup/cpu,cpuacct cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu /sys/fs/cgroup/hugetlb cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,hugetlb /sys/fs/cgroup/pids cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,pids /sys/fs/cgroup/perf_event cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,perf_event /sys/fs/cgroup/freezer cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,freezer /sys/fs/cgroup/devices cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,devices /sys/fs/cgroup/memory cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,memory /sys/fs/cgroup/cpuset cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,cpuset
カーネル 5.17
こちらも3.10と同じようにすべてのプロセスはルートグループに所属している。
# ホスト上でcgroup v2が利用されていることを確認 $ findmnt -T /sys/fs/cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot $ sudo docker exec -it 0e48eee6ab06 bash # 以下コンテナ内 $ cat /sys/fs/cgroup/cgroup.procs 1 7 35
先程試した3系の結果に対してcgroup namespaceが道入されている5.17ではpid 1の所属するcgroupは/(root)
となっていてホスト側で見えていた情報と異なっている。
このようにDcokerで作成したコンテナはcgroup namespaceが分割されることによってコンテナからホスト上のcgroupに関する情報は見えないようになっている。
ホスト情報とコンテナのコンテナのリソースを分割しコンテナからホスト情報へのアクセス遮断はセキュリティ的に大きなメリットがある。
$ cat /proc/1/cgroup 0::/ $ findmnt -t cgroup2 TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup cgroup cgroup2 ro,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot
関連・参照
cgroups を使用した RHEL 7 のシステムリソースの管理 第2章 コントロールグループの使用
runc systemd cgroup driver
dockerd documentation