cgroupv2について理解する

前回はcgroupv1の仕組みついては書いたが、今回はLinuxカーネル4.5以降で道入されているcgroup v2について触れる。

blog.ty-tbs.com

cgroupはLinuxリソースを制限したり管理するための機能であり、cgroupfsというファイルシステムを経由して操作管理することができる。

cgroup v1の欠点、v2での改善

cgroup v1の問題点がありcgroup v2より全体の仕様が再設計された。そのうちいくつかを紹介する。

複数階層による管理

cgroup v1は複数の階層から構成される設計となっている。この設計はリソース管理の柔軟性を向上させるための仕組みとして意図された。

Red Hat Enterprise Linux 6 リソース管理ガイドより引用

しかし上記のルールのように複数の階層にサブシステムを所属させることはできないので、全く別の階層で管理したい場合などは柔軟に構成できないのである。

cgroup v1ではそれぞれのサブシステムに関連付けられたツリーのコントロールグループでそれぞれリソースの設定をする。

下の画像でtaks_1を制御しようとしたときのことを考える。cpuサブシステムを扱うgroup_1を作成し、更にgroup_4にmemoryサブシステムを割り当てた。 この状態ではtask_1自体はgroup_1, group_2と2つのコントロールグループに加わることになる。

この構造ではタスクは各階層中に存在し得るため、特定のタスクがどういった制御を実施しているかわかりにくい。またcpuとmemory両方を単一の階層に紐付けた場合は、他の階層には使用したサブシステムを使用できないため、他のタスクも両コントローラの制御下に入る。

cgroupv2ではcgroup v1と同じように階層構造を形成しているが、全体で単一のツリーとなるため管理が容易である。 CPUとメモリをそれぞれ制御しようとした場合、目的のリソースを制御するためのコントロールグループを作成してタスクを追加する。 このときタスクはいずれか1つのグループにしか所属できない。

cgroup v2ではプロセスに対するリソースの制約を参照、設定したければ唯一つのグループを特定すればよいためシンプルである。

管理粒度

制御の粒度に関しても問題があった。実は前回の手順ではプロセス単位でリソースを制限したが、cgroup v1の一部のサブシステムの対象(task)はプロセスではなくスレッド単位なのである

これによって同一プロセスにも関わらず、異なるリソース制御を割り当てることが可能となりリソースの競合が発生する。さらにcpuサブシステムはスレッド単位、memoryサブシステムはプロセス単位など、一部のという点が複雑さに拍車をかけている。

cgroup v2では管理単位はスレッドではなくプロセスがデフォルトとなった

cgroup v2の動作確認

すでにcgroupfsがマウントされているデフォルトの状態できどうした。v1のようにサブシステムごとにマウントされていないことが確認できる。

$ findmnt |grep cgroup
│ ├─/sys/fs/cgroup           cgroup2          cgroup2    rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot

ルートグループ配下はこのような構成になっている。細部システムによってはルートグループではコントロールできないパラメターもあるため、コントロールグループを作成するとより多くのファイルが存在する。

$ ls /sys/fs/cgroup
cgroup.controllers      cgroup.stat             cpuset.cpus.effective  dev-mqueue.mount  io.pressure    memory.numa_stat  sys-fs-fuse-connections.mount  system.slice
cgroup.max.depth        cgroup.subtree_control  cpuset.mems.effective  init.scope        io.prio.class  memory.pressure   sys-kernel-config.mount        user.slice
cgroup.max.descendants  cgroup.threads          cpu.stat               io.cost.model     io.stat        memory.stat       sys-kernel-debug.mount
cgroup.procs            cpu.pressure            dev-hugepages.mount    io.cost.qos       limit          misc.capacity     sys-kernel-tracing.mount

試しにlimitというグループを作成して確認する。

$ sudo mkdir /sys/fs/cgroup/limit
$ ls /sys/fs/cgroup/limit
cgroup.controllers      cgroup.procs            cpu.max                cpuset.mems            io.latency     memory.current       memory.min           memory.swap.events
cgroup.events           cgroup.stat             cpu.max.burst          cpuset.mems.effective  io.max         memory.events        memory.numa_stat     memory.swap.high
cgroup.freeze           cgroup.subtree_control  cpu.pressure           cpu.stat               io.pressure    memory.events.local  memory.oom.group     memory.swap.max
cgroup.kill             cgroup.threads          cpuset.cpus            cpu.weight             io.prio.class  memory.high          memory.pressure      pids.current
cgroup.max.depth        cgroup.type             cpuset.cpus.effective  cpu.weight.nice        io.stat        memory.low           memory.stat          pids.events
cgroup.max.descendants  cpu.idle                cpuset.cpus.partition  io.bfq.weight          io.weight      memory.max           memory.swap.current  pids.max

利用可能なサブシステム(コントローラ)はcgroup.controllersを参照することで確認できる。 また該当グループで有効になっているサブシステムはcgroup.controllersで確認できる。

$ cat /sys/fs/cgroup/limit/cgroup.controllers
cpuset cpu io memory pids

$ cat /sys/fs/cgroup/cgroup.subtree_control
cpuset cpu io memory pids

先程作成したlimitグループを確認すると、初期状態では有効なサブシステムは登録されていないことがわかる。

$ cat /sys/fs/cgroup/limit/cgroup.controllers
cpuset cpu io memory pids

$ cat /sys/fs/cgroup/limit/group.subtree_control

そこでサブシステムを関連付けるためにgroup.subtree_controlに書き込みをする。 ドキュメントによるとこのような書式で登録、解除ができるように記載されている。(今回の場合親グループですでに有効になっているため本来は不要)

Space separated list of controllers prefixed with '+' or '-' can be written to enable or disable controllers. A controller name prefixed with '+' enables the controller and '-' disables. If a controller appears more than once on the list, the last one is effective. When multiple enable and disable operations are specified, either all succeed or all fail.

echo "+cpu" | sudo tee -a /sys/fs/cgroup/limit/group.subtree_control
$ echo "+cpu" | sudo tee -a /sys/fs/cgroup/limit/group.subtree_control
+cpu

さてこれでlimit内でcpuサブシステムが有効になったのでCPU時間を制限してみる。 cgroup v1とはパラメータの指定方法が変更になっているがcpu時間の考え方については変わっていない。

1スレッドのマシンの場合だと1周期100000(μs)と設定した場合その半分である50000を指定すると指定したプロセスでCPU使用量50%となる。 (対象プロセスが100%の負荷をかけている状態で、かつcgroup内に存在するプロセスが1つもしくは、他のプロセスはCPU時間を消費しないものという前提)

今回使った私のマシンの場合は16スレッドのため50000*16/2=400000を指定する。

stressコマンドで全スレッドに対して負荷をかけると概ね50%程度になり意図した通りcpu時間が制限されている。

echo "800000 100000" |sudo tee /sys/fs/cgroup/limit/cpu.max
800000 100000

$ echo $$ | sudo tee -a /sys/fs/cgroup/limit/cgroup.procs
$ stress -c 16

# 別窓で実行
$ sar 1 1 -u -P ALL

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     50.59      0.00      0.55      0.00      0.00     48.86
Average:          0     50.50      0.00      0.99      0.00      0.00     48.51
Average:          1     52.94      0.00      0.98      0.00      0.00     46.08
Average:          2     49.02      0.00      1.96      0.00      0.00     49.02
Average:          3     49.50      0.00      0.99      0.00      0.00     49.50
Average:          4     54.90      0.00      0.00      0.00      0.00     45.10
Average:          5     49.02      0.00      0.98      0.00      0.00     50.00
Average:          6     50.50      0.00      0.00      0.00      0.00     49.50
Average:          7     49.02      0.00      0.98      0.00      0.00     50.00
Average:          8     50.98      0.00      0.00      0.00      0.00     49.02
Average:          9     49.50      0.00      0.00      0.00      0.00     50.50
Average:         10     51.49      0.00      0.00      0.00      0.00     48.51
Average:         11     50.00      0.00      0.98      0.00      0.00     49.02
Average:         12     49.00      0.00      0.00      0.00      0.00     51.00
Average:         13     51.96      0.00      0.00      0.00      0.00     48.04
Average:         14     52.48      0.00      0.00      0.00      0.00     47.52
Average:         15     48.51      0.00      0.99      0.00      0.00     50.50

同じようにlimitグループに対してメモリリソースに対する制限を追加する。 group.subtree_controlへの追加は親グループでデフォルトで有効になっているため不要。

それではmemory.highへの書き込みによってメモリ使用量を制限する。

20GBのメモリを消費するようにstressを実行した。

$ echo "10g" |sudo tee /sys/fs/cgroup/limit/memory.high
10g
[yota@ubuntu01 limit]$ cat  /sys/fs/cgroup/limit/memoray.high
10737418240

# 別窓で実施
$ stress -m 1 --vm-hang 0 --vm-bytes 20G

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi        10Gi       4.0Gi       0.0Ki       738Mi       4.5Gi
Swap:          8.0Gi       8.0Gi          0B

cgorupv1では上限メモリ設定値を超えた場合OMMでkillされていましたが、cgroup v2ではリソースを有効に使い切るという観点から改善されており、cpu.high指定の容量はOOMせずに抑制しようとする。

cpu.maxの場合は最終上限として機能するため、指定すると指定容量から削減できない場合はOOMが実施される。

試しに5GBを設定した状態で先程と同じくstressを実行したところ、メモリ5GB使用後にスワップを使い切った段階でkillされた。

$ echo "5g" |sudo tee /sys/fs/cgroup/limit/memory.max
5g

$ stress -m 1 --vm-hang 0 --vm-bytes 20G
stress: info: [2502] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [2502] (415) <-- worker 2503 got signal 9
stress: WARN: [2502] (417) now reaping child worker processes
stress: FAIL: [2502] (451) failed run completed in 24s

なお他のcgroupに移動したい場合はそのままcgroup.procsに書き込めば元のグループからは自動的に削除される。

$ sudo mkdir /sys/fs/cgroup/limit2
$ echo $$ | sudo tee -a /sys/fs/cgroup/limit2/cgroup.procs

# 元のグループから消えた
$ cat  /sys/fs/cgroup/limit/cgroup.procs
$

参考・関連


cgroupについて理解する

cgroupはカーネルが提供するリソースコントロールのための仕組みである。プロセスをグループ化してCPUやメモリの使用量を制限や記録することができる。 この仕組はコンテナなどの仮想化技術のリソースマネジメントにも利用されていて大きな役割を担っている。

cgroupはカーネル4.5から導入されているv2とそれ以前に利用されていたv1に大きく分けられていて、内部のデータ構造などに大きな変更がある。この記事を書いている時点では、カーネルバージョン4.5未満を利用している環境は多く存在し、現役でv1が利用されている環境も多くある。

業務でv2を扱える環境が徐々に増えてきたこともありcgroupの基礎的な部分を中心に、バージョンによる差などをまとめる。

cgroupとは

cgroupはプロセスに割り当てるLinuxが提供するリソースを制限、監視するための仕組みである。例えば特定のアプリケーションが利用するメモリ上限を定めて、システム全体のクラッシュを防いだり、事前に取り決めたCPU時間を超えないように割り当てたりができる。さらには、特定のアプリケーションがどの程度リソースを消費したのか使用状況をカウントする事も可能である。

どのようなプロセスにどういった制限や監視をするかは、cgroupfsと呼ばれる疑似ファイルシステムを通して操作が行われる。インタフェースとし汎用的なファイルシステムを利用することによって様々なシステムやプログラムから操作が容易である。

登場する概念

タスク

プロセスのことを指す

コントロールグループ

ユーザが制御したい何かしらの基準によって分割されたプロセスのグループのこと。cgroupの世界ではリソースの制御はこのコントロールグループを単位として適用する。プロセスは何かしらのコントロールグループに所属する。コントロールグループは階層構造で構成し管理される。

サブシステム

Linuxリソース(CPUやメモリなど様々)に対する操作を実現するための手続きを抽象化したコントローラ。サブシステムをコントロールグループに接続することによって、コントロールグループがサブシステムによって制御される。各サブシステムの趣旨やパラメータの与え方については各カーネルバージョンのドキュメントを参照する。

  • cpu
  • cpuset
  • memory
  • blkio
  • devices
  • freezer
  • hugetlb
  • pids

上記の概念にはいくつかのルールが存在する。

1 単一階層には、単一または複数のサブシステムを接続することができる

Red Hat Enterprise Linux 6 リソース管理ガイドより引用

画像の例ではcpuサブシステムとmeoryサブシステムが関連付けられている。

2 すでに階層を割り当てているサブシステムは他の階層を割り当てることはできない

Red Hat Enterprise Linux 6 リソース管理ガイドより引用

すでに①でcpuサブシステムが、②でmemoryサブシステムの関連付けが行われているため、③の割当をすることはできない。

3 タスクは異なる階層の複数のcgoupのメンバになることが可能

Red Hat Enterprise Linux 6 リソース管理ガイドより引用

タスクは複数の階層のコントロールグループのメンバになることができる。しかし同一の階層の/cg1と/cg2のメンバになることは許されていない。 実際/cg1所属状態で/cg2に所属させる操作をした場合は、/cg1から/cg2への移動となる。 これは/cg1でCPU時間を全体の50%に制限していた場合、/cg2では異なる割合で制限をしており、設定値に矛盾が生じるためである。

4 プロセスがフォークした場合子プロセスは親のcgroupを継承する

Red Hat Enterprise Linux 6 リソース管理ガイドより引用

管理対象のプロセスが子プロセスを作成した場合、子プロセスは自動的にその親のコントロールグループのメンバーとなる。 この自動的に継承したコントロールグループから移動することも可能。

cgroup v1の動作確認

起動時にsystemdによって/sys/fs/cgroupにcgroupfsがマウントされた状態で起動する。このときいくつかのサブシステムが/sys/fs/cgroup配下にマウントされている事がわかる。 この状態は先ほほどの図でいうと、各サブシステムがそれぞれのhierarchyに接続されている状態である。

$ findmnt |grep cgroup
│ ├─/sys/fs/cgroup                    tmpfs      tmpfs      ro,nosuid,nodev,noexec,seclabel,mode=755
│ │ ├─/sys/fs/cgroup/systemd          cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
│ │ ├─/sys/fs/cgroup/cpu,cpuacct      cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu
│ │ ├─/sys/fs/cgroup/cpuset           cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,cpuset
│ │ ├─/sys/fs/cgroup/perf_event       cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,perf_event
│ │ ├─/sys/fs/cgroup/memory           cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,memory
│ │ ├─/sys/fs/cgroup/freezer          cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,freezer
│ │ ├─/sys/fs/cgroup/devices          cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,devices
│ │ ├─/sys/fs/cgroup/pids             cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,pids
│ │ ├─/sys/fs/cgroup/net_cls,net_prio cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls
│ │ ├─/sys/fs/cgroup/hugetlb          cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb
│ │ └─/sys/fs/cgroup/blkio            cgroup     cgroup     rw,nosuid,nodev,noexec,relatime,seclabel,blkio

利用できるサブシステムの一覧を表示したい場合は/proc/cgroupsを参照する。

$ cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset  3       1       1
cpu     2       1       1
cpuacct 2       1       1
memory  5       1       1
devices 7       1       1
freezer 6       1       1
net_cls 9       1       1
blkio   11      1       1
perf_event      4       1       1
hugetlb 10      1       1
pids    8       1       1
net_prio        9       1       1

そのコントロールグループにも付けられているプロセスを参照したければtasksファイルを参照するといい。試しにcpuサブシステうが関連付けられている頂点のcgroup(root cgroup)のtasksファイルを見てみる。するとこのように多くのプロセスが表示されるはずだ。これはシステム上のプロセスは初期状態でデフォルトのcgroupのメンバとなること担っているためである。

$ cat /sys/fs/cgroup/cpu/tasks | head
1
2
3
4
5
6
7
8
9
10

それでは実際に特定のプロセスに対してCPU時間を制限してみる。 新たにcpu_limitというcgroupを作成してそこにプロセスを所属させることにする。 ディレクトリを作成すると自動的にcpuサブシステムに必要なパラメータを与えるファイルとroot cgroupと同じようにtasksなどが自動的に作成されている。

もちろんroot cgroupと違い新しく作成したcgroupにはプロセスは何も登録されていない状態である。

$ sudo  mkdir /sys/fs/cgroup/cpu/cpu_limit
$ ls /sys/fs/cgroup/cpu/cpu_limit/
cgroup.clone_children  cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  cpuacct.stat  cpuacct.usage  cpuacct.usage_percpu  notify_on_release  tasks

$ cat /sys/fs/cgroup/cpu/cpu_limit/tasks
$

そして次にcgroup内に作成されたファイルに対して実際に値を設定する。 cpu.cfs_period_usには指定したcgroup内のプロセスCPUリソースが割当される頻度をマイクロ秒単位で指定する。 cpu.cfs_quota_usには1回割り当てられたタイミングで消費することのできるCPU時間の合計を指定する。

1スレッドのマシンの場合とcpu.cfs_period_usが100000だとするとその半分である50000を指定すると指定したプロセスでCPU使用量50%となる。 (もちろん対象プロセスが100%の負荷をかけている状態で、かつcgroup内に存在するプロセスが1つもしくは、他のプロセスはCPU時間を消費しないものという前提)

回使った私のマシンの場合は16スレッドのため50000*16/2=400000を指定する。

そして対象となるプロセス(pid)はtasksに指定する。今回指定するプロセスには現在使用しているshellを指定した。 こうすることで負荷をかけるshell上で実行したプロセス(子プロセス)は親のcgroupを自動的に継承し制限の対象となるためである。

$ echo "100000" | sudo tee /sys/fs/cgroup/cpu/cpu_limit/cpu.cfs_period_us
100000
$ echo "400000" | sudo tee /sys/fs/cgroup/cpu/cpu_limit/cpu.cfs_quota_us
400000

$ echo $$ | sudo tee -a /sys/gs/cgroup/cpu/cpu_limit/tasks
1293

まずは先程作ったcgroupに所属させていないシェルからstressコマンドを使って負荷をかける。そしてsarコマンドでCPU使用率を確認するが、当然すべてのCPUスレッドで100%の使用率となっている。

$ stress -c 16

$ sar 1 1 -u -P ALL
平均値:      CPU     %user     %nice   %system   %iowait    %steal     %idle
平均値:      all    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        0    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        1    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        2    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        3    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        4    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        5    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        6    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        7    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        8    100.00      0.00      0.00      0.00      0.00      0.00
平均値:        9    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       10    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       11    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       12    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       13    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       14    100.00      0.00      0.00      0.00      0.00      0.00
平均値:       15    100.00      0.00      0.00      0.00      0.00      0.00

次にcgroupに所属させたシェルから同じようにstressを実行する。こちらも期待通りすべてのスレッドで概ね50%となっている。

$ stress -c 16

平均値:      CPU     %user     %nice   %system   %iowait    %steal     %idle
平均値:      all     50.06      0.00      0.06      0.00      0.06     49.81
平均値:        0     50.00      0.00      0.00      0.00      0.00     50.00
平均値:        1     49.00      0.00      0.00      0.00      0.00     51.00
平均値:        2     50.00      0.00      0.00      0.00      0.00     50.00
平均値:        3     49.49      0.00      0.00      0.00      0.00     50.51
平均値:        4     50.51      0.00      0.00      0.00      0.00     49.49
平均値:        5     50.50      0.00      0.00      0.00      0.00     49.50
平均値:        6     48.48      0.00      0.00      0.00      0.00     51.52
平均値:        7     49.00      0.00      0.00      0.00      0.00     51.00
平均値:        8     51.00      0.00      0.00      0.00      0.00     49.00
平均値:        9     51.00      0.00      0.00      0.00      0.00     49.00
平均値:       10     51.00      0.00      0.00      0.00      0.00     49.00
平均値:       11     50.00      0.00      0.00      0.00      0.00     50.00
平均値:       12     50.00      0.00      0.00      0.00      0.00     50.00
平均値:       13     50.50      0.00      0.00      0.00      0.00     49.50
平均値:       14     50.00      0.00      0.00      0.00      0.00     50.00
平均値:       15     50.51      0.00      0.00      0.00      0.00     49.49

またstressを実行した状態でtsaksを見るとbashの子プロセスであるstressが自動的に記載されていることがわかる。 これによって子プロセスは自動的にcgroupを継承することが確認できた。

$ cat /sys/fs/cgroup/cpu/cpu_limit/tasks
1293
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053

$ pstree -p -a 1293
bash,1293
  └─stress,2037 -c 16
      ├─stress,2038 -c 16
      ├─stress,2039 -c 16
      ├─stress,2040 -c 16
      ├─stress,2041 -c 16
      ├─stress,2042 -c 16
      ├─stress,2043 -c 16
      ├─stress,2044 -c 16
      ├─stress,2045 -c 16
      ├─stress,2046 -c 16
      ├─stress,2047 -c 16
      ├─stress,2048 -c 16
      ├─stress,2049 -c 16
      ├─stress,2050 -c 16
      ├─stress,2051 -c 16
      ├─stress,2052 -c 16
      └─stress,2053 -c 16

CPU時間の制限に加えて、メモリ使用量の制限を加えたい場合はこのようにmemoryサブシステムを利用して、おなじpidを新たなコントロールグループに所属させる。

試しにメモリ使用量を10GBに制限してみる。

$ sudo mkdir /sys/fs/cgroup/memory/memory_limit
$ echo "10g" | sudo tee /sys/fs/cgroup/memory/memory_limit/memory.limit_in_bytes

$ cat /sys/fs/cgroup/memory/memory_limit/memory.limit_in_bytes
10737418240

$ echo $$ | sudo tee -a /sys/fs/cgroup/memory/memory_limit/memory.limit_in_bytes

この状態で再びstressコマンドを起動して今度はメモリに負荷をかけてみる。

5GBを消費するようstressを起動すると普通に動くが、設定した上限である10GBを超えるように指定するとstressがOOM(sig9)され、終了していることがわかる。

# 何もしていない状態
$ free -h
              total        used        free      shared  buff/cache   available
Mem:            15G        273M         15G         16M         74M         15G
Swap:            0B          0B          0B

$ stress -m 1 --vm-hang 0 --vm-bytes 5G

$ free -h
              total        used        free      shared  buff/cache   available
Mem:            15G        5.3G         10G         16M         74M         10G
Swap:            0B          0B          0B

# 今度は15GB
$ stress -m 1 --vm-hang 0 --vm-bytes 15G
stress: info: [1653] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [1653] (415) <-- worker 1654 got signal 9
stress: WARN: [1653] (417) now reaping child worker processes
stress: FAIL: [1653] (451) failed run completed in 2s

もちろんこの状態では複数のサブシステムが関連付けられている状態のためCPUの制限も有効である。

まとめ

cgourpv1を中心に概念とその使用方法についておさらいした。 v2がリリースされしばらくたったとはいえ、cgroupv1で稼働している環境も多くあり現在は移行期といえる。

次回でcgroup v2の基本的な概念や操作方法、v1との相違点について着眼する。

参考・関連


cgroup namespaceの役割と動き

Linuxに実装されているnamepsaceはいくつかあるがその中でも比較的新しく、近年の仮想化需要に対応したものがControl group(cgroup) Namespaceである。 cgroup namespaceはカーネル4.6から導入されている。

cgroup namepsaceを分割する意味

コンテナによる仮想化ではpid namespaceやmount namespaceを利用することによってホスト環境からプロセス実行環境の仮想化を実現している。

これよって例えばコンテナ内からホスト上で実行している他のプロセスの情報を隠蔽する。

そして実行しようとするコンテナに対してCPUやメモリなどのリソースを制限しようとしたときに、利用するのがcgroupである。

以前こちらの記事でcgroupが持つ機能について触れたが、cgroupを利用すると特定のプロセス(タスク)が利用するリソース料の監視や制限をすることができ、コンテナによる仮想化でも利用されている。

しかしこれには欠点があり、cgorup namespaceが分割されていない状態ではコンテナ内からホスト側での管理情報を覗ける状態となっていた。 これを解消することができるのがcgroup namepaceというわけだ。

Dcokerを使ってcgroup namespaceの効果を確認する

まずdockerによってコンテナを作成することによってcgroup namespaceがどのように機能するかを確認してみる。 cgroup namespacecが実装されていない古いカーネルと実装済みカーネルで比較をする。

Cgorup namespace未実装カーネルでの挙動

/proc/PID/ns/を参照すると対象pidが所属するnamespaceを確認することができる。 カーネル3系のOSで試すと配下にはcgroup namespaceは存在しないことが確認できる。

$ uname -mr
3.10.0-1160.80.1.el7.x86_64 x86_64

$ ls -l /proc/self/ns/
合計 0
lrwxrwxrwx. 1 user user 0  1月 18 00:12 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 user user 0  1月 18 00:12 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 user user 0  1月 18 00:12 net -> net:[4026531956]
lrwxrwxrwx. 1 user user 0  1月 18 00:12 pid -> pid:[4026531836]
lrwxrwxrwx. 1 user user 0  1月 18 00:12 user -> user:[4026531837]
lrwxrwxrwx. 1 user user 0  1月 18 00:12 uts -> uts:[4026531838]

cgroup namespaceの存在しない3系でコンテナを立ち上げる。 そしてコンテナ内でプロセスが所属するcgroupの情報を見てみる。

その後下記のように/proc/PID/cgroupを参照してbashの所属するcgroupを確認する。 すると何やら/system.sliceを起点としたグループに所属している事が確認できる。

$ sudo docker run -d --name test-container  centos:latest  sleep 100000
a237598aa9101713ef38c89affde05ccfd6fe1c9eb424f222a6a0b86beea591d

$ sduo docker exec -it test-container bash
$ cat /proc/1/cgroup
11:pids:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
10:memory:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
9:perf_event:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
8:freezer:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
7:cpuacct,cpu:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
6:cpuset:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
5:devices:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
4:hugetlb:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
3:net_prio,net_cls:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
2:blkio:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
1:name=systemd:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope

実はこれはホスト側で見えているグループと一致している。 確認のためコンテナホスト上でcgroupの確認をする。

# ホスト側でのpidの特定
$ sudo docker inspect 3df416c8da1b|  jq .[0].State.Pid
1716

$ cat /proc/1716/cgroup
11:pids:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
10:memory:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
9:perf_event:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
8:freezer:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
7:cpuacct,cpu:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
6:cpuset:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
5:devices:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
4:hugetlb:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
3:net_prio,net_cls:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
2:blkio:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope
1:name=systemd:/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope

コンテナ内からcgourpfsのマウント情報を確認するとマウント情報を確認しても、ホスト上のcgroupfsの対照グループをマウントしているので、ホストに関する情報が見えている。

# findmnt -t cgroup
TARGET                          SOURCE                                                                                              FSTYPE OPTIONS
/sys/fs/cgroup/systemd          cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/blkio            cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/net_cls,net_prio cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/hugetlb          cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/devices          cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/cpuset           cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/cpu,cpuacct      cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/freezer          cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/perf_event       cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/memory           cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati
/sys/fs/cgroup/pids             cgroup[/system.slice/docker-3df416c8da1be95d4aaeb4752a87e0bdf6469ac249e0d17a8dfaa9d1764d1914.scope] cgroup ro,nosuid,nodev,noexec,relati

Cgroup namespace実装カーネルの挙動

/proc/PID/ns/配下にはcgroupの存在が確認できる。

$ $ ls -l /proc/self/ns/
total 0
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 mnt -> 'mnt:[4026531841]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 net -> 'net:[4026531840]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 pid -> 'pid:[4026531836]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 time -> 'time:[4026531834]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 user -> 'user:[4026531837]'
lrwxrwxrwx. 1 user user 0 Jan 18 23:59 uts -> 'uts:[4026531838]'

同じようにコンテナを作成して内部から、cgroupに関する情報を確認する。

コンテナ内ではプロセスのcgroup情報は/(ルート)となっており、ホストに関する情報は見えていない。 またマウント情報を確認しても同じようになっている。

$ sudo docker run -d --name test-container  centos:latest  sleep 100000
$ sudo docker exec -it test-container bash

$ 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

ホスト上から該当プロセスのcgorup情報を確認するとコンテナ内とは違いホスト側での所属cgroupが表示されている。

$ sudo docker inspect test-container|  jq .[0].State.Pid
1416

$ cat /proc/1416/cgroup
0::/system.slice/docker-3f141de544840800ac2aec654633947a3faf9b11c6b666f6f8a4e6b240de954f.scope

$ findmnt -t cgroup2
TARGET         SOURCE FSTYPE  OPTIONS
/sys/fs/cgroup cgroup cgroup2 ro,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot

Cgroupを手動で分割する

Dockerコンテナで確認した内容について手動でnamespaceを分割することによって確認する。

まずはログインした状態のbashプロセスが所属するcgroupを確認してみる。

$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/session-1.scope

$ ls -l  /proc/self/ns/cgroup
lrwxrwxrwx. 1 user user 0 Feb  4 06:04 /proc/self/ns/cgroup -> 'cgroup:[4026531835]'

次にunshareコマンドを利用してcgroup namespaceを分割する。

cgroup namespace分割後はrootグループに所属し、unshare実行前のグループとは異なることが確認できる。

sudo unshare --cgroup
$ cat /proc/self/cgroup
0::/

ls -l  /proc/self/ns/cgroup
lrwxrwxrwx. 1 root root 0 Feb  4 06:06 /proc/self/ns/cgroup -> 'cgroup:[4026532168]'

しかし親から見たグループは派生前の親と同一のグループに所属している。このことを確認するには、unshareしたbashプロセスのcgroup情報を別のbashプロセスから確認する。

するとunshare前のプロセスと同じグループに所属することがわかる。

# unshareでnamespaceを分割した実行したbashのpidを確認
$ echo $$
1245

# 別の窓から確認したpidのcgroupを確認する
$ cat /proc/1245/cgroup
0::/user.slice/user-1000.slice/session-1.scope

$ ls -al /proc/1245/ns/cgroup
lrwxrwxrwx. 1 root root 0 Feb  4 06:22 /proc/1245/ns/cgroup -> 'cgroup:[4026532168]'

cgroupfsのマウント情報

unshareを実行し分割したグループ内ではcgroupがrootとして表示されていることが確認できた。

しかしcgroupfsのマウント情報を確認するとルートからの相対パスで示されている。

$ findmnt -t cgroup2
TARGET         SOURCE             FSTYPE  OPTIONS
/sys/fs/cgroup cgroup2[/../../..] cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot

またmount情報はそのままのため、/sys/fs/cgroup/を参照すると 該当cgroupに所属するプロセス以外の情報も確認できる状態である。

これを解決するためにはcgroup namespaceとともにmount namespaceを分割する必要がある。

$ sudo  unshare --cgroup --mount

# mount namspaceを分割しただけではまだそのまま
$ findmnt -t cgroup2
TARGET         SOURCE             FSTYPE  OPTIONS
/sys/fs/cgroup cgroup2[/../../..] cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot

# cgroupfsを一度アンマウントし再度マウントする
$ umount /sys/fs/cgroup
$ mount -t cgroup2 cgroup2  /sys/fs/cgroup

# するとマウント情報でもルートとして見えるようになった
$ findmnt -t cgroup2
TARGET         SOURCE  FSTYPE  OPTIONS
/sys/fs/cgroup cgroup2 cgroup2 rw,relatime,seclabel,nsdelegate,memory_recursiveprot

cgroupfsの再マウントを実行した後では/sys/fs/cgroup配下を見ると他のグループに関連する情報は存在しない。

namespace内でこのように新しいグループを作成すると、親グループからも新たにグループが作成されていることが確認できる。

$ mkdir /sys/fs/cgroup/test
$ ls /sys/fs/cgroup/test
cgroup.controllers  cgroup.freeze  cgroup.max.depth        cgroup.procs  cgroup.subtree_control  cgroup.type   cpu.stat     memory.pressure
cgroup.events       cgroup.kill    cgroup.max.descendants  cgroup.stat   cgroup.threads          cpu.pressure  io.pressure

# 別窓から親のcgroupfsを確認すると新しく作成したgroupが存在
$ ls /sys/fs/cgroup/user.slice/user-1000.slice/session-1.scope/test
cgroup.controllers  cgroup.freeze  cgroup.max.depth        cgroup.procs  cgroup.subtree_control  cgroup.type   cpu.stat     memory.pressure
cgroup.events       cgroup.kill    cgroup.max.descendants  cgroup.stat   cgroup.threads          cpu.pressure  io.pressure

参考・関連