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

関連・参照