1.5 コンテナの正体は「隔離されたプロセス」|名前空間・cgroups・overlayfsの仕組みを図解

【第1章】Dockerとは何か

Dockerコンテナは「軽量な仮想マシン」ではありません。実態はLinuxカーネルの3つの仕組みで隔離されたプロセスです。この仕組みを理解すると、コンテナの起動が速い理由・メモリ制限が効く理由・ファイルシステムが独立している理由がすべて説明できるようになります。

本記事では 名前空間(namespaces)・cgroups・ユニオンファイルシステム(overlayfs)の3つをシミュレータと図解で掘り下げます。1-1でVM vs コンテナの違いを学んだ方が次に読む記事として設計しています。


目次

  1. コンテナの正体は「隔離されたプロセス」
  2. 名前空間(namespaces)— 「見える世界」を分離する
  3. PID名前空間:コンテナ内でPID 1が生まれる仕組み
  4. NET・MNT・UTS:ネットワーク・ファイルシステム・ホスト名の分離
  5. cgroups — リソース使用量を制限・計測する
  6. ユニオンファイルシステム — イメージのレイヤー構造
  7. 3つの仕組みが合わさってコンテナになる
  8. コマンドで確認する(Linux環境)
  9. まとめ

1. コンテナの正体は「隔離されたプロセス」

「Dockerコンテナ」という言葉からは仮想マシンのようなイメージを持ちがちですが、実態はずっと単純です。

💡 核心
コンテナは、Linuxカーネルの機能によって「見える世界」と「使えるリソース」を制限された、ただの Linux プロセスです。ゲストOSも独立したカーネルも存在しません。

docker run を実行したとき、内部では次のシステムコールが呼ばれています。

# Docker(runc)が内部で呼び出す主要なシステムコール(概念)
clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC, ...)
# → 新しい名前空間グループを持つプロセスを生成する

clone() は Linux の「プロセスを生成する」システムコールです。フラグによって「どの名前空間を新しく作るか」を指定します。VMのようにハードウェアをエミュレートするのではなく、カーネルが提供する「見せ方の仕組み」を使って隔離するだけです。これがコンテナの起動がミリ秒単位で終わる理由です。

この隔離を実現する仕組みが次の3つです。

仕組み 役割 隔離・制限する対象
名前空間(namespaces) 「見える世界」を分離 PID・ネットワーク・ファイルシステム・ホスト名 など
cgroups 「使えるリソース」を制限・計測 CPU・メモリ・I/O・プロセス数 など
ユニオンファイルシステム(overlayfs) 「ファイルシステム」を分離・重ね合わせ イメージのレイヤー・書き込み可能レイヤー

2. 名前空間(namespaces)— 「見える世界」を分離する

名前空間とは、「カーネルリソースのビュー(見え方)」をプロセスごとにカスタマイズする仕組みです。同じカーネルを共有しながら、各プロセスグループが自分だけの世界を持っているように見せます。

Linux では次の7種類の名前空間が利用できます。Dockerはデフォルトで5〜6種類を使ってコンテナを作ります。

名前空間 フラグ 隔離されるもの Dockerでの使用
pid CLONE_NEWPID プロセスID(PID) 常に使用(コンテナ内PID 1)
net CLONE_NEWNET ネットワークインターフェース・ルーティング・ポート 常に使用(コンテナ独自のIPアドレス)
mnt CLONE_NEWNS マウントポイント(ファイルシステムの見え方) 常に使用(コンテナのルートファイルシステム)
uts CLONE_NEWUTS ホスト名・ドメイン名 常に使用(コンテナ独自のホスト名)
ipc CLONE_NEWIPC SysV IPC・POSIXメッセージキュー 常に使用(プロセス間通信の分離)
user CLONE_NEWUSER UID・GIDのマッピング rootlessモード(Docker 20.10+)で使用
cgroup CLONE_NEWCGROUP cgroupのルートディレクトリの見え方 Linux 4.6+ / Docker 20.10+で使用
💡 なぜ mnt の フラグ名が CLONE_NEWNS なのか?
mount 名前空間はLinuxカーネルに最初に実装された名前空間(2002年・Linux 2.4.19)であり、当時は「名前空間」といえばこれだけでした。そのため省略形の NEWNS(New Namespace)が今もそのまま使われています。

3. PID名前空間:コンテナ内でPID 1が生まれる仕組み

PID名前空間は最もわかりやすい例です。コンテナ内でプロセス一覧を見ると、プロセスが数個しかなく、かつ最初のプロセスのPIDが 1 から始まります。

これは「コンテナが独立したOSを持っている」からではありません。カーネルが「このプロセスグループにはPIDを1から振り直して見せる」というビューを提供しているだけです。

ホスト PID 名前空間
PID COMMAND
1 systemd
2 kthreadd
3 rcu_gp
(多数のカーネルスレッド)
856 systemd-resolved
923 sshd
3842 nginx: master ← 本当のPID
3857 nginx: worker
コンテナ PID 名前空間
PID COMMAND
1 nginx: master process nginx
(← コンテナの “init”)
29 nginx: worker process
↑ ホスト側では PID 3842 だが
 コンテナ内からは PID 1 に見える

↑ 同じ Linux カーネルが両方の「ビュー」を同時に管理している

コンテナ内の PID 1 の重要性

Linuxではプロセス1(init / systemd)が特別な役割を担います。親を失った子プロセス(孤立プロセス)を引き取り、wait() を呼んでゾンビプロセスを回収します。

コンテナ内でも同じ役割が必要です。アプリケーションがゾンビ回収を実装していない場合、コンテナ内のPID 1に直接アプリを起動するとゾンビプロセスが蓄積する問題が起きます。これを解決するのが軽量initプロセス tinidocker run –init フラグで有効化できます)です。

# tini を使ってゾンビプロセスを適切に回収する
docker run --init nginx

# コンテナ内のプロセス一覧(PID 1 が tini になる)
docker exec <container> ps aux
# → PID   COMMAND
# →   1   /sbin/tini -- nginx -g daemon off;
# →   7   nginx: master process
# →  21   nginx: worker process

下のシミュレータで、ホストとコンテナの「見える世界」の違いを体験してみてください。


4. NET・MNT・UTS:ネットワーク・ファイルシステム・ホスト名の分離

NET 名前空間 — 独立したネットワークスタック

NET名前空間により、コンテナは独立したネットワークインターフェース・IPアドレス・ルーティングテーブル・ポート番号空間を持ちます。ホストとコンテナが同じポート番号(例:80番)を同時に使っても衝突しないのはこの仕組みのためです。

ホスト(Linux カーネル)
eth0: 192.168.1.10
docker0: 172.17.0.1(仮想ブリッジ)

↓ docker0 ブリッジ経由で各コンテナの仮想NIC(veth)に接続
コンテナ1
eth0:
172.17.0.2

コンテナ2
eth0:
172.17.0.3

コンテナ3
eth0:
172.17.0.4

↑ 各コンテナが独自の NET 名前空間(ネットワークスタック)を持つ

docker run -p 8080:80 のポートマッピングは、ホストの8080番ポートへのパケットをコンテナのNET名前空間内の80番ポートへ転送するルールを iptables(または nftables)に追加することで実現しています(第5章で詳しく解説)。

MNT 名前空間 — コンテナ独自のルートファイルシステム

MNT名前空間により、コンテナは独自のマウントポイントのツリーを持ちます。コンテナ内で / を参照すると、イメージで定義されたファイルシステムが見えます。ホストの /etc/passwd などは(バインドマウントで明示しない限り)見えません。

UTS 名前空間 — 独自のホスト名

UTS(Unix Time-sharing System)名前空間はホスト名とNISドメイン名を分離します。コンテナ内で hostname を実行するとコンテナIDの先頭12文字が返り、ホストのホスト名とは独立しています。

# ホストのホスト名
hostname
# → server01

# コンテナ内のホスト名(デフォルトはコンテナIDの先頭12文字)
docker exec demo hostname
# → a3f2b1c4d5e6

# --hostname オプションでカスタムホスト名を設定できる
docker run --hostname myapp nginx

5. cgroups — リソース使用量を制限・計測する

名前空間は「見える世界」を分離しますが、リソース(CPU・メモリ・ディスクI/O)の消費量まで制限するわけではありません。1つのコンテナが暴走してホスト全体のメモリを食い尽くすことを防ぐのが cgroups(Control Groups)です。

cgroups はLinuxカーネルのサブシステムで、プロセスグループに対してリソースの「上限設定」「計測」「優先度制御」を提供します。

コントローラー 制限・計測の対象 Dockerオプション例
memory メモリ・スワップの使用量 --memory=256m
cpu CPU時間のシェア・クォータ --cpus=0.5
io(v1: blkio) ディスクI/Oの帯域・IOPS --device-read-bps
pids 生成できるプロセス数(fork bomb対策) --pids-limit=100
cpuset 使用できるCPUコアの番号 --cpuset-cpus=0,1

cgroups v1 と v2

cgroups には2つのバージョンがあり、現在はv2への移行が進んでいます。

比較項目 cgroups v1 cgroups v2(推奨)
マウントパス /sys/fs/cgroup/{コントローラー}/ /sys/fs/cgroup/(統一階層)
デフォルト採用OS CentOS 7、Ubuntu 20.04以前 Ubuntu 22.04+、Fedora 31+、Debian 11+
コントローラー管理 コントローラーごとに別の階層 単一の統一階層
Dockerの対応 Docker 1.x〜(長年の実績) Docker 20.10+
# メモリ256MB・CPU 0.5コアに制限したコンテナを起動
docker run -d --name limited \
  --memory=256m \
  --cpus=0.5 \
  --pids-limit=100 \
  nginx

# コンテナのリソース使用量をリアルタイム確認
docker stats limited

# cgroups v2 環境(Ubuntu 22.04+ など)でのメモリ上限確認
CONTAINER_ID=$(docker inspect --format '{{.Id}}' limited)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
# → 268435456  (= 256 × 1024 × 1024 バイト)
⚠️ OOM Kill(Out of Memory Kill)
コンテナが --memory の上限を超えてメモリを確保しようとすると、Linuxカーネルの OOM Killer がコンテナ内のプロセスを強制終了します。docker inspectOOMKilled フィールドが true になっていれば原因はメモリ不足です。本番環境では docker stats や監視ツールで使用量を把握し、適切な上限を設定してください。

次のシミュレータでメモリ制限の動作を体験してみてください。


6. ユニオンファイルシステム — イメージのレイヤー構造

名前空間とcgroupsで「隔離」と「制限」はできました。残る課題は「コンテナ独自のファイルシステムをどう用意するか」です。VMのようにディスクを丸ごとコピーするのでは時間もスペースも無駄です。

Dockerはここで ユニオンファイルシステム(Union File System)を使います。Docker のデフォルト実装は overlay2(OverlayFS)です。

書き込み可能レイヤー(コンテナ固有・揮発性)
(例: ログ・アプリが生成するファイル) ← コンテナ起動時に追加。削除すると消える

Layer C: myapp コード (5 MB)
← イメージレイヤー(読み取り専用・共有)

Layer B: Python ランタイム (842 MB)

Layer A: ubuntu ベース (78 MB)

↑ OverlayFS がこれらを1つのファイルシステムとして見せる

重要なポイントは3つです。

  • イメージレイヤーは読み取り専用かつ複数コンテナで共有される(ディスク効率が高い)
  • コンテナ内でのファイル変更はすべて書き込み可能レイヤーに記録される(コピーオンライト方式)
  • コンテナを削除すると書き込み可能レイヤーも消える(永続化にはVolumeを使う — 第4章)
💡 イメージレイヤーの共有を体験する
1-1の記事に埋め込んだ

シミュレータで、ubuntu・python・myappの3イメージが同じレイヤーAを共有している様子を確認できます。レイヤーキャッシュの詳細は第3章「3-2. レイヤーキャッシュの仕組みとビルド最適化」で扱います。


7. 3つの仕組みが合わさってコンテナになる

docker run nginx を実行したとき、内部では次のような処理が順番に行われています。

docker run nginx
containerd(コンテナライフサイクル管理)
runc(低レベルコンテナランタイム / OCI準拠)
① clone() システムコール
CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC
→ 新しい名前空間グループを持つプロセスを生成

② OverlayFS マウント
nginx イメージレイヤー(読み取り専用)+ 書き込み可能レイヤーを
MNT 名前空間内の / にマウント

③ cgroup 設定
/sys/fs/cgroup/ 以下にコンテナ用グループを作成
メモリ・CPU上限を設定(指定がない場合は無制限)

④ exec() → nginx プロセスを起動
コンテナの PID 名前空間内では PID 1 として見える

VMとの決定的な違いは、BIOSもブートローダーもカーネル起動も存在しない点です。カーネルはホストのものをそのまま使い、ステップ①〜③はミリ秒〜数百ミリ秒で完了します。これがコンテナの起動が速い理由です。

💡 runc と containerd の関係
runc はコンテナを実際に作る低レベルツール(OCI仕様準拠)、containerd はruncを呼び出してコンテナのライフサイクル(起動・停止・イメージ管理)を管理する上位ランタイムです。docker CLIはcontainerdのAPIを通じてコンテナを操作します。全体のアーキテクチャは 1-2 で詳しく解説します。

8. コマンドで確認する(Linux環境)

ここで説明した内容は実際のコマンドで確認できます。Docker が動いているLinux環境(WSL2含む)で試してみてください。

① コンテナのPIDとnamespaceファイルを確認する

# テスト用コンテナを起動
docker run -d --name demo nginx

# ホスト上でのコンテナの実際のPIDを取得
HOST_PID=$(docker inspect --format '{{.State.Pid}}' demo)
echo "Host PID: $HOST_PID"

# そのPIDの名前空間ファイル一覧(各名前空間へのシンボリックリンク)
ls -la /proc/$HOST_PID/ns/
# → cgroup → cgroup:[4026532456]
# → ipc    → ipc:[4026532458]
# → mnt    → mnt:[4026532459]
# → net    → net:[4026532461]
# → pid    → pid:[4026532460]
# → uts    → uts:[4026532457]
# → user   → user:[4026531837]  ← デフォルトはホストと共有(同じinode)

# ホストのPID 1と比較すると pid/mnt/net/uts/ipc が異なるinode番号になっている
ls -la /proc/1/ns/

② コンテナ内からPIDの見え方を確認する

# コンテナ内のプロセス一覧(PID 1から始まる)
docker exec demo ps aux
# → PID   USER   COMMAND
# →   1   root   nginx: master process nginx -g daemon off;
# →  29   nginx  nginx: worker process

# ホスト上では全プロセスが見える(PIDはHOST_PIDと一致)
ps aux | grep nginx
# → 3842  root   nginx: master process nginx ...
# → 3857  nginx  nginx: worker process

③ cgroupsでリソース制限を確認する

# メモリ256MB制限付きコンテナを起動
docker run -d --name limited --memory=256m nginx

# cgroups v2 環境(Ubuntu 22.04+ など)での確認
CONTAINER_ID=$(docker inspect --format '{{.Id}}' limited)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
# → 268435456  (= 256 × 1024 × 1024 バイト = 256 MiB)

# リアルタイムのリソース使用量モニタリング
docker stats limited
# → CONTAINER   CPU %   MEM USAGE / LIMIT   MEM %
# → limited      0.0%   8.5MiB / 256MiB     3.3%

# OOM Killされたか確認する
docker inspect --format '{{.State.OOMKilled}}' limited
# → false(問題なし)/ true(メモリ超過で強制終了)
💡 Windows / macOS で試す場合
Docker Desktop for Windows(WSL2バックエンド)・macOSでは、コンテナがLinux VM内で動作するため /proc/{PID}/ns//sys/fs/cgroup/ はVM内のパスになります。docker exec demo cat /proc/self/cgroup を実行すると、コンテナが属しているcgroupのパスを確認できます。

9. まとめ

コンテナは「軽量VM」ではなく、Linuxカーネルの3つの機能によって隔離・制限されたプロセスです。

仕組み 何を分離・制限するか 「速い・軽い」への貢献
名前空間 PID・ネットワーク・ファイルシステム・ホスト名(7種類) カーネルを共有するためゲストOS起動が不要
cgroups CPU・メモリ・I/O・プロセス数 必要な分だけリソースを割り当てられる
overlayfs ファイルシステム(レイヤー重ね合わせ) イメージを共有しディスクを節約・起動を高速化
✅ 次のステップ
1-2 では、Docker全体のアーキテクチャ(Docker Client・Docker Daemon・containerd・runc)を俯瞰します。今回学んだ runc がどこに位置づけられ、docker コマンドを打ってからコンテナが起動するまでの全体像を把握できるようになります。

参考リンク


Dockerの基礎を動画で体系的に学びませんか?

実務で使う基礎だけを3時間に凝縮。環境構築から丁寧に解説しています。

Udemy Docker入門講座 クーポン割引で講座を見る →

コメント

タイトルとURLをコピーしました