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

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

サブシステム紹介(続き)

今回は前回紹介できなかったサブシステムを紹介したあと、cgroup開発の現状に少し触れたいと思います。

Memoryサブシステム

コンテナに対してメモリの制限を行いたい場合に使用するのがmemoryサブシステムです。cgroupに対して制限値を設定したり、cgroupのメモリ使用量の監視をしたりできます。

まずは制限を設定してみましょう。制限を設定するには以下の2つのファイルに値を書き込みます。

memory.limit_in_bytes
メモリ消費量の制限値の設定と表示(バイト)
memory.memsw.limit_in_bytes
メモリ+スワップメモリの消費量の制限値の設定と表示(バイト)

スワップの消費量も含めた制限を設定したい場合は後者を、スワップの消費量はカウントしない場合は前者を使用します。制限値の設定にはk、K、m、M、g、Gといった単位の文字が使えます。

たとえばtest1グループのメモリ使用量を100MBに設定したい場合は以下のように設定します。/sys/fs/cgroup/memoryにmemoryサブシステムをマウント済みとします。

$ sudo mkdir /sys/fs/cgroup/memory/test1
$ echo $$ | sudo tee /sys/fs/cgroup/memory/test1/tasks (現在のシェルをtasksに登録)
31784
$ echo 100M | sudo tee /sys/fs/cgroup/memory/test1/memory.limit_in_bytes (制限値を100MBに設定)
100M
$ cat /sys/fs/cgroup/memory/test1/memory.limit_in_bytes (設定された制限値の確認)
104857600

100Mという文字列を書き込むと、きちんと100MBに制限値が設定されているのがわかります。

では実際に制限値に達した時にどうなるかを見る前にmemoryサブシステムで使用するファイルをさらにいくつか見ておきましょう。

memory.usage_in_bytes
cgroup内のプロセスが現在消費しているメモリ
memory.max_usage_in_bytes
cgroup内のプロセスが今までに消費したメモリの最大値
memory.failcnt
cgroup内のプロセスのメモリ消費量が制限値にヒットした回数

memory.usage_in_bytesを継続して監視することで、グループ内のプロセスのメモリ消費が監視できますし、memory.max_usage_in_bytesmemory.failcntをチェックして、グループに設定する制限値を見積もったりすることができます。

さて、それでは実際にメモリの制限値を設定して動きを見てみましょう。例では、繰り返しmalloc()でメモリを確保して、memset()でメモリを消費するだけのmemoryというプログラムを作成し、実行しました。

$ ./memory 
Killed

実行して少し時間が経つと"Killed"と表示されてプロセスがkillされました。これだけではなんのことかわからないかと思います。これはLinuxでシステムのメモリが足りなくなった時に発動するOOM Killerによってcgroup内のプロセスが強制終了させられています。メモリの消費が上限に達したことをmemory.max_usage_in_bytesを見て確認してみましょう。

$ cat /sys/fs/cgroup/memory/test1/memory.max_usage_in_bytes 
104857600

先ほど設定した制限値が最大値になっており、上限に達してプロセスが強制終了されたことがわかります。OOM Killerでいきなり強制終了させるのでなく、OOM Killerを無効にして、eventfd(2)を通じて通知を受け取ることもできます。詳しくはカーネル付属文書のmemory.txtをご覧ください。

また、3.10 カーネルからはcgroup内のプロセスのメモリの圧迫度合いに応じて同様にeventfdで通知が行えるようになっています。この機能を使うと、OOMKillerが発動するような状況になる前に通知を受け取れます。

メモリ使用量の制限を行うだけであれば、ここまでで説明した通り簡単に設定できました。しかし、memoryサブシステムにはいろいろと設定する上で注意する必要のある点が存在します。特に注意すべき点をいくつか挙げておきましょう。

  • memoryサブシステムをマウントしたルート直下のファイルへの値の設定は行えません。
  • 前回説明した階層構造はデフォルトでは無効になっています。有効にするにはmemory.use_hierarchyというファイルに1を書き込みます。ただし、cgroupがすでに配下に子供のcgroupを持つときはこのファイルの値の変更はできません。
  • 先に紹介したスワップを含めたメモリ消費の制限を設定するmemory.memsw.limit_in_bytesへ値を設定したい場合は、あらかじめmemory.limit_in_bytesに値を設定しておかなければなりません。

ここで紹介した以外にもmemoryサブシステムには多数の機能があります。詳しくは先に紹介したmemory.txtをご覧ください。

ここで紹介したメモリ制限はユーザメモリの制限であり、カーネルが使うメモリの制限を行うにはmemory.kmemで始まるファイルを使用します。しかしカーネルメモリの管理機能はまだまだ実装や改良が続いており、落ち着くにはもう少し時間がかかるようです。

net_clsサブシステム

net_clsサブシステムはcgroupに属するプロセスが発信するパケットに識別用の識別子を付け、そのIDを使ってトラフィック制御ができるようにするための仕組みです。識別子をトラフィック制御を行うためのtcコマンドで指定して、cgroupのプロセスから発信されるパケットを制御します。

また3.14カーネルでこの識別子をnetfilterから使う機能が実装されています。パケットに識別子を付け、そのパケットをフィルタリングできます。ただし、この機能に対応したiptablesコマンドは、現時点ではリリースされていないようです。

net_clsサブシステムについてはカーネル付属文書のnet_clsをご参照ください。

blkioサブシステム

blkio(ブロックI/O)サブシステムは、cgroup内のタスクのブロックデバイスへのI/Oを制御したり、cgroup内のタスクがどれくらいブロックデバイスへのアクセスを行ったかの統計値を取得するのに使えます。

では、cgroup内のタスクのブロックデバイスに対する帯域制限をかけてみましょう。まずはこれまでと同様に/sys/fs/cgroup/blkioというサブシステム専用のディレクトリを作成してから、blkioサブシステムを指定してマウントし、テスト用のcgroup test1を作成します。その後、test1グループに対して現在のシェルのPIDを登録します。

$ sudo mount -n -t cgroup -o blkio cgroup /sys/fs/cgroup/blkio/ (blkioサブシステムのマウント)
$ sudo mkdir /sys/fs/cgroup/blkio/test1
$ echo $$ | sudo tee -a /sys/fs/cgroup/blkio/test1/tasks 
25462

ここでは/dev/vdbに対して単位時間辺りに書き込める量に制限をかけてみます。この時使用するファイルは以下のファイルです。

blkio.throttle.read_bps_device
デバイスからの読み込みの制限値(バイト/秒)
blkio.throttle.write_bps_device
デバイスへの書き込みの制限値(バイト/秒)

制限をかけるにはデバイスのノード番号が必要です。このノード番号と制限値を以上のファイルに書き込むことで制限できます。またblkioで制限ができないデバイスのノード番号を指定したり、/dev/vdb1などのパーティションを指定したりするとエラーで書き込めません。

$ ls -l /dev/vdb
brw-rw---- 1 root disk 253, 16 Jun 16 15:03 /dev/vdb

/dev/vdbのノード番号は 253:16 ですので、この番号と制限値として1Mバイト/秒をこれらファイルに書き込みます。まずは書き込みの制限です。

$ echo "253:16 1048576" | sudo tee /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device 
253:16 1048576
$ cat /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device 
253:16  1048576

無事書き込めていますね。複数のデバイスに対する設定を行う場合は、上記のような操作を繰り返し行います。

$ echo "253:0 1048576" | sudo tee /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device
253:0 1048576
$ cat /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device 
253:0   1048576
253:16  1048576

/dev/vdaのノード番号である253:0に対する制限を書き込むと、デバイスごとの制限値が書き込まれているのが確認できます。

制限を取り消したい場合は制限値として0を書き込みます。

$ echo "253:0 0" | sudo tee /sys/fs/cgroup/blkio/test1/blkio.throttle.write_bps_device 
253:0 0
$ cat blkio.throttle.write_bps_device 
253:16  1048576

先ほど存在していた/dev/vdaに対する制限が消えていますね。

さて、この制限がきちんと効いているか確認してみましょう。

$ sudo dd if=/dev/zero of=/data/testfile bs=4K count=1024 oflag=direct
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 4.00925 s, 1.0 MB/s

きちんと設定通りの値になっていますね。同様に読み込みの制限を設定してみましょう。

$ echo "253:16 1048576" | sudo tee /sys/fs/cgroup/blkio/test1/blkio.throttle.read_bps_device
253:16 1048576
$ dd if=/data/testfile of=/dev/null bs=4K count=1024 iflag=direct
1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 4.00191 s, 1.0 MB/s

書き込みと同様に制限がされているのがわかりますね。ここでは単位時間あたりのバイト数で制限しましたが、IOPSでも制限できます。

上記のような設定を行うと、以下のファイルでそのデバイスとの間で転送されたバイト数やIOの回数を確認できます。

blkio.throttle.io_serviced
IO操作の回数
blkio.throttle.io_service_bytes
転送されたバイト数

これも確認しておきましょう。

$ cat /sys/fs/cgroup/blkio/test1/blkio.throttle.io_serviced
253:16 Read 1027
253:16 Write 1024
253:16 Sync 2051
253:16 Async 0
253:16 Total 2051
Total 2051
$ cat /sys/fs/cgroup/blkio/test1/blkio.throttle.io_service_bytes 
253:16 Read 4206592
253:16 Write 4194304
253:16 Sync 8400896
253:16 Async 0
253:16 Total 8400896
Total 8400896

I/OスケジューラとしてCFSスケジューラを使用している場合は、帯域制限だけでなく、cgroup間の重みづけの配分も設定できます。また、以上で紹介した以外にもblkioサブシステムではいろいろなパラメータを確認できます。詳しくはカーネル付属文書のblkio-controller.txtをご覧ください。

perf_eventサブシステム

perf_eventサブシステムを使うと、Linuxの解析ツールであるperfでcgroupを指定した解析が行えます。

カーネル付属文書のperf-record.txtperf-stat.txtにcgroupの指定方法が載っています。

net_prioサブシステム

net_prioサブシステムは、cpuやblkioサブシステムの相対配分や重みづけ配分のように、複数のcgroupの間の優先度を割り当てます。インターフェースごとに優先度が設定できます。

優先度の設定と設定の確認にはnet_prio.ifpriomapファイルを使います。ここまでの例と同様に/sys/fs/cgroup/net_prioディレクトリにマウントして試してみましょう。

ここの例はUbuntu 12.04 LTSにlinux-current-genericカーネルを入れて試しています。12.04 LTS標準の3.2カーネルではnet_prioサブシステムはまだ存在しないためです[1]⁠。

$ sudo modprobe netprio_cgroup (net_prioのモジュールをロード)
$ sudo mount -n -t cgroup -o net_prio cgroup /sys/fs/cgroup/net_prio
$ sudo mkdir /sys/fs/cgroup/net_prio/test1
$ sudo mkdir /sys/fs/cgroup/net_prio/test2

ここではcgroupfsをマウントしたあと、test1test2という2つのグループを作成しました。まずはどのように制限値が設定されているか、このファイルの中を見てみましょう。

$ cat /sys/fs/cgroup/net_prio/test1/net_prio.ifpriomap 
lo 0
eth0 0
$ cat /sys/fs/cgroup/net_prio/test2/net_prio.ifpriomap 
lo 0
eth0 0

このようにインターフェースごとの優先度の値が書かれています。cgroupを作成した時のデフォルト値は親グループのnet_prio.ifpriomapの値が使われます。

それではここで作成した2つのグループに優先度を設定してみます。test1グループに1を、test2グループに10を設定します。数字が大きい方が優先度が高くなります。

$ echo "eth0 1" | sudo tee /sys/fs/cgroup/net_prio/test1/net_prio.ifpriomap 
eth0 1
$ echo "eth0 10" | sudo tee /sys/fs/cgroup/net_prio/test2/net_prio.ifpriomap 
eth0 10
$ cat /sys/fs/cgroup/net_prio/test1/net_prio.ifpriomap 
lo 0
eth0 1
$ cat /sys/fs/cgroup/net_prio/test2/net_prio.ifpriomap 
lo 0
eth0 10

それぞれのグループのnet_prio.ifpriomapファイルを見ると、eth0に対する優先度がそれぞれ1と10に設定されており、test2グループの方が優先度が高く設定されました。

net_prioグループについては、カーネル付属文書のnet_prio.txtもご覧ください。

hugetlbサブシステム

HugeTLBはメモリを管理する単位であるページのサイズを大きくすることで、たくさんのメモリを使うアプリケーションのパフォーマンスを改善する仕組みです。hugetlbサブシステムはこのHugeTLBをcgroupから扱うことのできる機能です。詳しくはカーネル付属文書のhugetlb.txtをご覧ください。

nsサブシステム

nsサブシステムは前回の表1では紹介しませんでした。なぜかと言いますと、nsサブシステムはいろいろと実装に問題があったため、3.0カーネルで削除されたためです。

元々は名前空間機能と連携してcgroupを作成する機能でした。2.6.37カーネルがリリースされた際に、cpusetサブシステムのところで説明したcgroup.clone_children機能が実装されるとともに、廃止予定の機能となり、3.0で廃止されました。

2.6.32カーネルを使用しているRHEL/CentOS 6ではこの機能が有効です。しかし、このサブシステムはマウントしない方が無難でしょう。RHEL/CentOS 6上でLXCを動かす場合は、このサブシステムをマウントしているとうまく動きません。

cgroup の今後

今回を合わせると3回に渡ってLinuxカーネルに実装されているcgroupの解説をしてきました。ここまで解説したようにかなり広範囲に渡る機能が実装されている一方で、現時点でもcgroupの改良、実装は続いています。実は、今後cgroupは大きな変更がされる予定で、この連載で説明した機能や特徴が大きく変わることになりそうです。

このような時期に、この連載を書くにあたってcgroupの説明をどうすべきか少し悩みました。しかし、長期にサポートされるRHEL 7やUbuntu 14.04 LTSがリリースされたばかりであり、RHEL/CentOS 6もまだまだ現役ですので、現時点のcgroupを説明する意味はあると考えました。

一方でcgroupの大きな変更がかなり進んできており、改良が続けられているサブシステムも存在します。筆者から見ると、次にリリースされる3.16カーネルはcgroupにとって一つの大きな変化の節目にも思えますので、ここで今後のcgroupの変化についても簡単に触れておきたいと思います。

memoryサブシステム

今回説明したmemoryサブシステムの機能はユーザメモリの制限でした。一方でカーネルが内部的に使用するカーネルメモリの制限機能も実装が進んでいます。現時点でもカーネルメモリの制限を行う機能は実装されておりmemory.kmem.で始まるファイルがカーネルメモリを制限するためのファイルです。

しかし、このカーネルメモリの制限機能は現在かなり激しく機能の追加や変更がされており、実用に必要な重要な機能の実装が済んでいないことから、3.16カーネルでは一旦「開発目的以外では使うべきではない」機能であるとされるようです。

また、システム全体でメモリ不足に陥った時でも、cgroupに対して最低限確保するメモリ量を保証するLow-limit機能の提案がされており、実装が進んでいます。

このようにまだまだ激しく変化している発展途上のサブシステムであり、しばらくは目が離せません。

cgroupの再設計と変更

これまで説明したようにcgroupの開発が進み多数のサブシステムが実装され、さまざまなリソースの制限ができるように実装が進んできました。しかし、一定の機能が実装された所でいろいろな問題点が指摘されるようになってきました。

今後cgroupで一番大きく変わるのが前回cgroupの特徴としてあげた 複数階層構造のサポート がなくなり、単一階層構造 になることでしょう。

複数階層構造のサポートは一見柔軟性を持っているようにみえます。しかし現在のcgroupの仕様を考えるとあまり役に立つことがないようです。

たとえば、複数の階層構造(=cgroupfs)を持てるといってもサブシステムは同時にはひとつの階層構造にしか所属できません。さらに、一度どこかの階層に所属してしまうと移動はできません。

freezerのようなグループのタスクに対して同時に何かを行うサブシステムを考えた場合、複数のcgroupfsがあると、ひとつのcgroupfsにしか所属できないため、他のcgroupfsのcgroupに対して処理が行えなくなります。

このような場合、まとめて処理を行いたいサブシステムを同じマウントポイントに同時にマウントすれば解決できます。しかし、cgroupはディレクトリによって表されますし、あるプロセスは同じcgroupfsの中では同時にはひとつのグループにしか所属できません。つまり複数のサブシステムを同じcgroupfsにマウントしてしまうと、cgroupfsの中の構造は全く同じになってしまいますし、あるプロセスは同じcgroupに属さなければなりません。異なるサブシステムで特定のプロセスに対して違う扱いをしたいと思ってもできません。

このように、複数階層構造をサポートするために内部の実装が複雑になるわりには自由度は低く、柔軟にcgroupを構成できなくなっています。

cgroupの実際のユースケースでは、複数階層構造で複雑な制御が必要なケースはあまりないため、ある程度の柔軟な設定が可能で、サブシステム間の動きの一貫性が取れるように、単一階層構造へ変更することになったようです。

これに伴い、プロセスのcgroupへの登録方法や登録の際の制約が大きく変わります。どのように変わるかは、変更が実装された時点のカーネルの付属文書に記載されますので、そちらをご覧ください。

ただし、あるバージョンのカーネルからいきなり単一階層構造にがらっと変わることはありません。後方互換性を保ちながら変更が可能なように実装されていきます。

この他にもcgroupに現在実装されているeventfdによる通知の仕組みも変わる予定のようです。また、既にリリースされている3.15カーネルではcgroupfsの内部構造が変更されています。

cgroupの管理

ここまで説明したように、cgroupはcgroupfsというディレクトリ構造の中のファイルでいろいろな制御パラメータを簡単に設定できます。これは逆にいうとカーネルの内部の重要なパラメータをアクセス権さえあれば自由に変えたり見たりできるということで、問題がある言われています。

ただ、これだけcgroupの実装が進んで、現在のcgroupの仕様での利用が広がっているので、cgroupfsという仕組みを変えるわけにはいきません。

そこで、将来的にはcgroupを管理するエージェントが単一階層構造になったcgroup全体を管理し、cgroupを使ってリソース制御を行いたい場合は、そのエージェントに依頼するといった流れになるようです。

このエージェントへの名乗りをあげているのが、最近いろいろなディストリビューションで採用が進んでいるsystemdです。initとしてsystemdを採用していないUbuntuでも14.04 LTSからはcgmanagerというソフトウェアが存在しており、LXCもcgmanager経由でcgroupを操作するようになっています。

まとめ

第3回から3回にわけてcgroupの説明を行いました。例で説明したようにcgroupfsを直接触ってcgroupの設定を行うことはあまりないと思いますが、どのような値を設定してどのような制限がされるのかを理解しておくのは、実際にcgroupを使用してリソース制限を行う場合には重要なことだと思います。それぞれのサブシステムが持っている機能の一部しか説明できませんでしたが、これをきっかけにcgroupの理解を深めていってください。

さて、LXCの連載なのになかなかLXCの解説に入れませんが、次回もまだカーネルの機能の説明が続きます。次回はLXCで使う事の多いネットワーク関連の機能を説明したいと思います。

LXC-JPグループ

LXCに限らず、日本語でコンテナの話題を扱う場としてLXC-JPを紹介しておきます。最近ではコンテナの話題を扱ったコンテンツも多くなりましたが、そういうところでは解決できない問題や疑問を解決するのに使えるかもしれません。

名前はLXC-JPという名前ですが、LXCに限らずコンテナの話題なら何でもOKです。ご覧いただくと分かるのですが、実はそれほど活発に議論がされているわけでもありません(^_^;)。ここに相談したからと言って疑問や問題が解決するとは限りませんが、コンテナの話を扱う場の一つとして参加してみてはいかがでしょうか。もちろん筆者も参加しています。

おすすめ記事

記事・ニュース一覧