2014/05/12

Docker を支える Linux Kernel の機能 (概要編)

はじめに

Docker はコンテナ型仮想化技術を使ってOSレベル仮想化を実現するコンテナ管理ソフトウェアです。類似のコンテナ管理ソフトとしては、Docker の他にも libvirt、 lxc-tools などがありますが、 Docker には以下の大きな特徴があります。
  • Infrastructure as Code の思想に基づき、コンテナをコード(Dockerfile) で管理できる
  • docker index  で、コンテナイメージを手軽に取得、共有できる
Docker は上記のような特徴を持つため、アプリケーションのポータビリティを大きく向上させることができると期待されています。


大変便利な Docker ですが、Docker によるコンテナ管理は、実は数多くの Linux Kernel の機能により実現されています。今回は Docker を支える Linux Kernel の機能についてご紹介します。
調査対象の Docker Version: 0.11

Docker を支える Linux Kernel の機能を、一枚絵にすると、以下のような図になります。



各機能は、大きく分けて、NamespacesCgroupsStorageNetworkingSecurity に大別できます。それぞれについて、概要を簡単に説明します。


Namespaces

ユーザプロセスが動作する空間を分離する Namespace はコンテナ型仮想化を実現する上で、核となる機能です。これらの機能の多くはParallels 社の OpenVZ チームを中心として開発されました。Docker が利用する Namespace には PID、MNT、IPC、UTS、NETの 5種類があります。

PID Namespace(Kernel 2.6.24)

PID(Process ID) Namespace は、プロセスが動作する空間を生成・分離します。あらたに作られたプロセス空間からは、親空間で動作するプロセスが見えなくなります。 "見えなくなる" というのは、kill() システムーコール等、PIDを指定するシステムコールで親空間で動作するプロセスと通信できなくなる、ということです。親空間からは、生成した空間で動作するプロセスは見えます。

MNT Namespace(Kernel 2.4.19)

MNT(Mount) Namespace は、プロセスに見えるファイルシステムのマウント空間を分離する機能です。mount コマンドで見えるファイルシステムのマウント情報を分離します。

IPC Namespace(Kernel 2.6.30)

IPC(Inter-Process Communication) Namespace は System V IPC(メッセージ・キュー、 セマフォ、共有メモリ) と Posix メッセージキュー の空間を分離します。IPC 関連の識別子が、他の空間からは見えなくなります。

UTS Namespace(Kernel 2.6.19)

UTS(Unix Time-sharing System) Namespace は、uname() システムコールで取得できる情報を分離します。Namespace のうち、最も単純で理解しやすいです。

NET Namespace(Kernel 2.6.29)

NET(Network) Namespace は、ネットワークデバイス、IPアドレス、ルーティングテーブル、iptables 情報を分離します。

Docker は、これらの機能により、プロセス間に仕切りを設け、空間を分離することでコンテナを構成します。
上記以外にも、ユーザおよびグループを分離する User Namespace(Kernel 3.8)という機能もあります。将来的に Docker が利用するかもしれません。


Cgroups

Cgroups はプロセス群に割り当てる計算資源(CPU、メモリ、I/O帯域)を管理する機能です。コンテナに割り当てる資源を調整するために使用します。

cpu

指定した通常優先度(Nice -19 ~ 20) プロセスが使用するCPUの利用割合です。

cpuset

指定したプロセスが動作するCPUを制限します。

memory

指定したプロセスが使用するメモリ量を制限します。

device

指定したプロセスが使用できるデバイス(/dev/*)を制限します。

Docker はこれらの機能を使い、コンテナに割り当てる計算資源を制限します。
上記以外にも ブロックI/O 帯域を制限する blkio subsystem や、プロセス群を停止する freeze subsystem 等があります。将来的には Docker でも使われるでしょう。

Storage

Docker は CoW(Copy on Write) と呼ばれる方式でコンテナイメージ間の差分を扱うことで、無駄なくコンテナイメージを管理します。Docker のコンテナイメージを管理する Storage プラグインでは、以下の Kernel の機能を使っています。

Device Mapper

Device Mapper は、ファイルシステム等が発行するブロック I/O とデバイスのマッピング関係を管理します。Docker は Device Mapper の提供する thin-provisioning の機能と、snapshot 機能を活用しています。個別のファイルシステムに依存しないため、幅広い環境で使用することができます。ファイルシステムより下層にあり、ファイル差分を管理できないため、docker diff コマンドの実行速度は Btrfs と比べ遅くなります。

Btrfs

Btrfs は Linux Kernel に取り込まれているファイルシステムの一つで、先進的な機能を持ちます。Docker は Btrfs の subvolume / snapshot 機能を使い、コンテナイメージの差分を管理します。差分はファイルシステム層で管理されるため、docker diff コマンドの実行速度はとても速いです。使用するには、docker のホームディレクトリが btrfs 形式でないといけません。現在 Docker の Btrfs Plugin は、200行ちょっとなのでとても簡単に読めます。

Aufs

Aufs は union ファイルシステムの一種で、ファイルシステム層で差分を管理できる機能を持ちますが、Linux Kernel のメインラインに入っていないため、今後はあまり使われないでしょう。

Networking

veth

仮想的なネットワークデバイスのペアを作る機能です。Network Namespace と組み合わせ、ホストとコンテナ間での通信に使います。これも OpenVZ チームが中心に開発したものです。

bridge

仮想的なブリッジを作る機能です。上述の veth と組み合わせ、コンテナ間の通信に使います。QEMU /KVM でおなじみと思います。

iptables

コンテナ間の通信を制御(Drop/Accept)するために使います。



Security

Capability

プロセスが持つ特権を細かい粒度で管理する機能です。コンテナ内から、ホストに悪影響を及ぼさないよう制御します。Docker では、例えば、カーネルモジュールのロード、OS時刻の変更などができないよう、デフォルト設定でコンテナ内プロセスの特権を落としています。デフォルト設定:default_template.go

SElinux

強制アクセス制御機能です。Docker ではコンテナ起動時に SElinux MCS ラベルをコンテナに付与し、コンテナ内プロセスの動作をコンテナ内に制限します。

seccomp

プロセスが発行できるシステムコールの種類を制限する機能です。Docker では、--lxc-conf でシステムコールリストファイルを指定してコンテナ内プロセスのシステムコール発行を制限できます。


まとめ

Docker は数多くの Linux Kernel の機能により実現されていることがわかりました。今後、Cgroups、User Namespace、Checkpoint/Restart In Userspace などの実装が進むと思われます。

参考文献

[LWN.net] Namespaces in operation, part 1: namespaces overview
[LWN.net]  LSS Secure Linux container
Linux コンテナ入門


2014/05/08

Docker のビルド方法に見る Golang の利点

以前、Docker をビルドしていて、以下の事実に気づきました。

事実: Docker は自身をビルドするのに Docker を用いてコンテナ内でビルドしている


実際、ソースコード直下に、以下の Dockerfile が置いてあります。中を参照すると、ubuntu のコンテナイメージをベースに、依存するソフトウェアを apt-get したり、git clone で取得したりしています。make コマンドで、依存するソフトウェアをインストールしたコンテナ内で、hack/make.sh を実行し、バイナリを作成します。生成したバイナリをコンテナから取り出してビルド終了となります。

Docker は Golang で書かれていますが、その理由の一つに、Golang の優れたポータビリティ (libc が入っている環境であればどこでも動作するバイナリを手軽に生成できること)があります。これにより、ビルドは Docker 内の固定した環境で行い、生成したバイナリだけを取得しインストールを済ませることができるのです。
参考:Docker and Go: why did we decide to write Docker in Go?

Docker を使ったビルドの利点は、Docker さえ動く環境であれば、どこでもビルドできるため、ビルド環境の構築に手こずらなくて済むことにあります。
誰しも、ソフトウェアのビルド・インストール作業で以下のような苦い経験があるのでは無いでしょうか。

  1. あるソフトウェアの最新のソースコードを取得してくる
  2. ./configure がエラーを吐くので、エラーメッセージを参照し、依存するソフトウェアを yum install する
  3. まだ ./configure がエラーを吐くので、yum-builddep で依存するソフトウェアをまとめてインストールする
  4. まだ ./configure がエラーを吐く。yum-builddep でインストールしたソフトウェア X のバージョンが古いらしい。
  5. ソフトウェア X の最新バージョンをインストールすべく、1. に戻る
Docker 内ビルドが広まると、上記のような作業で手こずることは稀になることでしょう。

Docker 内ビルドの欠点としては、Golang のようにポータビリティに優れた言語を採用しないといけないこと、と、実行バイナリが大きくなってしまうことがあります。

まとめ
  • Docker は自身をビルドするのに Docker を用いている
  • Docker が Golang で書かれている理由に、Golang の優れたポータビリティがある
  • Docker 内ビルドでビルド・インストールにかかる手間が大幅に削減出来る 
余談