今回はちょっと趣向を変えて、テーマはDockerです!
kubernetesのソースコードからは少々離れてしまいますが、
コンテナランタイムもkubernetesの重要なコンポーネントの1つなので知っておいて損はないはず…!
アジェンダは以下になります。
コンテナ仮想化技術の歴史とdockerの特徴
コンテナ仮想化技術の歴史
著者は、Dockerで初めて "コンテナ" という言葉を知りましたが、
その概念自体はDocker登場のずっと前から存在する歴史あるものとのことです。
“コンテナ仮想化技術”とは何か?
そもそもコンテナとは何でしょう?
広義のコンテナ仮想化技術とは、
隔離された環境におけるプログラムの実行、を実現する技術
と、定義されることが多いとのことです。
隔離(isolation)とは?
では、この場合の隔離という言葉は具体的に何を意味しているのでしょうか?
Dockerは、Linuxの名前空間機能を使用することで、
以下の6種類のリソースをホストOSから隔離します。
- PID名前空間
- Network名前空間
- User名前空間
- Mount名前空間
- UTS名前空間
- IPC名前空間
またDockerは、cgroups機能を使うことで、LinuxOS上のprocessとして実行されるプログラムに対し、 リソースへのアクセス制限を行います。
コンテナとしてのdockerの特徴
ホストOSのカーネルを共有
「コンテナ上のプロセスの実行に際し、ホストOSとコンテナ間でカーネルを共有する」というのがDockerのもう一つの特徴です。 OSがFedoraのホストマシンの上で、別のディストリビューションであるUbuntuのコンテナが動かせるはこの特徴があってこそといえます。LinuxKernelの Application Binary Interface (以下、ABI) は、常に後方互換性を維持するよう開発されているので、コンテナとホストでディストリビューション等の差異があっても、殆どのケースにおいてその差分は吸収されます。
ここで少し実験してみましょう。Fedoraが動作するホストOS上にて、
ubuntu:latest
イメージのDockerコンテナを起動してみます。
マシン | OS | bash | Linux Kernel |
---|---|---|---|
ホストマシン | Fedora | 4.4.23 | 5.1.6 |
Dockerコンテナ | Ubuntu | 4.4.19 | - |
bash --version
の実行結果から、ホストマシン/コンテナ内にて、bashについては、別々の実行ファイルが実行されていることが確認できます。
しかし、 uname -r
の実行結果は同一のホストOSが持つカーネルを示しています。
上記から、以下のようなケースにおいて、コンテナ上のプログラムは実行に失敗すると推察されます。
可搬性のあるimageとcontainer registry
コンテナ技術としてDockerの最後の特徴は、「Dockerイメージ」という器の中に、 任意のプログラム群をパッキングでき、それをDocker Hubをはじめとするcontainer registry上で push/pull させることができる点です。
では、その「Dockerイメージ」というのは具体的にはどのようなフォーマットなのでしょうか?
docker pull
でイメージレイヤーを取ってきて、docker build
でさらにレイヤーを重ねて〜、というのは何となく理解していましたが、
その実体はと言われるとよくわかってなかったり…
まずDocker Registry上で保持されるDockerイメージですが、distribution manifest というフォーマットで管理されているとのことでした。
distribution manifestはCPUアーキテクチャやVariants別に用意されており、
docker manifest inspect
(要 experiment機能有効化)でそのデータ構造をJSON形式で見ることができます。
amd64のLinuxとラズパイ等で同じDockerイメージを使っているつもりが実体としては別物なんですね。
ユーザーはdocker CLIで取得したDocker Imageをdistribution manifestとは別の image manifest というフォーマットでlocalに展開します。 image manifestはdocker image saveコマンドでtarファイルとして出力できます。tarballを展開すると以下のようなファイルが確認できます。
- layer(layer.tar)
- 実際のdocker imageを構成する断片化したファイル群
- config(Image config JSON)
- イメージビルド時の環境情報(JSON形式)
- manifest.json
- これらのlayer/cofigに対してのリンクを持つindex(目次)
なお、layerのファイル群はUnion File System(UFS)という技術を使用して一つのディレクトリツリーにマージをしてマウントされます。
Dockerの歴史とコンテナランタイム
製品としてのDockerの歴史とコンテナランタイムと呼ばれるdaemon/ライブラリ群に話を移します。 前述したように、Docker自体の構成も登場してから6年程度の中で大きく変わっています。
※画像は以下のスライドからの引用となります
37 Introducing Moby Project: a
dotCloud社(後のDocker社)によりOSSとして公開、その後Moby Projectに移行され、 改修が進んでいく中で、次第にコンポーネントの分割が進み、現在の形となりました。
このようなコンポーネント分割に至った経緯ですが、
- 実装を分割してpluggabilityを向上したい
- 相互運用性を担保するためにI/F仕様を標準化させたい
- (k8sの登場を受け)kubeletからもコンテナのライフサイクル管理の機能を共通ライブラリとして利用できるようにしたい
などの複数の関係者の思惑やニーズがあったのでは?と推測します。
以下、各コンポーネントの解説です。
dockerd
dockerdは、CLI / REST API / docker daemonの3層構造に分かれており、
各コンポーネントは独立した別個の機能/役割を有します。
OCI
containerd及びruncは、dockerd及びkubeletのバックエンドとして、 OCI(Open Container Initiative)に準拠する方式で、実際的なコンテナのライフサイクルの管理を行います。
containerdがハイレベルのコンテナのオーケストレーションを、 runcがローレベルな実際のLinuxとの対話を担当します。
OCI Specでは、OCI Image Format Spec と OCI Runtime Spec の2種が定められており、 Image SpecではContainer Registry上のコンテナイメージの配荷(distribution)の方法が、 Runtime Specではコンテナイメージを実際のLinuxのFilesystem上に展開する際の作法(Bundle)と コンテナの開始から終了に至るまでのライフサイクルがそれぞれ定義されています。
containerd
containerdは前述のように、ハイレベルなコンテナのライフサイクルを管理しています。
デーモンとして動作するよう実装されています。
具体的には下図のように、Container RegistryからのImageの取得(Distribution)、取得したImageのFileSystem上での展開(Bundle)、 コンテナの実体の生成と以降のライフサイクル/コンテナのステートの管理(Runtime Controll)を管理しているようです。
runc
runcはLinuxと直接会話し、コンテナの実体としての各種Namespaceやcgroupなどの操作を行うローレベルなコンテナランタイムです。 libcontainer というライブラリを内部に持っており、そこからcgoというライブラリでC言語のコードを実行し、Systemcallを発行することでLinuxを操作します。
dockerを運用するためのツールたち
以上のようにDockerについて深掘りしていきましたが、仕組みを理解するのはいいけど、実際何がどう動いているか観測できないと物足りない!
ということで、最後にDockerの浅いところから深いところまで覗けるツールをざっくりご紹介します。
bpftraceのバックエンドに使われているeBPFは、11月に2冊の新刊が発売予定ということで、今後の盛り上がりが期待されますね!
参考:
Linux Observability With Bpf: Advanced Programming for Performance Analysis and Networking
https://www.amazon.co.jp/dp/1492050202
BPF Performance Tools
https://www.amazon.com/dp/0136554822
systemd-cgtop
dockerpsns.sh
perf event
docker stats / ctop
csysdig
bpftrace
strace