Dev Study
← 仮想化とクラウドの土台
レッスン 4 / 10·約 9 分·第8期 仮想化とクラウドの土台

コンテナはVMではない — namespace で「見える世界」を分ける

アプリと DB の下に何が積まれているか(左が下、右が上)
  1. 物理機リージョン/AZ
  2. ハイパーバイザVM exit で捕まえる
  3. VMゲストOS
  4. コンテナnamespace / cgroup今ここ
  5. プロセスアプリ / DB

ここまではハードウェアの仕掛けで1台を分けてきました。ここからはカーネルの中の表で分けます。同じ「分ける」でも機構が別です。

まず結論

コンテナの中で動くのは、ホストのカーネルをそのまま使う普通のプロセスです。ゲストOSは無く、CPUの特権命令も1つも捕まえていません。

分けているのはカーネルが持つ表です。PIDの表、マウントの表、ネットワークの表をプロセスごとに差し替える。それが namespace で、定義は「グローバルな資源を包み、中のプロセスには自分専用に見せる」抽象です。

? 読む前に予測

コンテナの中で `ps` を打つと自分のアプリが PID 1 でした。ホスト側にも別の PID 1 があります。同じ番号が2つ存在できるのはなぜでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

カーネルは1つのまま、表だけが増える

前回まで、1台を分ける方法をハードウェアの側から見てきました。コンテナはその延長にありません。コンテナの中で動くのは、ホストのカーネルをそのまま使う普通のプロセスです。ゲストOSは無く、VM exit も起きません。

分けられているのは、カーネルが持つ資源の一覧表です。namespaces(7) の定義はこうです。

A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. Changes to the global resource are visible to other processes that are members of the namespace, but are invisible to other processes. One use of namespaces is to implement containers.

最後の一文が効きます。コンテナは namespace の用途の一つであって、namespace がコンテナのために作られたのではありません。種類は8つです。

namespaces(7) の Namespace types(原文の折り返しを詰め、Page 列を省いた)表
Namespace Flag Isolates
Cgroup CLONE_NEWCGROUP Cgroup root directory
IPC CLONE_NEWIPC System V IPC, POSIX message queues
Network CLONE_NEWNET Network devices, stacks, ports, etc.
Mount CLONE_NEWNS Mount points
PID CLONE_NEWPID Process IDs
Time CLONE_NEWTIME Boot and monotonic clocks
User CLONE_NEWUSER User and group IDs
UTS CLONE_NEWUTS Hostname and NIS domain name

光らせた Network・Mount・PID が、このあと実機で確かめる3つです。

追加時期はばらばらです。clone(2) と unshare(2) のフラグ見出しでは mount が Linux 2.4.19、PID と network が 2.6.24、cgroup が 4.6、time が 5.6。一斉に設計された機能ではなく、10年以上かけて別々に足された機構の集まりです。

具体実物

コンテナの中を実際に覗く

Docker Desktop 4.47.0 / Engine 28.4.0(macOS 上、カーネル 6.10.14-linuxkit)で素の Alpine を起動した出力です。

$ docker run --rm alpine ls -l /proc/self/ns(実機、全量。番号は実行ごとに変わる)コマンド出力
total 0
lrwxrwxrwx 1 root root 0 Sep 16 05:08 cgroup -> cgroup:[4026532964]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 ipc -> ipc:[4026532962]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 mnt -> mnt:[4026532960]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 net -> net:[4026532965]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 pid -> pid:[4026532963]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 pid_for_children -> pid:[4026532963]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 time -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 time_for_children -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Sep 16 05:08 uts -> uts:[4026532961]

角括弧の中は namespace の識別番号(inode)です。cgroup・ipc・mnt・net・pid・uts は 40265329xx の新しい番号を持つ一方、time と user だけ桁が違います。ホストと同じ namespace を使う印で、この構成では時刻とユーザIDが分けられていません。末尾が _for_children の2行は「次に作る子が入る namespace」で、ここでは pid・time と同じ番号です。

同じ実行のカーネル版とプロセス一覧です。

同じコンテナ内での ps -ef と uname -r(実機、抜粋)コマンド出力
PID USER TIME COMMAND
1 root 0:00 sh -c ls -l /proc/self/ns; ...
9 root 0:00 ps -ef
 
6.10.14-linuxkit

sh が PID 1 です。 uname -r が返したのは、このコンテナを動かす Linux(Docker Desktop なら macOS 上の Linux VM)のカーネル版そのもの。コンテナ専用のカーネルはありません。

pid_namespaces(7) は、この PID 1 がただの1番でないと規定します。

The first process created in a new namespace ... has the PID 1, and is the "init" process for the namespace (see init(1)).

If the "init" process of a PID namespace terminates, the kernel terminates all of the processes in the namespace via a SIGKILL signal.

PID 1 が終わった瞬間、同じ namespace の全プロセスがカーネルに終了させられます。「1つ終わっただけで全部落ちた」と見える挙動は、この規定どおりの結果です。

✓ ここまでの確認

コンテナの中で `uname -r` を打つと、ホストと同じカーネルの版が返りました。何を意味しますか。

抽象原理

「分ける」の正体は、参照する表の差し替え

一般化します。namespace がやるのは、プロセスが参照する表を差し替えることだけです。PID namespace ならプロセス番号の表、network namespace ならデバイス・ルーティング表・ポート番号の一式。

カーネルの実体は1つ。違うのは「どの表を引くか」だけです。だから特権命令は1つも捕まえていませんし、アドレス変換も増えていません。

network_namespaces(7) は分ける対象を列挙します。

Network namespaces provide isolation of the system resources associated with networking: network devices, IPv4 and IPv6 protocol stacks, IP routing tables, firewall rules, the /proc/net directory ..., port numbers (sockets), and so on.

ルーティング表もポート番号も namespace ごとに別の表です。同じ 5432 番が、別の namespace では誰も待たない番号になります。

1つのカーネルが、プロセス群ごとに別の表を引く
1つのカーネル特権命令の捕捉は無い
引く表を差し替える
PID表A / mount表A / net表APID 1 から振り直し
PID表B / mount表B / net表Bここにも別の PID 1
プロセス群AコンテナA の世界
プロセス群BコンテナB の世界
  • ・表が別なので、同じ番号(PID 1、ポート 5432)が別のものを指す
  • ・カーネルは共有なので、その不具合は両方に及ぶ
  • ・user と time は分けない構成もある(上の実機出力がその例)
具体実物

同じイメージ、違う見え方

同じ Alpine イメージ、同じ ps -ef で、PID namespace を分けるかどうかだけを変えます。

$ docker run --rm --pid=host alpine ps -ef(実機、抜粋。「ホスト」は Linux VM)コマンド出力
PID USER TIME COMMAND
1 root 0:57 /init
2 root 0:00 [kthreadd]
3 root 0:00 [pool_workqueue_]
4 root 0:00 [kworker/R-rcu_g]
15 root 0:35 [ksoftirqd/0]
16 root 2:19 [rcu_sched]

先ほどは自分の sh だけが PID 1 でした。今回は VM の /init が PID 1 として並び、カーネルスレッドまで見えます。イメージもコマンドも同じです。見えなくしていたのは namespace であって、イメージでもランタイムでもありません。

ネットワークも同じです。

コンテナ内の ip addr と、同時刻のホスト(Linux VM)側の ip link(実機、抜粋)コマンド出力
# コンテナ内
11: eth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 65535
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
 
# ホスト(Linux VM)側
75: vethe442d4f@ip6_vti0: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> master docker0
16: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 65535
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

コンテナ側の eth0@if75 とホスト側の 75: vethe442d4f が番号で対になっています(名前と番号は実行ごとに変わります)。network_namespaces(7) が「パイプのような抽象」と呼ぶ veth の片端がコンテナの namespace に、もう片端がブリッジ docker0(172.17.0.1)に刺さる構図です。

第18週『ルーティング表 — 複数の行が当てはまったら、どれを選ぶのか』の現場ノートで、Docker が 172.17.0.0/16 の行を足して社内DB宛てのパケットを吸い込む例を見ました。その行は、ここで見た namespace と veth とブリッジの帰結で、ブリッジを1本立てたから必要になった行です。

✓ ここまでの確認

PID namespace を分けたコンテナで PID 1 のプロセスが終了すると、何が起きますか。

⇄ 2つを見比べる
具体例 A

仮想メモリの番地(第9週)

2つのプロセスが同じ仮想アドレスを持っても、行き先の物理メモリは別です。変換表がプロセスごとに別で、表に載っていない番地には到達できません。

同じ仮想アドレスを2つのプロセスが持つとき表
プロセスA プロセスB
仮想アドレス 同じ値 同じ値
変換表 Aの表 Bの表
物理番地 別の場所 別の場所
具体例 B

PID namespace の番号

2つのコンテナが共に PID 1 を持ちます。番号空間が namespace ごとに分かれるためで、pid_namespaces(7) は「異なる PID namespace のプロセスは同じ PID を持ちうる」と書きます。

同じ PID 1 を2つのコンテナが持つとき表
コンテナA コンテナB
PID 1 1
PID表 Aの表 Bの表
実際の実体 別のプロセス 別のプロセス

仮想メモリの番地と、コンテナの PID。層も年代も違いますが、同じ番号が別のものを指せる理由は共通です。何が違うから、そうなるのでしょうか。

「番号を見て行き先を決める」とき、その間に何が挟まるかを両方で書き出してみてください。

この章のまとめ

  • コンテナの中身は、ホストのカーネルを使う普通のプロセス。ゲストOSも特権命令の捕捉も無い。
  • namespaces(7) の定義は「グローバルな資源を包み、中のプロセスには自分専用に見せる」抽象。用途の一つがコンテナであって、コンテナ専用の機能ではない。
  • 種類は cgroup / IPC / network / mount / PID / time / user / UTS の8つで、mount 2.4.19・PID と network 2.6.24・cgroup 4.6・time 5.6 と別々の時期に足された。
  • 分けているのは参照する表。同じ番号が別のものを指すのは、第9週の仮想メモリと同じ構造。
  • PID 1 が終了すると、カーネルが namespace 内の全プロセスを SIGKILL する(pid_namespaces(7))。
  • カーネルは共有のまま。「見える範囲を分けた」と「境界を越えられない」は別の主張。

この先の学びにどう繋がるか

今回は「何が見えるか」を分けました。次は「どれだけ使えるか」を配る cgroup です。見える範囲を分けても、CPUとメモリは奪い合いのままだからです。

問題で固めるなら、クイズの 仮想化とコンテナ隔離の原理 の「仮想化III(Linux名前空間)」と OS・データベース の「OS III(並行処理とI/O・コンテナ)」が対応します。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

namespace について、man-pages の定義に照らして正しいものはどれですか。

用語集(4語)
namespace
グローバルな資源を包み、中のプロセスには自分専用に見せるカーネルの抽象(namespaces(7))。cgroup / IPC / network / mount / PID / time / user / UTS の8つ。
PID namespace
プロセスID の番号空間を分ける namespace。最初のプロセスが PID 1 となり init として振る舞い、それが終了するとカーネルが配下を SIGKILL する。
network namespace
デバイス・プロトコルスタック・ルーティング表・ファイアウォール規則・ポート番号などを分ける namespace。物理NICは1つの namespace にしか属せない。
veth
仮想ネットワークデバイスの対。network_namespaces(7) が「パイプのような抽象」と説明する。片端を別の namespace に置ける。