名前空間7種とコンテナの正体
一言でいうと
カーネルのソースにstruct containerのようなものはありません。コンテナは、ネームスペース + cgroup + セキュリティレイヤー + rootfsを1つのプロセスに束ねた状態を、人が呼んでいる名前です。そのうちネームスペースが担当する問いは、たった1つです。このプロセスは何を見られるのか、です。
なぜ必要なのか
「コンテナを作る」という表現のせいで、多くの人が、カーネルのどこかにコンテナという箱があると想像します。ところがホストでpsを打ってみると、コンテナの中のプロセスがそのまま見え、カーネルのバージョンもホストとまったく同じに出ます。箱があるなら、こうはなりません。実際に起きていることは、正反対です。プロセスは最初からホストにただ存在していて、カーネルがそのプロセスに対して、リソース一覧の一部を別のバージョンで見せているだけです。分離ではなく、見える範囲の制限です。
この違いは、実務ですぐに表に出ます。コンテナの中でnprocが32と出るのに、実際には0.5コアしか使えない状況、2つのコンテナがそれぞれポート80を開く状況、コンテナの中で消したファイルがホストには残っている状況。これらはすべて、「何が分離され、何が分離されていないか」を知っていれば予測できます。
どう動くのか
ネームスペースは7種類あります。
| 種類 | 分離されるもの |
|---|---|
| pid | プロセス番号の空間。PIDが1になると、シグナル処理とゾンビの刈り取りの責任が伴います |
| net | インターフェース、ルーティングテーブル、iptablesのルール、ソケット、ポート番号の空間のすべて |
| mnt | マウントテーブル。イメージのrootfsが/として見える理由です |
| uts | hostname |
| ipc | System V IPC、POSIXメッセージキュー |
| user | UID/GIDのマッピング。rootlessコンテナの核心です |
| cgroup | 自分のcgroupの位置をルートとして見せます |
netがポート番号の空間まで丸ごと分けるという点が重要です。そのため、2つのコンテナがそれぞれポート80を開いても衝突しません。ポートの衝突は、コンテナの間ではなく、ホストへパブリッシュする瞬間に起きます。
各ネームスペースにはinode番号が付いていて、/proc/<pid>/ns/<종류>をreadlinkすると、その番号が出ます(プレースホルダーはpidと種類です)。同じ番号なら同じネームスペースです。実際に測ってみると、こうなります。
호스트 pid:[4026531836] net:[4026531840] user:[4026531837] time:[4026531834]
alpine pid:[4026532500] net:[4026532562] user:[4026531837] time:[4026531834]
userとtimeだけが同じです。Dockerがデフォルトではユーザーネームスペースを使わないからです。そのため、コンテナの中のrootがホストのrootと同じUID 0になり、これが次のコースで扱うセキュリティ問題の根っこです。
現場での姿
KubernetesのPodは、まさにこの構造をそのまま使ったものです。Podにはpauseという約1MBのコンテナが最初に起動し、このコンテナがnet/ipc/utsのネームスペースを保持します。アプリのコンテナは、新しく作る代わりに、そのネームスペースに参加し、mntとcgroupだけをコンテナごとに別々に持ちます。そのため、同じPodの中のコンテナどうしはlocalhostで通信してポートを共有しますが、ファイルシステムはそれぞれ違います。
Dockerでも、この構造を自分で作れます。--network container:<이름>が、まさに「他のコンテナのnetネームスペースに参加せよ」という命令です(プレースホルダーはコンテナ名です)。サイドカーパターンをDockerだけで再現するときに使うのが、これです。
ネームスペースはいつ消えるのか
ネームスペースはオブジェクトではなく、参照が残っているあいだだけ存在するものです。その中の最後のプロセスが終わり、誰もそれを握っていなければ、カーネルが回収します。この性質を知っていると、コンテナ運用で不思議に見えるいくつかのことが説明できます。
Podが消えたのにネットワーク設定が残っています。どこかがまだそのネームスペースを握っているという意味です。握る方法は3つあります。その中で動いているプロセス、/proc/<PID>/ns/<종류>を開いたままにしているファイルディスクリプター(プレースホルダーはPIDと種類です)、そしてそのパスにかけてあるバインドマウントです。最後のものがip netnsの使う方法で、プロセスがすべて終わっても名前が残っていれば、ネームスペースも残ります。
コンテナを削除したのに名前が残り続けます。同じ理由で、実務では、たいてい後片付けの手順がそのバインドマウントを解除していない場合です。
逆に、この性質を利用することもあります。プロセスが1つもないネームスペースをあらかじめ作っておき、あとでそこに参加させる方式です。先ほど見たpauseコンテナが、まさにその役割を果たします。アプリのコンテナが死んでまた起動しても、ネットワークネームスペースはそのまま生きているので、IPが変わりません。PodのIPがコンテナの再起動に耐える理由が、これです。
もう1つ、指摘しておきます。ネームスペースはリソースを分けません。何を見られるかを決めるだけで、どれだけ使えるかを決めるのはcgroupです。そのため、ネームスペースだけでは、1つのコンテナがCPUを使い切ることを防げません。コンテナが分離されて見えるのは、この2つが一緒にかかっているからで、どちらか片方だけをかけると、その半分はそのまま漏れ出します。
次のラボですること
ホストとコンテナのネームスペースのinodeを自分で比較し、PID 1が誰かを確認し、ホスト名とファイルツリーとインターフェース一覧が、それぞれどのネームスペースのせいで違うのかを1つずつ確認します。最後に、2つのコンテナにnetネームスペースを共有させて、Podの構造を手で再現します。