前章で、カーネルは各プログラムに「あなただけのメモリ世界」という幻想を見せられるようになった。だが、分離できるのはメモリだけだろうか。
もし、プロセスID・ネットワーク・ファイルシステム・ホスト名——あらゆる資源を丸ごと分離できたらどうなる? そのプロセスは、自分がマシンを独占していると信じ込み、まるで独立した一台のサーバーの中にいるように振る舞うだろう。
これこそがコンテナの正体だ。そして、多くの人が誤解している点をはっきり言おう。コンテナは仮想マシンではない。ただの、隔離されたプロセスだ。
仮想マシンとの決定的な違い — 歴史の必然
コンテナを理解するには、まず仮想マシン(VM)との対比が要る。
2000年代、一台の物理サーバーで複数のサービスを動かすため、VMが普及した。VMはハードウェアそのものを仮想化し、その上でゲストOS(カーネルごと)を丸々動かす。分離は完璧だが、代償が大きい。ゲストOSの起動に数十秒、メモリも各VMがGB単位で消費する。
仮想マシン(VM) コンテナ
┌──────┐┌──────┐┌──────┐ ┌──────┐┌──────┐┌──────┐
│ App A ││ App B ││ App C │ │ App A ││ App B ││ App C │
├──────┤├──────┤├──────┤ └──────┘└──────┘└──────┘
│ゲストOS││ゲストOS││ゲストOS│ ← 重い (ゲストOSが無い!)
├──────┴┴──────┴┴──────┤ ┌──────────────────────┐
│ ハイパーバイザ │ │ 共有する1つのカーネル │ ← 軽い
├────────────────────┤ ├──────────────────────┤
│ ホストOS/カーネル │ │ ホストOS/カーネル │
├────────────────────┤ ├──────────────────────┤
│ 物理ハードウェア │ │ 物理ハードウェア │
└────────────────────┘ └──────────────────────┘
コンテナにはゲストOSが無い。ホストのカーネルを共有し、その上でプロセスとして直接動く。だから起動は一瞬(ミリ秒)、メモリも数MBで済む。VMが「一軒家を建てる」なら、コンテナは「一つの家を間仕切りで区切る」ようなものだ。
ではその「間仕切り」は何でできているのか。答えは、Linuxカーネルの2つの機能——namespaces(何が見えるかの分離)とcgroups(どれだけ使えるかの制限)だ。
厨房のアナロジー — シェアキッチンの間仕切り
大きな一つの厨房(ホストOS)を、複数の料理人(コンテナ)でシェアする状況を考えよう。VM方式なら、料理人ごとに独立した厨房を建て増しする——贅沢だが場所も金もかかる。
コンテナ方式は違う。一つの厨房を間仕切りで区切る。
- namespaces(見える範囲の間仕切り): 料理人Aには「自分の調理台・自分の食材棚・自分の流し」だけが見える。隣で料理人Bが作業していても、互いの存在すら見えない。Aにとって、この厨房は自分専用に見える。
- cgroups(資源の割り当て): 「Aはコンロを2口まで、水道は毎分5Lまで」と上限を決める。一人が全部の火力を独占して、他の料理人を止めてしまうのを防ぐ。
私が働いていた飲食店でも、忙しい時間帯は一つの厨房を持ち場(担当エリア)で分けていた。「あなたは前菜、あなたは焼き場」と見える範囲と使える設備を区切ることで、狭い厨房でも複数人が衝突せずに回る。コンテナの隔離は、まさにこの持ち場分けの発想だ。
namespaces — 「何が見えるか」の分離
namespace(名前空間)は、「そのプロセスに、システムの何が見えるか」を分離するカーネルの機能だ。種類ごとに、異なる資源を隔離する。
| namespace | 分離するもの | 効果 |
|---|---|---|
| PID | プロセスID | コンテナ内では自分が PID 1(他のプロセスが見えない) |
| Mount | ファイルシステムのマウント | 独自のファイルツリー(/ から別世界) |
| Network | ネットワークスタック | 独自のIP・ポート・ルーティング |
| UTS | ホスト名 | 独自のホスト名を持てる |
| IPC | プロセス間通信 | 共有メモリ等が隔離される |
| User | ユーザーID | コンテナ内のrootをホストの一般ユーザーに対応づけ |
たとえば PID namespace に入ったプロセスは、ps を実行しても自分と自分の子プロセスしか見えない。ホストでは数百のプロセスが動いていても、コンテナの中からはその存在すら観測できない。第7章の「すべてはファイル」でfdが番号札だったように、namespaceは「見えるものの範囲」そのものを付け替える。
生の syscall でコンテナを「手作り」する
Dockerを使わず、システムコールだけで簡易コンテナを作れる。核心は clone や unshare システムコールに渡すフラグだ。
#define _GNU_SOURCE
#include <sched.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
static char child_stack[1024 * 1024];
/* 子プロセスとして実行される中身 */
int child_fn(void *arg) {
/* この中では、新しい UTS/PID/Mount namespace が有効。
ホスト名を変えても、ホスト側には影響しない */
sethostname("in-container", 12);
printf("コンテナ内 PID: %d\n", getpid()); /* → 1 と表示される! */
execlp("/bin/sh", "sh", NULL); /* シェルを起動 */
return 0;
}
int main(void) {
/* CLONE_NEW* フラグで「新しい名前空間」を要求する = これが隔離の本体 */
int flags = CLONE_NEWUTS /* ホスト名を分離 */
| CLONE_NEWPID /* プロセスIDを分離 */
| CLONE_NEWNS /* マウントを分離 */
| SIGCHLD;
clone(child_fn, child_stack + sizeof(child_stack), flags, NULL);
wait(NULL);
return 0;
}
getpid() が 1 を返すのが決定的だ。ホスト上では、このプロセスは実際には PID 12345 かもしれない。だがPID namespaceの中では「自分が最初のプロセス(PID 1)」に見える。コンテナの独立性は、この見え方の付け替えで作られている。Dockerは、これらのフラグ設定を人間に優しく包んだツールにすぎない。
cgroups — 「どれだけ使えるか」の制限
namespaceは「何が見えるか」を分けるが、資源の量は制限しない。放っておけば、一つのコンテナがCPUを100%使い切り、メモリを食い尽くして、ホストごと巻き添えにする。これを防ぐのが cgroups(control groups) だ。
cgroupsは、プロセスのグループごとにCPU・メモリ・ディスクI/O・ネットワーク帯域の上限を設定する。Linuxでは擬似ファイルシステム(/sys/fs/cgroup/)として公開されており、第7章の「すべてはファイル」らしく、ファイルへの書き込みで制限を設定する。
# メモリ上限を 100MB に制限する例(cgroups v2)
mkdir /sys/fs/cgroup/mygroup
echo 104857600 > /sys/fs/cgroup/mygroup/memory.max # 100MB = 100 * 1024 * 1024
echo $$ > /sys/fs/cgroup/mygroup/cgroup.procs # 今のシェルをこのグループに入れる
# これ以降、このシェルとその子孫は 100MB を超えるとOOM Killerに殺される
CPUの配分も、比率や上限で指定できる。たとえば「このコンテナには全CPU時間の20%まで」と決めるなら、周期あたりの使用可能時間を制限する:
docker run --memory=100m --cpus=0.2 ... というお馴染みのオプションは、内部でこのcgroupsの値を設定しているだけだ。Kubernetesの resources.limits も、辿ればここに行き着く。
Docker が本当にやっていること
ここまでで、Dockerの「魔法」を分解できる。docker run nginx を実行したとき、Dockerが行うのはおおよそ次の手順だ。
docker run nginx
│
├─(1) イメージ層を重ねて root ファイルシステムを用意(OverlayFS)
│ └ これが Mount namespace の「新しい / 」になる
│
├─(2) 新しい namespaces を作成(PID/Net/Mount/UTS/IPC/User)
│ └ 「何が見えるか」を隔離
│
├─(3) cgroups で資源上限を設定(CPU/メモリ/...)
│ └ 「どれだけ使えるか」を制限
│
└─(4) その隔離環境の中で nginx プロセスを起動
└ nginx から見れば、自分専用のサーバーの中にいる
つまりコンテナ= 「独自のファイルシステムを持ち(Mount ns)、独自のプロセス空間・ネットワークを持ち(PID/Net ns)、資源上限を課された(cgroups)、ただのプロセス」 だ。仮想化された別世界に見えるが、ホストの ps aux を叩けば、nginxはホストのプロセス一覧に普通に並んでいる。
この事実は、コンテナの利点も限界も説明する。ゲストOSが無いから軽量で速い。だがカーネルはホストと共有しているから、VMほど分離は強くない(カーネルの脆弱性はコンテナの壁を越えうる)。「軽さ」と「分離の強度」は、このアーキテクチャの必然的なトレードオフなのだ。
イメージが「軽い差分」で済む理由
Dockerイメージが層(レイヤー)を重ねて作られ、共通部分を使い回せるのも、OverlayFSという「差分を重ねるファイルシステム」のおかげだ。ベースの ubuntu 層は複数イメージで共有し、変更点だけを上に薄く重ねる。これも第7章「すべてはファイル」の思想の延長線上にある。
まとめ — 「おまじない」の消し方
- コンテナはVMではない:ゲストOSを持たず、ホストのカーネルを共有する「ただのプロセス」
- namespacesが「見える範囲」を分離:PID・ネットワーク・マウント等を隔離し、自分専用のマシンという幻想を作る
- cgroupsが「使える量」を制限:CPU・メモリ等の上限をファイル書き込みで設定し、資源の独占を防ぐ
- Dockerは包み紙:namespaces + cgroups + OverlayFS の設定を、人間に優しくまとめたツール
- 軽さと分離はトレードオフ:カーネル共有ゆえに軽量だが、VMほど強く隔離はされない
「なぜコンテナは一瞬で起動するのか」「docker run --memory は何をしているのか」——その答えは、カーネルの隔離機能そのものにあった。これでPart 3——カーネルが作る分離と幻想の世界——を一巡した。
次のPart 4では、視点を「一台のマシンの中」から「大量の要求を捌くサーバー」へ移す。1万の接続を同時に相手にするとき、素朴なプログラムはなぜ破綻するのか。次章、C10K問題とepollへ。
「魔法だと思っていたものが、ただの仕組みの組み合わせだと分かるとき、人は初めてそれを支配できる。コンテナは魔法ではない。カーネルが元から持っていた力の、賢い使い方だった。」