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

第34回Linuxカーネルに実装されたcgroup名前空間 ─ コンテナ内のcgroup管理[2]

さて、今年もAdvent Calendarの季節になりました。例年通り今年もいろいろなカレンダーがあって盛り上がっていますね。筆者もいくつかエントリしたので、そのための記事を書くのが大変です。

そのエントリしたカレンダーのひとつがLinux Advent Calendar 2016です。一昨年から、この連載の記事をLinux Advent Calendar 20152014へのエントリと兼ねて書いてきました。

今年も同様に、この記事をLinux Advent Calendar 2016 13日目の記事として書きました。Linux Advent Calendar 2016には今年もマニアックで面白いエントリが並んでいますね。

昨年のエントリ第30回は、Linuxカーネルに新たに追加されたcgroupのpidsサブシステムを紹介しました。今年は、Linuxカーネルが持つコンテナ関連機能としてはcgroupと双璧をなす、名前空間(Namespace)の新しい機能を紹介しようと思います。

今回の実行例はUbuntu 16.04上で実行しています。


前回は、コンテナ内でcgroupfsを利用するための仕組みとして、cgmanagerLXCFSを紹介しました。

Ubuntu 14.04 LTSでは、cgroupを管理するためのデーモンとしてcgmanagerが導入され、LXC 1.0と共に動作するように設定されました。しかしその後、Ubuntu 15.04ではLXCFSに置き換わりました。LXC 1.0はまだサポート期間ですので、cgmanagerはまだしばらくは使われ続けサポートが続きますが、短命なソフトウェアでしたね。

cgmanagerから置き換わったLXCFSがUbuntu 15.04で導入されたものの、その後リリースされた長期サポート版であるUbuntu 16.04 LTSでは、コンテナ内にcgroupfsツリーを提供する機能については使われなくなりました。LXCFSには前回紹介した通り、/proc以下をコンテナ向けに仮想化する機能がありますのでなくなるわけではありませんが、cgroup関連機能については今後不要になっていきます。

LXCFSのcgroup関連機能が不要になった理由は、新たにLinuxカーネルにcgroupを仮想化する機能が追加されたためです。これがcgroup名前空間という機能です。

/proc/[PID]/nsディレクトリ

cgroup名前空間を紹介する前に、これまでの名前空間の記事では紹介していなかった/proc/[PID]/nsディレクトリについて紹介しておきましょう("[PID]"はプロセスのPIDが入ります⁠⁠。

各プロセスに関連する情報を格納したファイルが存在する/proc/[PID]ディレクトリ以下にはnsというディレクトリが存在します。

このディレクトリには、どのような名前空間が使えるか、そしてそのプロセスが属している名前空間がわかる特殊なファイルが置かれています。

ls -lを実行すると、以下のように名前空間名をファイル名とする特殊なシンボリックリンクが見えます。

$ ls -l /proc/self/ns 
total 0
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 net -> net:[4026531957]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 pid -> pid:[4026531836]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 user -> user:[4026531837]
lrwxrwxrwx 1 ubuntu ubuntu 0 Dec  6 19:41 uts -> uts:[4026531838]

リンク先に数字が表示されていますが、これが名前空間を表しており、この数字が同じだと同じ名前空間に属しており、異なると異なる名前空間に属していることになります。

この/proc/[PID]/ns以下のファイルには、もうひとつ重要な役割があります。

あるプロセスが所属する名前空間に移動するためのシステムコールとしてsetns(2)というシステムコールがあります。このsetns(2)はファイルディスクリプタと名前空間の種類を引数に取ります。ここで与えるファイルディスクリプタが、/proc/[PID]/nsディレクトリ以下に存在するファイルのファイルディスクリプタです。

LXCでsetns(2)を使うコマンドは、起動中のコンテナ内に入るために使うlxc-attachです。この/proc/[PID]/ns以下の実装が完成したのは3.8カーネルの時で、setns(2)がすべての名前空間に対してきちんと動作し、lxc-attachコマンドが動作するようになったのがこの時でした。

cgroup名前空間

それではいよいよcgroup名前空間について見ていきましょう。

cgroup名前空間は4.6カーネルで導入された機能です。Ubuntu 16.04 LTSでインストールされるカーネルのバージョンは4.4ですが、このカーネルにもバックポートされており、Ubuntu 16.04でもcgroup名前空間が使用できます。

cgroup名前空間は名前空間ごとにcgroupを仮想化します。具体的には、新たにcgroup名前空間を作成すると、LXCFSが実現していたように自身に関連するcgroupだけが見えるようになります。見え方はLXCFSとは少し違います。

cgroup名前空間が使えるかどうかは、先に紹介した/proc/[PID]/ns以下にcgroupというファイルが存在するかを見ればわかります。先の実行例で示したように、Ubuntu 16.04上でnsディレクトリ以下を見ると、

$ ls /proc/self/ns
cgroup  ipc  mnt  net  pid  user  uts

以上のようにcgroupというファイルが存在しており、cgroup名前空間が使えることがわかります。cgroup名前空間をサポートしていないカーネルで/proc/[PID]/nsを見ても、cgroupというファイルは存在しません。

cgroup名前空間が提供する機能

LXCFSが提供するコンテナ内のcgroupfs

まずは比較のために前回紹介したLXCFSを使うとコンテナ内でcgroupfsがどのように見えたかをおさらいしておきましょう。

$ lxc-start -n xenial01

以上のように起動した非特権コンテナ"xenial01"用のcgroupは、

親環境上で見た場合
/sys/fs/cgroup/cpu/user/1000.user/1.session/lxc/xenial01/に存在
コンテナ内で見た場合
/sys/fs/cgroup/cpu/user/1000.user/1.session/lxc/xenial01/に存在

以上のように親であるホスト上の環境でもコンテナ内でも同じパスに存在しました(具体的なパスはユーザ環境により異なります⁠⁠。

ただし、コンテナ内では"xenial01"以下のディレクトリ以外にはcgroup関連のファイルが存在せず、他のディレクトリは空でした。

つまりLXCFSは、親と同じツリー構造を見せつつ、自分が権限を持つグループだけcgroupの中身を見せていました。

cgroup名前空間が提供するコンテナ内のcgroupfs

それではUbuntu 16.04上で同様に非特権コンテナを起動してみましょう。

$ lxc-start -n xenial01
$ lxc-attach -n xenial01 -- ls -F /sys/fs/cgroup/cpu,cpuacct/
cgroup.clone_children  cpuacct.usage_percpu  cpu.stat
cgroup.procs           cpu.cfs_period_us     notify_on_release
cpuacct.stat           cpu.cfs_quota_us      tasks
cpuacct.usage          cpu.shares

cpuサブシステムがマウントされた直下のルートグループを見ると、ディレクトリはありません。つまりルートcgroupのみ存在しています。OSを起動してcgroupfsをマウントしたときと同じですね。

そして、このコンテナ内に存在するcgroupfsのルート以下は、親環境ではルートグループより深いパスに存在していたコンテナ用cgroupの中身が見えているのです。このように、cgroup名前空間を使うと、コンテナ内では自身のcgroupがルートになります。とても自然な動きですね。

つまり非特権コンテナ"xenial01"用のcgroupは、

親環境上で見た場合
/sys/fs/cgroup/cpu,cpuacct/user/ubuntu/1/lxc/xenial01に存在(ログインユーザの環境により異なります)
コンテナ内で見た場合
/(ルート)に存在

実際にはコンテナ用のcgroupがコンテナ内でのルートになるのは、マウント名前空間をあわせて使っているからです。この連携はあとで説明します。

cgroupに属するPIDが書かれたtasksファイル(とcgroup.procsファイル)には、PID名前空間内でのPIDが書かれます。

$ head -n1 /sys/fs/cgroup/cpu,cpuacct/user/ubuntu/1/lxc/xenial01/tasks 
1374 (コンテナ内systemdの親の名前空間でのPID)
$ lxc-attach -n xenial01 -- sudo head -n1 /sys/fs/cgroup/cpu,cpuacct/tasks 
1 (コンテナ内systemdのコンテナ内でのPID)

このように、コンテナ内でもホストOS上と同様のcgroupfsの見せ方を提供する機能がcgroup名前空間です。

cgroup名前空間の機能を簡単に紹介したところで、もう少しcgroup名前空間について詳しく見ていきましょう。

/proc/[PID]/cgroupファイル

プロセスがどのcgroupに属しているかは、/proc/[PID]/cgroupというファイルを見ればわかります。

まずはテスト用にcgroupを作成します。

# mkdir /sys/fs/cgroup/memory/test01 (test01グループを作成)
# echo $$
1220
# echo 1220 > /sys/fs/cgroup/memory/test01/tasks (プロセスをtest01に登録)

以上のようにmemoryサブシステムのルート直下に"test01"グループを作成し、シェルのPIDを登録しました。

# cat /proc/1220/cgroup | grep memory
4:memory:/test01 (所属するcgroupは"/test01")

cgroupファイルのmemory行を見ると、/test01と書かれています。つまり/test01グループに属しているということですね。

cgroup名前空間と/proc/[PID]/cgroupファイル

cgroupファイルの内容が理解できたところで、unshareコマンドを使って新たな名前空間を作成してみましょう。

なぜかUbuntu 16.04にインストールされるunshareコマンドはcgroup名前空間を扱えません。そこで別途util-linux 2.29をソースからコンパイルし、対応するunshareコマンドを作成しました。あとで使うために同時にマウント名前空間を作成しています。

# ./unshare --cgroup --mount bash (cgroupとマウント名前空間を作成)
# echo $$
1237
# cat /sys/fs/cgroup/memory/test01/tasks | egrep "(1237|1220)"
("test01"グループのtasksファイルにPIDが存在することを確認)
1220
1237

名前空間を作成してbashを実行すると、親プロセス(PID:1220)/test01グループに所属していたので、その子プロセス(PID:1237)/test01所属となりました。

ここで/proc/[PID]/cgroupファイルを確認してみましょう。

(作成した名前空間内で確認)
# cat /proc/1220/cgroup | grep memory
11:memory:/
# cat /proc/1237/cgroup | grep memory
11:memory:/

先ほどは/test01に所属していたように表示されていた親プロセスも、子プロセスも、名前空間内で見るとルートに属していると表示されます。

このようにcgroupファイルの中身が、名前空間が作成された時点に所属していたcgroupをルートとしたツリーで見えるようになるのが、cgroup名前空間の機能です。

cgroup名前空間とマウント名前空間の連携

cgroupファイルはcgroup名前空間の機能によって、名前空間内での見え方に変わっていることが確認できました。

しかし、新たなcgroup名前空間を作成しても、マウントは特に変更していませんのでそのままの状態です。cgroupfsを確認すると、

# ls -d /sys/fs/cgroup/memory/test01
/sys/fs/cgroup/memory/test01
# cat /proc/1220/mountinfo | grep memory (親プロセスのマウント情報の確認)
39 27 0:33 /.. /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,memory
# cat /proc/1237/mountinfo | grep memory (自身のマウント情報の確認)
181 160 0:33 /.. /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime - cgroup cgroup rw,memory

依然として/sys/fs/cgroup/memory/test01が存在しており、親環境と同じツリーが見えています。プロセスのマウント情報を格納しているmountinfoファイルでは、4つ目のマウントのルートを表すエントリが/..(ルートディレクトリの親ディレクトリ)とちょっと変な表示になっていますね。

それではcgroupfsをマウントしなおしてみましょう。

まずは、systemdが起動時にすべてのマウント操作を共有する設定を行っているので、マウント操作を行う前にそれを無効にします。これで、マウント名前空間ごとに独立したマウントが見えるようになりますinitがsystemd以外の場合は不要です⁠⁠。

# mount --make-rslave / (名前空間ごとに独立したマウントとする)

そして、アンマウントした後に、再度cgroupをマウントします。

# umount /sys/fs/cgroup/memory (cgroupfsをアンマウント)
# cat /proc/self/mountinfo | grep '/sys/fs/cgroup/memory'
(アンマウントされたことを確認)
# mount -t cgroup -o memory memory /sys/fs/cgroup/memory
(再度/sys/fs/cgroup/memoryにmemoryサブシステムをマウント)

これでマウント情報がどうなったか確認してみましょう。

# cat /proc/self/mountinfo | grep '/sys/fs/cgroup/memory'
115 143 0:33 / /sys/fs/cgroup/memory rw,relatime - cgroup memory rw,memory
(4つ目のエントリも"/"になった)

/proc/self/mountinfoの内容が変わっており、ルートは"/"と表示されていることがわかります。

# find /sys/fs/cgroup/memory/ -type d
/sys/fs/cgroup/memory/ (ルート以下にディレクトリは存在しない)

実際のcgroupfsを見ても、/sys/fs/cgroup/memory以下にはcgroupは存在せず、ルートグループのみが存在する状態になりました。

このようにマウント名前空間と連携して、実際のcgroupfsツリーも/proc/[PID]/cgroupファイルの記載と一致するツリーとなります。

cgroup名前空間内でグループを作成

このcgroup名前空間内でルート直下に新たにグループを作ってみましょう。

# mkdir /sys/fs/cgroup/memory/test02 (名前空間内でtest02グループを作成)

親環境からtest01グループ内を見てみると、

$ ls -F /sys/fs/cgroup/memory/test01 (親の名前空間からtest01内を見てみる)
  :(略)
memory.kmem.tcp.usage_in_bytes      tasks
memory.kmem.usage_in_bytes          test02/

test01グループ直下にtest02グループがありますね。つまり名前空間内では、親と同じcgroupfsツリーを、自身のcgroupをルートとしてそれ以下だけを見せていることがわかります。

cgroup間のプロセスの移動

今度は同じ階層にグループをふたつ作成し、その間を移動して動きを見てみましょう。

# mkdir /sys/fs/cgroup/memory/test0{1,2} (test01,test02グループを作成)
# echo $$
1227
# echo 1227 > /sys/fs/cgroup/memory/test01/tasks (自身をtest01に登録)
# ./unshare --cgroup --mount bash (名前空間を作成)
# echo $$
1254
# cat /proc/1254/cgroup | grep memory (名前空間内ではルートに所属することを確認)
5:memory:/

test01test02というグループを同じ深さの階層(ルート直下)作成し、先ほどと同様にtest01グループにプロセスを登録し、名前空間を作成しました。すると名前空間内ではルートに所属しています。ここまでは先ほどと同じです。

それでは、自身(PID:1254)test02に移動させてみましょう。自身はtest01にいますが、cgroupfsを再マウントしていませんので、まだtest02も見えているはずです。移動はPIDを新たに所属させたいグループのtasksファイルに登録しなおすだけでしたね。

# ls -dF /sys/fs/cgroup/memory/test*
/sys/fs/cgroup/memory/test01/  /sys/fs/cgroup/memory/test02/
# echo 1254 > /sys/fs/cgroup/memory/test02/tasks (test02グループへ移動)
# cat /proc/1254/cgroup  | grep memory (所属するグループを確認)
5:memory:/../test02

test02へ移動後に、cgroupの所属を確認してみると、/../test02となっています。つまりルートはtest01のままで、test01からの相対パスがcgroupファイルに現れます。一度ルートが定まると、名前空間内のツリーから外に移動してもルートは変わりません。

実際にはこのような操作をする意味はないでしょうし、カーネル文書にも推奨されないと書かれていますが、面白い動きですね。

まとめ

今回は新しい名前空間としてcgroup名前空間を紹介しました。

コンテナ内にコンテナ向けのcgroupfsツリーを見せるという少し地味な機能でした。

コンテナ内でも、普通にOSを起動したホスト上でマウントしたcgroupfsと同じような構造に見えるため、特にシステムコンテナ内で自然にcgroupfsを扱えます。cgroupfsを使うsystemdなどのソフトウェアがコンテナ内で実行されても問題ありませんし、コンテナ内でさらにコンテナを起動してもcgroupfsが自然な形で存在していますので、問題なく起動します。

cgroup名前空間がカーネルに実装されため、LXCFSは今後/proc関連の機能のみが使われるようになっていきます。

おすすめ記事

記事・ニュース一覧