LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第4回Linuxカーネルのコンテナ機能[3] ─cgroupとは(その2)

前回は、cgroupを使ってリソース制御を行うのに必要な基礎知識となるcgroupfsの特徴を紹介しました。

今回からは、具体的にどのようなリソースの制御が可能なのかを紹介しながら、それぞれの機能を理解しやすいように簡単な例を挙げていきたいと思います。

サブシステム

名前空間と同様に、cgroupも扱うリソースによって『サブシステム』と呼ばれる独立した機能でリソースを扱います。サブシステムには大きく分けて、数値で制限をかけるような機能と、それ以外のアクセス権やグループ内のプロセスに対する操作を行う機能に分かれます。なお、サブシステムは『コントローラ』と呼ばれることもあります。

サブシステムには以下のようなものがあります。

表1 サブシステムの種類と機能
サブシステム機能の概要実装されたバージョン
cpuCPUのスケジューリングを制御
相対配分…グループ間のCPU時間の割当を割合で指定2.6.24
帯域制御…単位時間内にグループ内のタスクが実行できる合計時間を制限3.2
cpuacctグループ内のタスクが消費するCPU時間をレポート2.6.24
cpusetグループへのCPU、メモリノードの割り当て2.6.24
deviceグループ内のタスクのデバイスへのアクセスの許可、禁止の指定2.6.26
freezerグループ内のプロセス全てを同時に一時停止・再開2.6.28
memoryグループ内のタスクが消費するメモリリソースのレポートと制限2.6.29
net_clsグループ内のプロセスが発信するパケットの制御。パケットに識別子を付与。
tcで制御2.6.29
netfilterで制御3.14
blkioブロックデバイスに対する制限
重みづけ配分…グループ間のI/Oアクセスの比率を割合で指定2.6.33
帯域制限…グループ内のタスクが各デバイスに対して行える操作数の制限2.6.37
perf_eventcgroup単位でのperfツールの使用2.6.39
net_prioグループ内のタスクのネットワークの優先度の制御3.3
hugetlbcgroupからのhugetlbの使用3.6

表1にある「実装されたバージョン」はカーネルに実装されたバージョンで、ディストリビューションによってはこれよりも古いバージョンのカーネルにバックポートされている場合もあります。また、実装された時点では階層構造がサポートされておらず、後のバージョンで実装されたものもあります。

お使いのシステムで使用可能なサブシステムは、カーネルのバージョンだけでなく、カーネルをどのような設定で作成したかによっても違います。使用可能なサブシステムは以下のように/proc/cgroupsで確認できます。

$ cat /proc/cgroups 
#subsys_name    hierarchy       num_cgroups     enabled
cpuset          0               1               1
cpu             0               1               1
cpuacct         0               1               1
memory          0               1               1
devices         0               1               1
freezer         0               1               1
blkio           0               1               1
perf_event      0               1               1

カーネルによってはサブシステムの一部がモジュールとなっていて、デフォルトではロードされていない場合があります。

$ sudo modprobe cls_cgroup
$ cat /proc/cgroups | grep net_cls
net_cls 0       1       1

前の実行例の環境で新たにnet_clsサブシステムのモジュールをロードすると/proc/cgroupsにnet_clsが現れているのがわかります。

サブシステムのマウント

cgroupfsをマウントする際にはサブシステムごとにマウントできます。前回の説明の際にオプションとして-o cpuという指定をしました。これはcpuサブシステムをマウントするという意味になります。同様に別のマウントポイントに-o memoryと指定してmemoryサブシステムをマウントしてみましょう。

$ sudo mount -t tmpfs cgroup /sys/fs/cgroup
$ sudo mkdir /sys/fs/cgroup/cpu
$ sudo mount -n -t cgroup -o cpu cgroup /sys/fs/cgroup/cpu(ここまでは前回の例と同じ)
$ sudo mkdir /sys/fs/cgroup/memory (memoryサブシステム用のディレクトリ作成)
$ sudo mount -n -t cgroup -o memory cgroup /sys/fs/cgroup/memory (memoryサブシステムを指定してマウント)
$ ls /sys/fs/cgroup/memory/
cgroup.clone_children            memory.move_charge_at_immigrate
cgroup.event_control             memory.numa_stat
cgroup.procs                     memory.oom_control
memory.failcnt                   memory.soft_limit_in_bytes
memory.force_empty               memory.stat
  :(略)

/sys/fs/cgroup/memory以下にmemoryサブシステムをマウントしました。cpuサブシステムとは作られているファイルが違いますね。ここで見たように複数の階層構造を持てるというのもcgroupの特徴のひとつです。

また、複数のサブシステムを同時に1つのマウントポイント以下にマウントすることも可能です。たとえば以下のようにcpuとmemoryを同時に1つのマウントポイント以下にマウントする事ができます。上でcpuとmemoryサブシステムをマウントしましたので、アンマウントしてから実行します。

$ sudo umount /sys/fs/cgroup/cpu
$ sudo umount /sys/fs/cgroup/memory
$ sudo mkdir /sys/fs/cgroup/cpu_memory
$ sudo mount -n -t cgroup -o cpu,memory cgroup /sys/fs/cgroup/cpu_memory
              (cpu、memoryサブシステムを同時にマウント)
$ ls /sys/fs/cgroup/cpu_memory/
cgroup.clone_children      memory.memsw.limit_in_bytes
cgroup.event_control       memory.memsw.max_usage_in_bytes
cgroup.procs               memory.memsw.usage_in_bytes
cpu.cfs_period_us          memory.move_charge_at_immigrate
cpu.cfs_quota_us           memory.numa_stat
  : (略)

ディレクトリ内を確認してみると、cpuサブシステム関連のファイルとmemoryサブシステム関連のファイルが同時に作られており、同じマウントポイントに2つのサブシステムがマウントされたことがわかります。

ただし、同じサブシステムを同時に別のマウントポイントにマウントすることはできません。上記のマウントを実行した後に再度memoryサブシステムをマウントしようとするとエラーとなります。

$ sudo mount -n -t cgroup -o memory cgroup /sys/fs/cgroup/memory
Mount: cgroup already mounted or /sys/fs/cgroup/memory busy

別階層へのプロセスの登録

サブシステムの解説とは少し外れますが、複数のcgroupfsをマウントする例をあげた所で、複数のcgroupfsへのプロセスの登録についても説明しておきましょう。

前回、cgroup間でのプロセスの移動とところで説明したとおり、あるcgroupにプロセスを登録すると、前に登録されていたcgroupからは自動的にプロセスが削除され、同時に2つのcgroupにプロセスが登録されることはありません。

それでは、cgroupfsが複数マウントされている場合はどうなるのでしょうか。先にcpuとmemoryのサブシステムを同時にマウントする例を試していますので、それをアンマウントしたあと、cpuとmemoryを別々にマウントして試してみましょう。

$ sudo mount -n -t cgroup -o cpu cgroup /sys/fs/cgroup/cpu
$ sudo mkdir /sys/fs/cgroup/cpu/test1    (cpu以下にtest1グループを作成)
$ sudo mount -n -t cgroup -o memory cgroup /sys/fs/cgroup/memory
$ sudo mkdir /sys/fs/cgroup/memory/test2 (memory以下にtest2グループを作成)
$ echo $$ | sudo tee -a /sys/fs/cgroup/cpu/test1/tasks (PIDをtest1に登録)
1738
$ cat /sys/fs/cgroup/cpu/test1/tasks     (test1のtasksを確認)
1738
2152
$ echo $$ | sudo tee -a /sys/fs/cgroup/memory/test2/tasks (PIDをtest2に登録)
1738
$ cat /sys/fs/cgroup/memory/test2/tasks  (test2のtasksを確認)
1738
2171
$ cat /sys/fs/cgroup/cpu/test1/tasks     (test1のtasksを確認)
1738
2188

memoryサブシステムに作成したtest2にプロセスを登録した後も、cpuサブシステムに作成したtest1にはPID 1738は登録されたままになっています。つまりcgroupfsの別のマウントであれば、同じプロセスを登録できることがわかります。

実際のシステムでも、サブシステムごとにマウントしたcgroupfsごとにコンテナ用のcgroupを作り、サブシステムごとにPIDを登録してコンテナのリソース制限を行うといったことはよく行われています。


サブシステムの説明と複数のcgroupfsへのプロセスの登録の話が済んだところで、各サブシステムの機能を説明していきましょう。

なお各サブシステムの説明は、それぞれのサブシステムの機能を理解するのに必要な最低限の機能の説明に限りますので、詳しい機能についてはカーネル付属の文書などをご参照ください。

cpuサブシステム

cpuサブシステムでは、以下のような方法でcgroupに対してcpuを割り当てできます。

帯域制限
単位時間内にcgroup内のタスクがCPUを使用できる合計時間を制限
相対配分
cgroup内のタスクが使えるcpu時間の割り当てを相対的に設定

最初に帯域制限を試してみましょう。まずは準備です。test1test2というcgroupを作成します。

sudo mkdir /sys/fs/cgroup/cpu/test1 /sys/fs/cgroup/cpu/test2

test1グループに現在のシェルを登録します。

$ echo $$ | sudo tee -a /sys/fs/cgroup/cpu/test1/tasks 
2278

ここで別のシェルを実行し、同様にtest2グループに登録します。

$ echo $$ | sudo tee -a /sys/fs/cgroup/cpu/test2/tasks
2953

帯域制御を行うにはcpu.cfs_quota_usに実行できる時間をマイクロ秒単位で設定します。これでcpu.cfs_period_usで指定されている単位時間内でどれだけCPUを使用できるかが設定できます。設定方法はtasksファイルへの書き込みと同様にcpu.cfs_quota_usに設定したい値を書き込みます。

ここではtest1グループには5ミリ秒、test2グループには10ミリ秒を設定してみましょう。

$ cat /sys/fs/cgroup/cpu/test1/cpu.cfs_period_us (単位時間は100ミリ秒)
100000
$ cat /sys/fs/cgroup/cpu/test2/cpu.cfs_period_us (単位時間は100ミリ秒)
100000
$ echo 5000 | sudo tee /sys/fs/cgroup/cpu/test1/cpu.cfs_quota_us (5ミリ秒の制限を設定)
5000
$ echo 10000 | sudo tee /sys/fs/cgroup/cpu/test2/cpu.cfs_quota_us (10ミリ秒の制限を設定)
10000
$ cat /sys/fs/cgroup/cpu/test1/cpu.cfs_quota_us (設定の確認)
5000
$ cat /sys/fs/cgroup/cpu/test2/cpu.cfs_quota_us (設定の確認)
10000

さて、ここで2つ起動しているそれぞれのシェルで以下のような処理を実行します。通常であればCPU使用率がほぼ100%になりますね。

$ while :; do true ; done

これをtop -p 2278,2953としてCPU使用率を確認すると、測定タイミングによって値は変化しますが、それぞれのプロセスのCPU使用率はそれぞれおよそ10%と5%になっていることがわかります。

 PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 2953 karma     20   0 24768 7224 1632 R 10.0  0.7   0:54.35 bash
 2278 karma     20   0 24860 7364 1680 R  5.0  0.7   0:27.72 bash

相対配分も簡単に説明しておきましょう。相対配分を行う場合は各cgroup以下のcpu.sharesファイルを使います。このファイルに書かれた数値が1024であるtest1グループと、512であるtest2グループがある場合、test1グループにはtest2グループの倍のCPU時間が割り当てられます。

帯域制御についてはカーネル付属文書のsched-bwc.txtに、相対配分についてはsched-design-CFS.txtに少し説明があります。

cpuacctサブシステム

cpuacctはcgroup内のプロセスが使用したCPU時間のレポートが生成されます。このサブシステムには設定するような項目はありません。詳しくはカーネル付属文書のcpuacct.txtをご覧ください。

たとえば以下はcgroup内のプロセスが消費したCPU時間のレポートを表示しています(単位はナノ秒⁠⁠。

$ cat /sys/fs/cgroup/cpuacct/test1/cpuacct.usage
462998808

cpusetサブシステム

cpusetサブシステムは、cgroupにCPUやメモリノードを割り当てます。CPUとメモリノードの設定はそれぞれcgroup内のcpuset.cpuscpuset.memsファイルに設定します。ルート直下のcgroup中のこの2つのファイルはシステム上の全てのCPUとメモリノードが設定されています。

ルート以下に新たにcgroupを作成した場合はこの2つのファイルは空になっており、この2つのファイルに値を設定しなければtasksファイルにプロセスを登録できませんので注意が必要です。ただしcgroupを作成した際、親のcgroupと同じ設定が書き込まれた状態でこの2つのファイルを作成して良いのであれば、cgroup.clone_childrenファイルに1を書き込んでおけば、cgroupを作成した時に親のcgroupの設定がコピーされます。

この2つのファイルには"-"(ハイフン)や","(カンマ)を使って複数の値を設定できます。たとえばCPU 0から1と5を登録する場合、

$ echo "0-1,5" | tee /sys/fs/cgroup/cpuset/test1/cpuset.cpus

といった風に設定できます。

詳しくはカーネル付属文書のcpuset.txtをご覧ください。

devicesサブシステム

devicesサブシステムはこれまでに説明したような数値で表されるリソースを制限するサブシステムではなく、cgroupに対するアクセス権を設定する機能です。

Linuxカーネルの持つコンテナ機能では、デバイスはコンテナごとに仮想化されることはありません。そのため、ホストOSから見えているデバイスへのコンテナに対するアクセス権を設定できるようにdevicesサブシステムが存在します。

アクセス権の設定はcgroup内のdevices.allow(cgroupがアクセス可能なデバイスリスト)devices.deny(cgroupがアクセスできないデバイスリスト)ファイルに設定します。この2つのファイルに3つのフィールドからなるエントリを設定し、アクセス権を設定します。

タイプ
ブロックデバイス(b⁠⁠、キャラクタデバイス(c⁠⁠、両方(a)
デバイスノード番号
メジャー番号とマイナー番号をコロンで連結
アクセス権
読み取り(r⁠⁠、書き込み(w⁠⁠、デバイスファイルの作成(m)

以下はtest1グループに対して、まず全てのアクセスを拒否した後、/dev/nullに対するアクセスを許可している例です。

$ echo a | sudo tee /sys/fs/cgroup/devices/test1/devices.deny 
a
$ echo 'c 1:3 rwm' | sudo tee /sys/fs/cgroup/devices/test1/devices.allow
c 1:3 rwm
$ cat /sys/fs/cgroup/devices/test1/devices.list
c 1:3 rwm

devices.allow,devices.denyファイルは設定専用のファイルですので、設定した結果はdevices.listファイルで確認します。

詳しくはカーネル付属文書のdevices.txtをご覧ください。

freezerサブシステム

freezerサブシステムはcgroupのサブシステムの中では少し変わった存在です。freezerはリソースを制御するのではなく、cgroupに属するタスクそのものの制御を行います。この機能を使うとcgroup内のプロセスを一括で一時停止させたり、一括で再開させたりできます。

実行中のコンテナの状態を保存して停止させ、その後再開させるcheckpoint/restartという機能があり、そこに使うことを考えて実装されたようです。しかし、現在checkpoint/restart機能のメジャーな実装であるCRIUではいまのところ使われていないようです。Docker Engine中のdocker pause/unpauseコマンドではこの機能を使っています。

ではfreezerサブシステムの使い方を簡単に見ておきましょう。今までの例と同様に、freezerサブシステムを/sys/fs/cgroup/freezerにマウントし、そこにtest1グループを作った上で試しています。freezerサブシステムには以下のようなファイルが存在します。

$ ls /sys/fs/cgroup/freezer/test1
cgroup.clone_children  cgroup.procs   notify_on_release
cgroup.event_control   freezer.state  tasks

freezerサブシステムを使ってcgroup内のタスクの状態を変えたり、現在の状態を確認するにはfreezer.stateファイルを使います。以下の3つの状態が存在し、freezer.stateファイルに、そのcgroupの状態が書かれています。

FROZEN
タスクは一時停止中
FREEZING
タスクの一時停止処理を実行中
THAWED
タスクは実行中

状態を変更するにはfreezer.stateファイルに変化させたい状態の文字列を書きます。一時停止させたいときはFROZENを、再開させたいときはTHAWEDを書きます。FREEZINGは状態の確認専用の値であり、書き込みはできません。

まずは他のサブシステムと同様にcgroupのtasksにシェルのPIDを登録します。

$ echo $$ | sudo tee /sys/fs/cgroup/freezer/test1/tasks 
1732

ここで別のシェルを起動します。

$ ps u -p 1732
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
karma     1732  0.0  0.7  24916  7436 pts/0    S+   15:03   0:00 -bash
$ cat /sys/fs/cgroup/freezer/test1/freezer.state 
THAWED

プロセスの状態の変化を見るためにpsコマンドを実行しています。freezer.stateファイルで状態を確認するとTHAWEDとなっており、プロセスは実行状態であることがわかります。

この状態でfreezer.stateファイルにFROZENを書き込み、一時停止状態にします。

$ echo 'FROZEN' | sudo tee /sys/fs/cgroup/freezer/test1/freezer.state
FROZEN
$ cat /sys/fs/cgroup/freezer/test1/freezer.state 
FROZEN
$ ps u -p 1732
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
karma     1732  0.0  0.7  24916  7436 pts/0    D+   15:03   0:00 -bash

書き込んだ後にfreezer.stateファイルを確認するとFROZENとなっており、一時停止状態であることがわかります。登録したPIDのシェルは反応がないはずです。psコマンドのSTAT欄を見てもプロセスの状態が変化しているのがわかります。

再度実行状態にするにはfreezer.stateTHAWEDを書き込みます。

$ echo 'THAWED' | sudo tee /sys/fs/cgroup/freezer/test1/freezer.state
THAWED
$ cat /sys/fs/cgroup/freezer/test1/freezer.state 
THAWED

状態がTHAWEDに変わっており、シェルも反応するはずです。

なお、ルート直下のcgroupに対しては一時停止ができません。そのためfreezer.stateファイルは存在しません。

freezerサブシステムもカーネル付属文書にfreezer-subsystem.txtがありますのでご参照ください。

まとめ

今回は最初に現在カーネルに実装されているサブシステムの紹介を行いました。続くサブシステムのマウントやプロセスの登録について説明の部分で、いくつかcgroupの特徴を説明しましたので、ここでまとめておきましょう。

今回紹介した特徴をあげる前に、おさらいとして前回紹介した特徴を再度載せておきます。

  • cgroupはcgroupfsという特別なファイルシステムで管理する
  • cgroup はディレクトリで表される
  • cgroupfsをマウントするとデフォルトでは全てのプロセスがトップディレクトリのグループにデフォルトで登録される子プロセスはデフォルトでは親のプロセスと同じcgroupに属する
  • cgroupは階層構造を取り、グループ間の親子関係を構成できる
  • システム上のプロセスは必ずcgroupfsのトップディレクトリ以下のcgroupのどれか1つだけに属する

ここに今回紹介した特徴が加わります。

  • cgroupを使って制御を行いたい機能ごとに『サブシステム』が存在する
  • cgroupfsには単独でサブシステムをマウントすることも、任意の複数のサブシステムを同時にマウントこともできる
  • cgroupfsは複数の階層を持つことができる
  • 同一のサブシステムを同時に別のマウントポイントにマウントすることはできない

最初に紹介した通り、サブシステムにはたくさんの種類がありますので、今回で全部を紹介することはできませんでした。次回は残ったサブシステムの紹介を行い、その後にcgroup開発の現状について少し触れたいと思います。

おすすめ記事

記事・ニュース一覧