Dockerは軽量な仮想化を提供する。
それはどんな仕組みで動いている?
この疑問を解消したくて調べたことを書く。少し理解できた。
結論
自分が理解したことを書く。
- Dockerとコンテナを明確にしよう
- コンテナ技術を簡単に提供するアプリの名がDocker
- つまり理解すべきはコンテナ技術
- コンテナの正体はLinuxの1プロセス。プロセスは上手く隔離して仮想化を実現
- 隔離には
overlayfs,namespace,cgroupなどの技術が使われているoverlayfs: 複数のディレクトリ構成を1つのディレクトリ構成に見せるファイルシステム技術namespace: Linux標準の隔離技術cgroup: Linux標準のリソース制限技術
Dockerとは
Dockerとはコンテナ技術の利用を簡単にしてくれるアプリケーションである。Dockerが有名になりすぎているが、コンテナ技術こそが(一応)本質である。
Dockerはクライアント・サーバ形式を取ったアプリケーションで、それぞれを「Dockerクライアント」と「Dockerデーモン」と呼ぶ。
コンテナ技術を上手く扱ってくれるのは「Dockerデーモン」で、そのデーモンに通信をするクライアントが「Dockerクライアント」だ。
つまりdocker buildなどのコマンドを実行すると、DockerクライアントがDockerデーモンに通信を行い、そこでコンテナ技術を利用したもろもろの処理をやってくれて、その結果をクライアントに返してコンソールに表示している。
アーキテクチャはここに書かれている。
https://docs.docker.jp/get-started/overview.html#docker-architecture
コンテナ技術の仕組み
docker runをすると、コンテナ環境で例えばJavaアプリケーションが実行される。
コンテナ環境は隔離されており、他のコンテナやホストOSに影響を与えない。VMよりも軽量とも呼ばれている。
この隔離された環境である「コンテナ」は一体何者でどうやって実現されているのか。
コンテナの正体
分かったのは、コンテナの正体はただのプロセスだということ。
例えばsudo docker run -d -it centos tail -f /dev/nullコマンドを実行した時、ps -auxなどをホストOSで実行すると以下のような結果が増えている(他にも増えるものはある)。
root 19623 0.0 0.2 23048 2432 pts/0 Ss+ 01:09 0:00 /usr/bin/coreutils --coreutils-prog-shebang=tail /usr/bin/tail -f /dev/null
プロセスを起動しているだけなのに、まるで新たにOSが起動したかのような仮想環境を実現している。どうやって?
コンテナ実現の仕組み
コンテナ(プロセス)の隔離環境はnamespaceとcgroupというLinux標準の機能を使って実現されている。
namespace
概要だけ掴む。namespaceを使うとグローバルリソースを抽象化し、リソースをそのnamespaceで分離する。
https://www.man7.org/linux/man-pages/man7/namespaces.7.html
コンテナはこれを上手く使って隔離環境を実現している。
- プロセス
- コンテナはPID1で起動できる。これはPID空間を隔離しているから
- ファイルシステム
- コンテナは独自のファイルシステムを持つ
- マウントを隔離しているので、ホストや他のコンテナに影響を与えない
- ネットワーク
- 詳しくないけど、コンテナはポートを指定できるのは見たことある
cgroup
cgroupを使うとリソースを制限することができる。
この機能を使ってコンテナがリソースを専有しすぎないように設定できる。
ここは詳しくみない。そういうものがあるんだなぁ程度で。
コンテナのファイルシステムの隔離だけ深堀る
ビルドしたイメージから複数個のコンテナを作っても、それらは上手く隔離されている。
どうなっているのか。
https://qiita.com/zembutsu/items/24558f9d0d254e33088f
上記の記事が詳しいが、自分でもいろいろ動かす。
イメージビルドとoverlayfs
Dockerイメージをビルドする時、以下のようなログが目に入ると思う。
Step 1/5 : FROM ubuntu:latest
---> 174c8c134b2a
Step 2/5 : RUN apt update
---> Using cache
---> b6e9263a9d36
Step 3/5 : COPY sample.txt .
---> Using cache
---> e36bba019980
Step 4/5 : ADD sample2.txt .
---> Using cache
---> c16a93ba7a55
Step 5/5 : CMD ["ls", "/"]
---> Using cache
---> e43247699baf
Dockerイメージはレイヤー状になっているという記事をよくみかける。他にも、レイヤーキャッシュが上手く効くようにDockerfileを書いたほうが良い、という記事も見たことがある。
その通りで、Dockerfileのコマンド定義ごとにコマンドを実際に実行し、その結果はレイヤーに保存される。このログはレイヤー作成のログだったのだ。このレイヤーとはoverlayfsという技術である。overlayfsとはざっくりと、READONLYなレイヤーたちとREADWRITEなレイヤーを重ねて、1つのファイルシステムに見せる技術だ。
例えば先ほどのDockerイメージビルドを例にすると、
- Linuxのディレクトリ構造があるレイヤー
- apt updateの結果を持つレイヤー
- ルートディレクトリに
sample.txtが置かれているレイヤー - ルートディレクトリに
sample2.txtが置かれているレイヤー
がそれぞれ作られる。docker inspect <image_name>をするとさらに実感が湧く。
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/dbf7a66baf0e587fdc994c3a8e99043aa630d20e041f17ae73ca8f9b6258a80d/diff:/var/lib/docker/overlay2/8c954a2ac7582784b65b231cb5d4dcb07f4e2ab8c84c2f3d5533de4cdd9ab40b/diff:/var/lib/docker/overlay2/87e265a5fb0ef24518d3f243d7755998bd3d268d2e7274acdd5c50d084152383/diff",
"MergedDir": "/var/lib/docker/overlay2/dd203d9d44fccb791d3300726a383bc5aabb6433c7b5e1e97fd837358fc276c5/merged",
"UpperDir": "/var/lib/docker/overlay2/dd203d9d44fccb791d3300726a383bc5aabb6433c7b5e1e97fd837358fc276c5/diff",
"WorkDir": "/var/lib/docker/overlay2/dd203d9d44fccb791d3300726a383bc5aabb6433c7b5e1e97fd837358fc276c5/work"
},
"Name": "overlay2"
},
このなかのLowerDirに書かれたディレクトリのうち、1つの中身を覗いてみると、
$ sudo ls /var/lib/docker/overlay2/dbf7a66baf0e587fdc994c3a8e99043aa630d20e041f17ae73ca8f9b6258a80d/diff
sample.txt
sample.txtがあるだけのディレクトリ(これは3番目のレイヤーだ)だとわかる。このように、Dockerfileに書かれた各コマンドは実行され、その結果を保存するレイヤーが生まれるのだ。
そしてコンテナを実行する時は、これらレイヤー(LowerDir)の上にさらに1枚の読み書き可能なレイヤー(UpperDir)を置き、それを含めたすべてのレイヤーをoverlayfsの技術で1つのファイルシステムに見せて(MergedDir)、コンテナ実行時にマウントしている。UpperDirは読み書き可能なので、コンテナ実行中に置きた変更はすべてここに保存される。
これがDockerイメージビルドとコンテナ実行の正体だった。
なぜこんな仕組みに?
ドキュメントを調べたわけではないが、コンテナを複数個起動するときに従来の(?)ファイルシステムだとコンテナの数だけ同じファイルシステムを構築しなければならなくなる。コピーも大量に発生する。
それがoverlayfsを利用すれば、READONLYのレイヤー部分は共通で使いまわして、再度にREADWRITEなレイヤーをコンテナの数だけ用意すれば済むので、効率的と言えそう。
あとはレイヤーはキャッシュできるので、イメージビルドの高速化にも都合が良かったんだと思う。
自分なりのポイント・まとめ
Dockerの中で起きていることを調べたいなと思って探求してみたが、とても面白かった。

- Dockerはコンテナ技術をいい感じにラップして使いやすくしてくれているツールだ
- コンテナの正体はLinuxのプロセスだった。なのでカーネルは共有
namespaceやcgroupなどを使って、いい感じにリソースを隔離している- Dockerイメージのデータは結局ホストにある。
/var/lib/docker/overlay2/... overlayfsという技術を使ってファイルシステムの構築を効率化
Dockerで何が置きているのか全く分かっていなかったが、プロセスを起動しているだけで、結局データはホストOSのディレクトリの中に存在していることが分かると、とても身近に感じられるようになった。
結局cgroupやnamespaceの仕組みなどの深いところまでは理解していないけど、Dockerコンテナの仕組みを軽く知れてよかったと思う。