「Dockerってなんで仮想マシンより軽いの?」
「namespaceとかcgroupって何をしているの?」
「runcって聞くけど、Dockerとどう違うの?」
Dockerを使い始めた方なら、一度はこんな疑問を抱いたことがあるのではないでしょうか。
Dockerコンテナが軽量で高速な理由は、Linux カーネルの機能を巧妙に組み合わせた仕組みにあります。その中核となるのが、namespace、cgroup、そしてruncという技術です。
この記事では、技術評論社の「[改訂新版]イラストでわかるDockerとKubernetes」を参考に、Dockerの内部構造とコンテナ技術の仕組みについて、初心者にもわかりやすく解説していきます!
「なんとなく使えているけど、中身がよくわからない」から「仕組みを理解して自信を持って使える」へ、一緒にステップアップしていきましょう!
Dockerコンテナの軽量さの秘密
仮想マシンとの比較
まずは、なぜDockerコンテナが軽量なのかを理解するために、従来の仮想マシンと比較してみましょう。
【仮想マシンの構成】
┌─────────────────────────────────────┐
│ 物理サーバー │
│ ┌─────────────────────────────────┐ │
│ │ ホストOS │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────┐ │ │
│ │ │ハイパー │ │VM1 │ │VM2 │ │ │
│ │ │バイザー │ │ ┌─────┐ │ │┌──┐│ │ │
│ │ └─────────┘ │ │ゲストOS│ │ ││OS││ │ │
│ │ │ │ ┌───┐ │ │ │┌┐││ │ │
│ │ │ │ │APP│ │ │ │││││ │ │
│ │ │ │ └───┘ │ │ │└┘││ │ │
│ │ │ └─────┘ │ │└──┘│ │ │
│ │ └─────────┘ └─────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
【Dockerコンテナの構成】
┌─────────────────────────────────────┐
│ 物理サーバー │
│ ┌─────────────────────────────────┐ │
│ │ ホストOS(Linux) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────┐ │ │
│ │ │Docker │ │Container1│ │Cont2│ │ │
│ │ │Engine │ │ ┌─────┐ │ │┌──┐│ │ │
│ │ └─────────┘ │ │ APP │ │ ││APP││ │ │
│ │ │ └─────┘ │ │└──┘│ │ │
│ │ └─────────┘ └─────┘ │ │
│ │ 共有カーネル │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
重要な違い:
- 仮想マシン: 各VMが独自のOSを持つ
- Dockerコンテナ: ホストOSのカーネルを共有
この違いにより、Dockerコンテナは:
- 起動が高速(数秒)
- メモリ使用量が少ない
- オーバーヘッドが最小
コンテナが軽量である理由
Dockerコンテナが軽量な理由は、Linux カーネルの以下の機能を活用しているからです:
- namespace: プロセスの隔離
- cgroup: リソース制御
- Union ファイルシステム: レイヤー化されたファイルシステム
- copy-on-write: 効率的なデータ管理
Linux カーネル機能の活用
重要なポイントは、Dockerは「新しい技術を発明した」わけではなく、既存のLinux機能を組み合わせて使いやすくしたということです。
【Dockerの技術スタック】
┌─────────────────────────────────────┐
│ Docker CLI │ ← ユーザーインターフェース
├─────────────────────────────────────┤
│ Docker Engine │ ← コンテナ管理
├─────────────────────────────────────┤
│ containerd / runc │ ← ランタイム
├─────────────────────────────────────┤
│ namespace + cgroup + ... │ ← Linux カーネル機能
├─────────────────────────────────────┤
│ Linux Kernel │ ← OS基盤
└─────────────────────────────────────┘
つまり、Dockerはこれらの複雑な機能を抽象化し、簡単なコマンドで使えるようにしてくれているのです!
namespaceによるプロセス隔離
namespaceとは何か
namespace(ネームスペース)は、Linuxカーネルの機能の一つで、プロセスが見える世界を制限する仕組みです。
簡単に言うと、「コンテナの中から外の世界が見えないようにする魔法」のようなものです!
各種namespaceの役割
Linuxには複数のnamespaceが存在し、それぞれ異なる側面でプロセスを隔離します:
1. PID namespace
プロセスIDの隔離を行います。
# ホスト側から見たプロセス
$ ps aux | head -3
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 167872 11532 ? Ss Oct01 0:01 /sbin/init
root 2 0.0 0.0 0 0 ? S Oct01 0:00 [kthreadd]
# コンテナ内から見たプロセス(PID 1から開始)
$ docker exec -it mycontainer ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4624 828 ? Ss 10:00 0:00 /bin/bash
root 12 0.0 0.0 34400 2812 ? R+ 10:01 0:00 ps aux
重要なポイント:
- コンテナ内のプロセスは、自分たちだけしか見えない
- コンテナ内の最初のプロセスがPID 1になる
- ホスト側では別のPIDを持つ
2. Network namespace
ネットワークインターフェースの隔離を行います。
# ホスト側のネットワークインターフェース
$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
# コンテナ内のネットワークインターフェース
$ docker exec -it mycontainer ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
14: eth0@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP
3. Mount namespace
ファイルシステムのマウントポイントを隔離します。
# ホスト側のマウント情報
$ mount | grep "^/"
/dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro)
/dev/sda2 on /home type ext4 (rw,relatime)
# コンテナ内のマウント情報(異なるルートFS)
$ docker exec -it mycontainer mount | head -2
overlay on / type overlay (rw,relatime,lowerdir=...)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
4. UTS namespace
ホスト名とドメイン名を隔離します。
# ホスト側
$ hostname
my-server
# コンテナ内(異なるホスト名)
$ docker exec -it mycontainer hostname
a1b2c3d4e5f6
5. User namespace
ユーザーIDとグループIDを隔離します。
# コンテナ内ではroot(UID 0)でも
# ホスト側では一般ユーザーとして実行される
6. IPC namespace
プロセス間通信を隔離します。
実際のコマンド例と動作確認
namespaceの動作を確認してみましょう:
# 新しいPID namespaceでbashを起動
$ sudo unshare --pid --fork --mount-proc bash
# この新しい環境内でpsコマンドを実行
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 22796 5164 pts/0 S 10:00 0:00 bash
root 7 0.0 0.0 38460 3336 pts/0 R+ 10:00 0:00 ps aux
このように、unshare
コマンドを使うことで、Dockerと同様のnamespace隔離を手動で作成できます!
cgroupによるリソース制御
cgroupの基本概念
cgroup(Control Groups)は、プロセスが使用できるリソースを制限・監視するLinux カーネルの機能です。
namespaceが「見えるものを制限」するのに対し、cgroupは「使えるリソースを制限」します。
【cgroupで制御できるリソース】
┌─────────────────────────────────────┐
│ cgroup │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐│
│ │CPU制限 │ │メモリ制限│ │I/O制限 ││
│ │ │ │ │ │ ││
│ │50% CPU │ │512MB │ │100MB/s ││
│ └─────────┘ └─────────┘ └─────────┘│
│ ┌─────────┐ ┌─────────┐ │
│ │ネットワーク│ │プロセス数│ │
│ │帯域制限 │ │制限 │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────────┘
CPU・メモリ・I/Oの制限方法
CPU制限の例
# CPUを50%に制限してコンテナを起動
$ docker run -d --name cpu-limit --cpus="0.5" nginx
# CPU使用率を確認
$ docker stats cpu-limit
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
a1b2c3d4e5f6 cpu-limit 45.67% 10.5MiB / 1.944GiB
メモリ制限の例
# メモリを512MBに制限
$ docker run -d --name mem-limit --memory="512m" nginx
# メモリ使用量を確認
$ docker stats mem-limit
CONTAINER ID NAME CPU % MEM USAGE / LIMIT
a1b2c3d4e5f6 mem-limit 0.01% 5.2MiB / 512MiB
I/O制限の例
# ディスク読み書き速度を制限
$ docker run -d --name io-limit \
--device-read-bps /dev/sda:50mb \
--device-write-bps /dev/sda:10mb \
nginx
Dockerでのcgroup活用例
Dockerは内部的にcgroupを使用してリソース制御を行っています。実際のcgroup設定を覗いてみましょう:
# 実行中のコンテナのIDを取得
$ docker ps --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID NAMES
a1b2c3d4e5f6 my-nginx
# cgroupの設定ファイルを確認(CPU制限)
$ cat /sys/fs/cgroup/cpu/docker/a1b2c3d4e5f6.../cpu.cfs_quota_us
50000
# cgroupの設定ファイルを確認(メモリ制限)
$ cat /sys/fs/cgroup/memory/docker/a1b2c3d4e5f6.../memory.limit_in_bytes
536870912
リソース制限の実践例
本番環境でよく使われるリソース制限の例:
# Webサーバーコンテナ
$ docker run -d \
--name web-server \
--cpus="1.0" \ # CPU 1コア
--memory="2g" \ # メモリ 2GB
--memory-swap="2g" \ # スワップ無効
--restart=unless-stopped \
nginx:alpine
# データベースコンテナ
$ docker run -d \
--name database \
--cpus="2.0" \ # CPU 2コア
--memory="4g" \ # メモリ 4GB
--memory-swappiness=10 \ # スワップ使用を最小限に
--ulimit nofile=65536:65536 \ # ファイルディスクリプタ制限
postgres:13
cgroupバージョンの違い
#### cgroupバージョンの違い
最新のLinuxディストリビューションでは、cgroup v2が使用されています:
bash
cgroup バージョンの確認
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)
cgroup v2での制限確認
$ cat /sys/fs/cgroup/docker/containers/*/memory.max
536870912
## runcとコンテナランタイムの仕組み
### runcの役割と位置づけ
**runc**は、OCIランタイム仕様に準拠した**低レベルコンテナランタイム**です。実際にコンテナを起動・管理する最終的な責任を持ちます。
【Dockerのアーキテクチャ】
┌─────────────────────────────────────┐
│ Docker CLI │ ← ユーザーコマンド
└─────────────┬───────────────────────┘
│ REST API
┌─────────────▼───────────────────────┐
│ Docker Engine │ ← イメージ管理、API提供
│ (dockerd) │
└─────────────┬───────────────────────┘
│ gRPC
┌─────────────▼───────────────────────┐
│ containerd │ ← 高レベルランタイム
│ │ (ライフサイクル管理)
└─────────────┬───────────────────────┘
│
┌─────────────▼───────────────────────┐
│ runc │ ← 低レベルランタイム
│ │ (実際のコンテナ実行)
└─────────────┬───────────────────────┘
│ システムコール
┌─────────────▼───────────────────────┐
│ Linux Kernel │ ← namespace, cgroup等
│ (namespace, cgroup, capabilities) │
└─────────────────────────────────────┘
### OCIランタイム仕様
**OCI**(Open Container Initiative)は、コンテナの標準仕様を策定する団体です。主要な仕様:
1. **Runtime Specification**: コンテナ実行時の仕様
2. **Image Specification**: コンテナイメージの形式
3. **Distribution Specification**: イメージ配布の仕様
runcは、OCI Runtime Specificationに準拠した参照実装です。
#### OCIランタイム設定の例
runcは、以下のようなJSONファイルでコンテナの設定を受け取ります:
json
{
“ociVersion”: “1.0.0”,
“process”: {
“terminal”: false,
“user”: {
“uid”: 0,
“gid”: 0
},
“args”: [“/bin/sh”],
“env”: [
“PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”
],
“cwd”: “/”
},
“root”: {
“path”: “/var/lib/container/rootfs”
},
“linux”: {
“namespaces”: [
{“type”: “pid”},
{“type”: “network”},
{“type”: “mount”},
{“type”: “uts”},
{“type”: “ipc”}
],
“resources”: {
“memory”: {
“limit”: 536870912
},
“cpu”: {
“quota”: 50000,
“period”: 100000
}
}
}
}
### Dockerエンジンとruncの関係
実際にDockerコマンドが実行される流れを追ってみましょう:
bash
1. Dockerコマンドの実行
$ docker run -it ubuntu bash
2. Docker Engine (dockerd) が処理
3. containerdにコンテナ作成を依頼
4. containerdがruncを呼び出し
5. runcがnamespace、cgroupを設定してコンテナを起動
#### runcを直接使用してみる
runcは単独でも使用できます:
bash
1. コンテナイメージからrootfsを展開
$ mkdir -p /tmp/container/rootfs
$ docker export $(docker create ubuntu) | tar -C /tmp/container/rootfs -xf –
2. OCI設定ファイルを生成
$ cd /tmp/container
$ runc spec
3. runcでコンテナを起動
$ sudo runc run mycontainer
これでubuntuコンテナが起動!
root@container:/# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4624 828 ? Ss 10:00 0:00 /bin/bash
#### 他のOCIランタイム
runc以外にも、様々なOCIランタイムが存在します:
- **gVisor (runsc)**: Googleが開発したサンドボックス化されたランタイム
- **Kata Containers**: 軽量仮想マシンベースのランタイム
- **Firecracker**: AWS Lambdaで使用される超軽量ランタイム
bash
gVisorを使用してコンテナを起動
$ docker run –runtime=runsc -it ubuntu bash
Kata Containersを使用してコンテナを起動
$ docker run –runtime=kata-runtime -it ubuntu bash
各ランタイムは異なる特徴を持ちます:
- **runc**: 標準的で高性能
- **gVisor**: セキュリティ重視(パフォーマンスはやや劣る)
- **Kata Containers**: VM級の隔離性能
## 実践!コンテナ技術の理解を深める
ここまでで、Dockerの内部構造について理解が深まったでしょうか?
**今回学んだ要点をまとめると:**
1. **namespace**: プロセスの見える世界を制限(隔離)
2. **cgroup**: プロセスが使用できるリソースを制限
3. **runc**: OCIランタイム仕様に基づいてコンテナを実際に起動
4. **Docker**: これらの複雑な機能を使いやすく抽象化
### 実際の業務での活用
この知識は、以下のような場面で役立ちます:
#### トラブルシューティング
bash
コンテナのリソース使用状況を詳細に確認
$ docker stats –no-stream
cgroupの制限値を直接確認
$ cat /sys/fs/cgroup/memory/docker/*/memory.limit_in_bytes
namespaceの確認
$ sudo lsns -t pid
#### セキュリティ対策
bash
User namespaceを使用してrootレスコンテナを実行
$ docker run –user 1000:1000 -it ubuntu bash
読み取り専用ファイルシステムで起動
$ docker run –read-only -it ubuntu bash
capability を制限
$ docker run –cap-drop=ALL –cap-add=NET_BIND_SERVICE nginx
#### パフォーマンス最適化
bash
CPUアフィニティを設定
$ docker run –cpuset-cpus=”0,1″ -it ubuntu bash
メモリスワップを無効化
$ docker run –memory=1g –memory-swap=1g nginx
I/O優先度を設定
$ docker run –blkio-weight=500 nginx
“`
さらなる学習への道筋
コンテナ技術の理解をさらに深めたい方は、以下のトピックを学習することをおすすめします:
1. コンテナセキュリティ
- AppArmor/SELinux: 強制アクセス制御
- seccomp: システムコール制限
- capabilities: 権限の細分化
2. コンテナネットワーク
- CNI (Container Network Interface): ネットワークプラグイン
- iptables: ネットワークルール管理
- overlay network: 複数ホスト間のネットワーク
3. ストレージ
- Union FS: オーバーレイファイルシステム
- Volume drivers: ストレージプラグイン
- CSI (Container Storage Interface): ストレージ標準
4. オーケストレーション
- Kubernetes: コンテナオーケストレーション
- Docker Swarm: Docker純正クラスター
- Nomad: HashiCorp製オーケストレーター
次のステップの紹介
今回はDockerの内部構造を中心に解説しましたが、これらの技術はKubernetesでも活用されています。
特に、KubernetesのPod内でのコンテナ実行には、今回学んだnamespace、cgroup、runcの仕組みが重要な役割を果たしています。
さらに詳しく学びたい方には、「[改訂新版]イラストでわかるDockerとKubernetes」が強くおすすめです!
この書籍では:
- 視覚的でわかりやすいイラストで複雑な概念を理解
- 実践的な例を通じて実際の動作を確認
- 最新の技術動向(containerd、CRI-O等)も網羅
- 現役エンジニアによる現場の知見が満載
著者の徳永航平さんは、CNCF containerdのレビュワーやMobyプロジェクトのBuildKitメンテナという、まさにコンテナ技術の最前線で活躍されている方です。
「なんとなく使えている」から「仕組みを理解して自信を持って使える」へ。
「トラブルが起きても慌てない」技術力を身につけましょう!
コンテナ技術は奥が深いですが、基本的な仕組みを理解することで、より効果的に活用できるようになります。ぜひ、この記事をきっかけに、さらなる学習を進めてみてください!
コメント