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

第37回 Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2[1]

この記事を読むのに必要な時間:およそ 5 分

この連載をはじめてから,毎年この連載の記事でAdvent Calendarに参加してきました。昨年までは,この連載の記事で"Linux Advent Calendar"に参加してきました。

今年は参加するカレンダーを少し変えて,まずはこの記事で私が所属する会社のAdvent Calendarであるファーストサーバ Advent Calendar 2017の5日目に参加してみます。あまりたくさんの人が興味を持たなさそうな話題なので,会社のAdvent Calendarにマッチするのか心配です (^_^;)。

さて,Linuxでコンテナでリソース制限を行うための機能として,この連載では第3回から第5回まで3回に渡ってcgroupについて解説しました。

cgroupは,多くのLinuxディストリビューションでinitとして採用されているsystemdが使っていますので,今やLinuxをお使いであれば意識をせずにcgroupを使っている人が多いでしょう。

このときに紹介したcgroupは,執筆時点の最新バージョンである4.14カーネルではcgroup v1と呼ばれています。一方,第5回の最後で紹介した,cgroupの再設計と改良の動きは4.5カーネルで正式機能となり,cgroup v2と名付けられました。

今回は,そのcgroup v2について紹介していきたいと思います。

cgroup v1の特徴のおさらい

cgroup v2の説明に入る前に,以前説明したcgroupの機能や特徴を軽くおさらいしておきましょう。詳しくは第3回から第5回の記事をご覧ください。

cgroupfs

cgroupを使うには,cgroupfsという特別なファイルシステムをマウントして使います。cgroupfsは通常のファイルシステム風の見た目を持ち,通常のファイルシステム風に操作が行えます。

リソースを制御する単位となるcgroupはディレクトリで表され,cgroupを作成するときはmkdirコマンドを使うなど,通常のファイルシステムと同様の操作でcgroupを操作できます。

$ sudo mount -t cgroup -o pids cgroup /sys/fs/cgroup/pids
$ sudo mkdir /sys/fs/cgroup/pids/test01 (cgroup "test01"の作成)
$ ls /sys/fs/cgroup/pids/test01 (作成したcgroupには制御用のファイルが自動で作成される)
cgroup.clone_children  notify_on_release  pids.events  tasks
cgroup.procs           pids.current       pids.max

階層構造

先に述べたようにcgroupfsは通常のファイルシステムと同様の操作によりグループ操作を行います。つまり通常のファイルシステムのようなツリー構造を取ります。

複数階層構造

そして,cgroupのこの階層構造は複数持てます。複数回cgroupfsをマウントして階層を複数作った場合,システム上の各プロセスはすべての階層構造それぞれの中で,どこかのcgroupに属します。

サブシステム

cgroupは実装面から見るとふたつに分かれています。

階層構造の管理など,cgroup自身の機能を管理するためのコア部分と,実際にリソース制御を行うサブシステム(コントローラ)です。

実際に制御したいリソースがある場合は,そのリソース用のサブシステムを実装する必要があり,4.14カーネル時点では13のサブシステムが準備されています。サブシステムは,マウントの際にオプションでサブシステムの指定を行い,階層構造に紐付けます。ひとつの階層にひとつだけサブシステムを紐付けることも,複数のサブシステムを紐付けることもできます。

$ sudo mount -t cgroup -o cpu cpu /sys/fs/cgroup/cpu
(cpuサブシステムを使う指定をしてcgroupfsをマウント)

cgroupへのタスクの登録

cgroupへはスレッド単位でタスクが登録できます。

cgroupへタスクを登録するには,各ディレクトリ以下にできるファイルにプロセス,もしくはスレッドのIDを書き込みます。

$ echo $$ | sudo tee /sys/fs/cgroup/pids/test01/tasks ("test01"グループへのプロセスの登録)
13077

cgroup v1の問題点

以上のような特徴を持つcgroup v1は,さまざまなサブシステムが徐々に追加され,広く利用されるようになってきました。それと共に色々な問題点が指摘されるようになってきました。

複数階層構造

複数の階層を自由に作成でき,それぞれの階層に自由にサブシステムが所属できるという cgroup v1の特徴は,非常に柔軟で自由度が高く見えます。しかし,実際にはcgroup v1にはさまざまな制限があり,実はそれほど自由度がありません。

まず,サブシステムはひとつの階層にしか所属できません。cgroupのサブシステムのうち,ユーティリティ的な機能を持つfreezerサブシステムは,所属するプロセスすべてに対して同時に複数の階層で使えると便利です。しかし,複数の階層を持った場合には特定の階層でしか使えません。また,一度どこかの階層にサブシステムを所属させると,他の階層に移動ができません。

上の問題を回避するために,複数またはすべてのサブシステムをひとつの階層に所属させることもできます。そうした場合,たとえばCPUとメモリは別の階層構造(=ディレクトリ構造)を持たせて管理を行いたい,というような要求に応えられません。

つまり階層を複数持てて,それぞれの階層に任意の数のサブシステムを所属させられると言っても,意外に柔軟性がないのです。

また,複数の階層を持って,それぞれの階層内で異なる構造を作成しても管理が複雑になるだけで,実際にそのように使うようなケースはほとんどありませんでした。

結局は,サブシステムごとに別の階層を作り,密接に関係するサブシステムのみ同じ階層に属させることがほとんどになりました。また,複数の階層を持っても,各階層内の構造は同じような形の階層構造を取る運用がほとんどです。

実際,LXCコンテナでcgroupを使う場合は,コンテナ名のcgroupを各階層に作成しますので,全階層が同じ構造になります。

$ ls -F /sys/fs/cgroup/ (Ubuntu 16.04でのcgroup構造)
blkio/    cpu,cpuacct/  freezer/  net_cls@           perf_event/
cpu@      cpuset/       hugetlb/  net_cls,net_prio/  pids/
cpuacct@  devices/      memory/   net_prio@          systemd/
$ tree -L 2 -d /sys/fs/cgroup/ (各階層で同じような構造になっている様子)
/sys/fs/cgroup/
├── blkio
│   ├── init.scope
│   ├── lxc
│   ├── system.slice
│   └── user.slice
  :(略)
├── cpu,cpuacct
│   ├── init.scope
│   ├── lxc
│   ├── system.slice
│   └── user.slice
  :(略)
├── devices
│   ├── init.scope
│   ├── lxc
│   ├── system.slice
│   └── user.slice
  :(略)

サブシステム間の連携

cgroup v1では,サブシステムは実装に関わる規則に沿っていれば,特に他のサブシステムのことを気にせず実装できました。つまり各サブシステムの機能は独立しているということです。これは色々なサブシステムを徐々に実装していくという面ではメリットだったように思います。実装時も自身の機能のみ考えて実装すれば良いので実装がしやすいでしょう。

当然,このようにバラバラに実装が進むと当然サブシステム間で連携する機能はありません。また,複数階層構造を取り,どの階層にどのサブシステムが属するかわからない状態では,サブシステム間で連携するのは難しいでしょう。cgroupにさまざまなサブシステムが実装されていくにつれ,これが問題になるケースがでてきました。

たとえば,ファイルのI/Oでは,通常はメモリ上に確保されるページキャッシュを通した入出力が行われます。しかし,memoryサブシステムとblkioサブシステムは別々に実装されているため,cgroup v1ではページキャッシュを通したBuffered I/Oを制限できません。cgroup v1のblkioサブシステムを使って,きちんと制限が設定できるのはダイレクトI/Oの場合のみでした。

サブシステム間の一貫性

上に書いたように,各サブシステムは別々に実装が進められたため,動きに一貫性がありません。

たとえば,cpusetサブシステムを使えるようにcgroupfsをマウントし,mkdirコマンドでcgroupを作成します。そのままの状態で,このcgroupにタスクを登録しようとするとエラーになります。

$ sudo mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset
$ echo $$ | sudo tee /sys/fs/cgroup/cpuset/test01/tasks 
3046
tee: /sys/fs/cgroup/cpuset/test01/tasks: No space left on device

これは,cpusetでcgroupを作成した直後は,cpuset.cpuscpuset.memsというふたつのファイルが空だからです。

タスクを登録する前にこれらのファイルの中身をきちんと設定するか,親cgroupのcgroup.clone_childrenファイルに1を書き込んでおけば,親cgroupの値が子にコピーされますので,エラーにはなりません。

しかし,このようにcgroup作成後にできるファイルの中身がデフォルトで空で,cgroup.clone_childrenファイルで動きが変わるサブシステムは,cpusetサブシステムのみです。

他にも,memoryサブシステムは,階層構造になっているツリーで祖先(上位)のcgroupの制限の影響を受けるか受けないかをmemory.use_hierarchyというファイルで制御できます。このような設定ができるのはmemoryサブシステムだけです。他のサブシステムでは,上位に割り当てられたリソースを子に分配していきます。

つまり,使用するサブシステムごとに,そのサブシステム独自の設定を行う必要があり,操作面から見ると一貫性が欠けていると言われても仕方がないでしょう。

どのノードにもタスクが所属できる

cgroupは,ディレクトリによって表されるcgroupでツリー構造を形成します。cgroup v1では,ツリー構造のどの部分のノードにもタスクが所属できます。

図1 cgroup-v1の階層構造

図1 cgroup-v1の階層構造

cpuサブシステム用のツリーで,図のように途中の階層にタスクが複数所属し,その子cgroupにもタスクが所属している場合を考えてみましょう。ツリーの階層が深くなるごとにふたつに分岐し,それぞれに50%ずつCPU時間を割り当てます。

cgroup Aにタスクが所属していなければ,BとCに25%ずつCPU時間を割り当てれば良いのですが,図ではAにも3つタスクが所属しています。このときA,B,Cに所属するタスクそれぞれにどれだけCPU時間が割り当たるのか,非常にわかりづらいです。

この問題は,子cgroupのタスクと親cgroupの内部タスクの競合と呼ばれ,cgroup v1の大きな問題点とされていました。この問題を解決するための方法はコントローラに任され,コントローラ間の一貫性を欠いていました。

スレッド単位での制御

cgroup v1では,cgroupへの所属するタスクの最小単位はスレッドでした。しかし,サブシステムが扱うリソースの中にはスレッド単位で制御できるリソースもあれば,スレッド単位では扱わないリソースもあります。たとえば前者はCPU,後者はメモリです。

元々プロセス単位でリソースを割り当てたのに,同じリソースを共有している複数のスレッドを別々にコントロールできるのが果たして正しいのか,どのように処理すべきなのかという問題があります。


以上のように,cgroup v1の特徴である自由度の高さが原因で,逆に色々と問題視されるようになってきました。筆者にとって,カーネルのcgroup関連の構造やコードは非常に複雑で理解しづらいです。この複雑さは,この自由度の高さが原因ではないかと思っています。

著者プロフィール

加藤泰文(かとうやすふみ)

2009年頃にLinuxカーネルのcgroup機能に興味を持って以来,Linuxのコンテナ関連の最新情報を追っかけたり,コンテナの勉強会を開いたりして勉強しています。英語力のない自分用にLXCのmanページを日本語訳していたところ,あっさり本家にマージされてしまい,それ以来日本語訳のパッチを送り続けています。

Plamo Linuxメンテナ。株式会社IDCフロンティア所属。

Twitter:@ten_forward
技術系のブログ:http://tenforward.hatenablog.com/