Dockerと名前空間
導入
最近大学の研究で忙しいので息抜きでDockerについても少し深掘ってみた.
Dockerって一言で何?
Dockerはアプリケーションコンテナの実行環境の事を指し, 仮想化マシンを用いる事なく,名前空間による分離によって独立したリソースとして管理できるソフトウェアである.
本記事ではDocker, 特にDocker Engineとその基礎概念である名前空間についての解説を行う.
Docker Engine とは
Dockerを構成する主な要素としてDocker Engineがある.
Docker Engineは以下のように
- Docker CLI
- REST API
- Docker Deamon
の3つの要素で構成されている.

Dockerのアーキテクチャは以下のようになっている.

これがそれぞれ何をしているかというと
Docker CLI
Docker CLIは文字の通りユーザーに対してコマンドインターフェースを提供しており, Docker Deamonに対してのリクエストを転送する事ができる.
ex)
docker run
docker push
docker pull
REST API
Docker CLIはユーザーからのコマンドを受け取り、それをHTTPリクエストとしてDockerデーモンに転送する. したがって内部的にはDocker CLIとDocker DeamonmはDocker RestAPIを介してコミュニケーションを行っている.
また,REST APIのインターフェイスを用いてユーザーは直接Docker Deamonとやり取りを行うことも可能である.
このREST APIはunix domain socketを用いて通信を行う. DOCKER_HOSTという環境変数の中にドメインソケットが定義されている.
- 確認方法
echo $DOCKER_HOST
これで以下のように表示されたことから
unix:///Users/takashi/.docker/run/docker.sock
ソケットは
/Users/takashi/.docker/run/docker.sock
に定義されている事がわかる.
(Linuxでは.docker/run/docker.sockに定義されてた.)
このソケットを使用してDocker Deamonにアクセスする事ができる.
したがって
curl --unix-socket /Users/takashi/.docker/run/docker.sock http://v1.43/containers/json | jq
によってコンテナの一覧を得る事ができる. ここでAPIのエンドポイント一覧が見れる.
ここでunix socketを使用しているのでUDPプロトコルを使用してことから高速にデータ通信ができている.
TCPとUDPの違いについては以下.
Docker Deamon
Docker DeamonはDockerオブジェクトを作成・管理する. Dockerにおいてコア機能を持つ
(イメージ, コンテナ, ネットワーク, データ・ボリュームなど)
名前空間
さっき説明したようにDockerは名前空間による分離によって独立したリソースとして管理している. この名前空間はカーネルの機能であるnamespaceの事である.
| 名前 | 意味 |
|---|---|
| pid 名前区間 | プロセスの分離に使います(PID:プロセス ID) |
| net 名前区間 | ネットワーク・インターフェースの管理に使います(NET:ネットワーキング) |
| ipc 名前区間 | IPC リソースに対するアクセス管理に使います(IPC:InterProcess Communication、内部プロセスの通信) |
| mnt 名前区間 | マウント・ポイントの管理に使います(MNT:マウント) |
| uts 名前区間 | カーネルとバージョン認識の隔離に使います(UTS:Unix Timesharing System、Unix タイムシェアリング・システム) |
| https://docs.docker.jp/v1.12/engine/understanding-docker.html |
pid 名前空間
pidはprocess idである.
まずLinuxシステム上でのプロセスはroot pid nsという名前空間に属する.
以下コマンドでプロセスIDの一覧(pid テーブルを表示できる)
ps -e -o pid,ppid,command
ここでDockerでは異なるプロセスの名前空間をroot pid nsの子名前空間として作る事ができる.
以下のように子名前空間1, 子名前空間2はそれぞれ異なる名前空間で管理する. このそれぞれの名前空間のコンテナという単位で管理しているのでコンテナからローカルのプロセスを見ることはできないし, コンテナから他のコンテナのプロセスを見ることもできない.

このpid名前空間をDockerを使わずに試すにはunshareコマンドを用いて行う事ができる.
unshareコマンドによって新しい名前空間を作成する. そして --forkオプションにより新しいプロセスのフォークをしmemoryの割り当て, --pidオプションにより新しいプロセス名前空間を作成することにより親名前空間とは隔離したpidをもたせた. --mount-procによって/procをマウントする. これをしないとps -e -o pidを実行した時に,親の名前空間の/procが参照されてしまい, 子名前空間から親名前空間が覗けてしまう.
最後に新しい名前空間でbashプロセスを開始するためである.
sudo unshare --fork --pid --mount-proc bash
新しい名前空間上でプロセスの一覧を表示しようとすると
root@takashi-desktop:/home/takanao# ps ax
PID TTY STAT TIME COMMAND
1 pts/1 S 0:00 bash
16 pts/1 R+ 0:00 ps ax
となっており, bashプロセスと現在実行したps axしかプロセスとして動いていないことがわかる.
しかし, root pid ns側から見てみると
takasi@takashi-desktop:~$ ps ax | wc -l
319
と319プロセスが走っている事がわかる.
ここで親名前空間から子名前空間のunshareプロセスを覗いてみる.
takashi@masudalab-takashi:~$ pstree -p | grep unshare
|-sshd(36241)-+-sshd(70500)---sshd(70628)---bash(70654)---sudo(71124)---sudo(71125)---unshare(71126)---bash(71127)
bash(71127)
これはさっきunshareプロセスから走らせたbashプロセスである.root@takashi-desktop:/home/takanao# ps ax PID TTY STAT TIME COMMAND 1 pts/1 S 0:00 bash 16 pts/1 R+ 0:00 ps ax
ここで見た時はPidは1になっていた.このことから親名前空間から見ると実際には異なるPidで管理されている事がわかる.
下のようにそれぞれの名前空間からさっき作成した子名前空間は"4026532621"という識別子を持っている事がわかる.
takashi@takashi-desktop:~$ sudo ls -l /proc/71127/ns/pid
[sudo] password for takashi:
lrwxrwxrwx 1 root root 0 Jan 7 07:48 /proc/71127/ns/pid -> 'pid:[4026532621]'
root@takashi-desktop:/home/takashi# ls -l /proc/1/ns/pid
lrwxrwxrwx 1 root root 0 Jan 7 07:43 /proc/1/ns/pid -> 'pid:[4026532621]'
上記はpid名前空間の分離について解説したが, このような名前空間の分離を以下の名前空間全てで行っているという事である.
名前 意味 pid 名前区間 プロセスの分離に使います(PID:プロセス ID) net 名前区間 ネットワーク・インターフェースの管理に使います(NET:ネットワーキング) ipc 名前区間 IPC リソースに対するアクセス管理に使います(IPC:InterProcess Communication、内部プロセスの通信) mnt 名前区間 マウント・ポイントの管理に使います(MNT:マウント) uts 名前区間 カーネルとバージョン認識の隔離に使います(UTS:Unix Timesharing System、Unix タイムシェアリング・システム)
試しにpid: 71127がどのような名前空間に属しているのか見てみる
takashi@takashi-desktop:~$ sudo ls -l /proc/71127/ns
[sudo] password for takashi:
でプロセス71127が属する名前空間の一覧を出してみると以下のようになる.
lrwxrwxrwx 1 root root 0 Jan 7 07:48 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 mnt -> 'mnt:[4026532620]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 net -> 'net:[4026531840]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 Jan 7 08:36 pid_for_children -> 'pid:[4026532621]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Jan 7 08:36 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 Jan 7 07:48 uts -> 'uts:[4026531838]'
cgroup, ipc, mnt, net, pid, pid_for_children, time, time_for_children, user, utsという名前空間に属す様子.
Docker Engineを支えるソフトウェア群
Docker Engineのアーキテクチャは以下のようになっている.

ここからDockerは内部的にContainerd, Runcを使用している事がわかる.
Containerdは高レベルランタイム, RunCは低レベルランタイムとしてコンテナランタイムとしてそれぞれの抽象レベルで機能している.
Containerd, RuncはDocker社が開発したものであり,
Containerd
元々containerdはDockerの下でコンテナの管理機能を提供するDeamonとしてDocker社によって開発されたもので
その後独立したプロダクトとして2017年にCNCF(Cloud Native Computing Foundation)に移行した様子.
その後Docker Engineの内部技術としてコンテナ管理の基本動作をサポートしていたところからDocker Engineそのものの機能をカバーするようになっている. したがってKubernetesからのDockerとContainerdの使用は以下のようになっていてk8s v1.20からDockerの使用は非推奨に.
Runc
Runcに関してもDocker社が開発したものであり, 同じく独立したプロジェクトとしてOCIによって標準化されている.
このRuncはコンテナを起動しOS Kernelと連携してコンテナ化プロセスをサポートする.
OCI (Open Container Initiative)が
という Container Runtimeの仕様を標準化し
Dockerの内部技術を分離して取り出されたのがRuncである.
DockerImage
Docker Imageはコード, ランタイム, ライブラリ, 環境変数, 設定ファイルなどを含んだコンテナのテンプレートのようなものである. このDocker Imageは
OCIによって標準化された
というContainer Imageの標準にそっている.
感想
標準化ってすごい大事だなとこれみて思う. 企業が独自の規格でやりすぎるのは全体の発展速度を遅める原因になりそう.
Discussion