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 内ビルドでビルド・インストールにかかる手間が大幅に削減出来る 
余談

2014/02/26

Erlang VM(BEAM) スレッド構成

Erlang の実行環境である BEAM の動作を理解するため、BEAM のスレッド構成を調査しました。

BEAM は SMP(マルチコア) 環境と非 SMP 環境では動作が大きくことなります。SMP環境と非SMP環境に分けてスレッド構成を記載します。
調査対象の OTP のバージョンは R16B03-1です。

非SMP環境

Erlang Interactive Shell を起動する際に、オプションとして '-smp disable' を付与すると、CPUはSMPでも、BEAMとしては非SMPモードで起動できます。
'erl -smp disable' で起動すると、11個のスレッドが見つかりました。11スレッドの内訳は以下のようになります。
スレッド名関数名個数
Main Threadprocess_main1
Async Threadasync_main10

Main Thread
 BEAM byte-code を解釈し、実行するスレッドです。Erlang プロセスをスケジュールします。

Async Thread
 Erlang プロセスによるファイル操作を非同期に行います。プロセスが file モジュールを通じてファイルの読み書きや開閉を行うと、Main Thread に代わってAsync Threadがそれらの処理を請け負います。byte-code を解釈実行する Main Thread の動作を止めないために、処理を肩代わりしているのです。スレッドの起床は futex()システムコールでおこないます。
 Async Thread の個数は erl 起動時に '+A' オプションで変更できます。例えば、'erl +A 5' とすると、Async Thread は5個になります。ちなみに riak はデフォルトで64個起動します。

SMP環境

 SMP環境では些か構成が複雑になります。オプションなしで erl を起動すると、論理4core(物理2core)環境では19スレッドできました。内訳は以下になります。

スレッド名関数名個数
Main Threaderts_sys_main_thread1
Signal Handling Threadsignal_dispatcher_thread_func1
System Message Handling Threadsys_msg_dispatcher_func1
Async Threadasync_main10
Child Waiting Threadchild_waiter1
Scheduling Threadsched_thread_func4
Aux Threadaux_thread1

Main Thread
 非SMP環境とは異なり、Erlang プロセスの実行はしません。単にシグナルを受信して、pipe経由でSignal Handling Thread に通知するだけのスレッドです。select(0, NULL, NULL, NULL, NULL) で待ちぼうけです。

Signal Handling Thread
 シグナルハンドラ本体です。Main Threadが受信したシグナルに相当するハンドラを起動します。erl 起動時に '+B' オプションでシグナル受信時の挙動を変更できます。例えば 'erl +B i' でブレークシグナルを無視するようになります。

System Message Handling Thread
 システムメッセージのハンドラです。システムメッセージは、トレース情報の出力やプロセスの再開・中断等をリクエストする特殊なメッセージです。詳しくは sys module のドキュメントを参照ください。

Async Thread
 非SMP環境と同様の非同期I/Oスレッドです。

Child Waiting Thread
 「OTP-3906 : Solaris で子スレッドが大量に終了した際、 SIGCHLD がうまく伝わらない問題」を修正するため、子スレッドの終了を waitpid() で待ち受けます。

Scheduling Thread
 process_main() を実行し、 byte-code 解釈実行、プロセススケジューリングを行います。デフォルトでは論理コアと同じ数だけ生成されます。'+S' オプションでスレッド数を調整できます。他の Scheduling Thread と比較して負荷が偏らないようにバランシングとプロセスマイグレーションも行います。

Aux Thread
 若干時間のかかる処理を受け持つ補助的なスレッドです。メモリアロケーションやGCの情報を取得する際等に、Scheduling Thread から処理をオフロードされます。例えば、'elrang:statistics(garbage_collection)' でのGC統計情報取得は、aux_threadで行われます。

通常 erl 起動時に作られるスレッドは以上ですが、他にも、NIF や driver 関係のスレッドがあります。

まとめ

 ・Erlang VM(BEAM)のスレッド構成はSMP/非SMPで大きく異なる
 ・Scheduling Thread の動作を阻害しないために、一部処理が他スレッドにオフロードされる

参考文献

Erlang User's Guide: erl
Erlang/OTP のソースコード (otp/erts)

2014/01/16

Erlang/OTP crypto モジュールエラー on Fedora 19

Fedora 19 環境で自分でインストールした Erlang/OTP を使って、rebar を利用したところ、以下のようなエラーが出ました。

Uncaught error in rebar_core: {'EXIT',
                              {undef,
                                  [{crypto,start,[]},
                                   {rebar_core,run,1},
                                   {rebar,main,1},
                                   {escript,run,2},
                                   {escript,start,1},
                                   {init,start_it,1},
                                   {init,start_em,1}]}}

どうやら、crypto モジュールをstart()した時に問題が発生したようです。
調べてみると、bugzilla にエントリが在りました。

Bug 1023017 - Restore ECC support in Erlang's crypto library

さらに詳しく調べると、ruby でも問題になっていました。

backport r41808(openssl build issue on fedora)

原因は、OpenSSL で OPENSSL_NO_EC が define されていないにも関わらず、OPENSSL_NO_EC2M が define されている環境においてビルドすると、定義されていないシンボル EC_GROUP_new_curve_GF2m (ガロア体GF(2m)上の楕円曲線暗号を扱う関数)を利用してしまうことにあるようです。
解決策として、Erlang/OTP の lib/crypto/c_src/crypto.c の EC_GROUP_new_curve_GF2m を利用している if 節を "ifndef OPENSSL_NO_EC2M" でくくってやれば問題ないことを確認しました。
さて、パッチを送るかと思っていると、既に今より16日前に解決済みであることを知りました。

対策パッチ:
crypto: selective support for GF2m curves

関連Pull Request:
more EC curves

上記のパッチを当てれば、問題は解決されます。

2014/01/08

BEAM(Erlang VM) 参考資料まとめ

はじめに

Erlang/OTP で開発したアプリケーションは、通常 BEAM (Erlang VM)と呼ばれる仮想マシン上で動作させます。BEAMに関する資料は、今のところ世の中にあまり多くないようです。BEAMの情報が得やすくなるよう、ここにまとめておきます。

BEAM(Erlang VM) 参考資料

Hitchhiker’s Tour of the BEAM

Erlang Solutions Ltd. の Robert Virding 氏による BEAM の概要解説。Scheduler, Memory 管理, GC, Async Threads について小気味よくまとまっています。

The evolution of the Erlang VM

同氏による Erlang VM の歴史解説。Erlang VM ごく初期のProlog Interpreter や JAM(Joe's Abstract Machine) からBEAMに至るまでの経緯が書かれています。

Erlang Engine Tuning, Know Your Engine – Part II: the BEAM

ERTS本執筆中のErik (Happi) Stenman氏による BEAM解説。Erlang からコンパイルされたBEAM コードがどう解釈されて動作するのか説明されています。

Inside the Erlang VM

Ericsson の Ludin 氏によるErlang VM のについての解説。 主に Scheduler の実装と SMP 対応について書かれています。

How Erlang does scheduling

Erlang VM の Scheduling 方法についての平易な解説。プロセス優先度設定や、プロセスコンテキストスイッチの契機、他の言語(実行環境)との差異について書かれています。


2013/12/03

blktrace で block IO の分布を可視化する

はじめに

blktrace は block IO の発行をトレースする有名なツールです。blktrace で集めたトレースデータを解析する btt には、ディスク上での block IO の分布を可視化する bno_plot.py というツールが付属しています。bno_plot.py を使うと、gnuplot を用いて下記のような図を生成できます。


図では、x軸が時間、y軸が Block Number、z軸が Block per IO になっています。これから、いつ頃に、どのセクタに、どのくらいのIOが発行されたかがわかります。

使い方

bno_plot.py の使い方をご説明します。
blktrace と gnuplot をインストールします。
# yum -y install blktrace gnuplot

blktrace でトレースを収集します。
# blktrace -w 30 -d /dev/sda -o sda

収集したトレース情報を、btt で解析します。解析したデータをファイルに出力します。
# btt -i sda.blktrace.2 -B sda_cpu2
サフィックスが r.dat, w.dat, c.dat になっているファイルが生成されます。それぞれ、Read, Write, Read and Write データに対応します。

解析データを bno_plot.py に与え、gnuplot で図を描画します。
# bno_plot.py sda_cpu2_8,0_c.dat
これで、上記の画像のような図が得られます。

Block IO の分布を可視化すると、どのように負荷がかかっているのかわかりやすいです。
また、@akiradeveloper 氏が開発している、dm-writeboost のように、ランダムライトをシーケンシャルライトに変換する特殊なディスクキャッシュについて、IO分布を可視化すると特徴が見て取れて面白いかもしれません。

2013/12/6 追記:
早速 @akiradeveloper 氏がdm-writeboost の block IO 可視化をしてくれました!ありがとうございます。
テストステ論: (writeboost report) writeboostのI/Oを可視化した
予想通りdmへのIOがランダムでも、その下のデバイスへのIOはシーケンシャルになっている様子が見て取れます。

2013/11/27

Erlang/OTP の systemtap トレース機能の使い方

はじめに

Erlang/OTP には systemtap と連携したトレース機能が備わっています。Erlang/OTP で systemtap 連携を有効にすると、systemtap から Erlang のトレースポイントを扱うことができます。トレースポイントを使うことで、BEAM 仮想マシンの挙動をより正確に把握したり、性能解析しやすくなります。

使い方

Erlang/OTP の ビルド

systemtap 連携を使うには、Erlang/OTP を configure --with-dynamic-trace=systemtap でビルドする必要があります。
# git clone git://github.com/erlang/otp.git
# cd otp
# ./otp_build autoconf
# ./configure --with-dynamic-trace=systemtap
# make

Erlang Shell を起動して [systemtap] と表示されていれば、systemtap が有効になっていることが確認できます。
# ./bin/erl
Erlang R16B03 (erts-5.10.4) [source-fb0006c] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [systemtap]

Eshell V5.10.4  (abort with ^G)

stap スクリプト起動方法

まず、stap -L コマンドで トレースポイントを確認しましょう。予め、beam バイナリファイルへの PATH を通しておいてください。
# PATH=/home/eiichi/git/otp/bin/x86_64-unknown-linux-gnu/:$PATH stap -L 'process("beam.smp").mark("*")' 
process("beam.smp").mark("aio_pool__add") $arg1:long $arg2:long
process("beam.smp").mark("aio_pool__get") $arg1:long $arg2:long
process("beam.smp").mark("bif__entry") $arg1:long $arg2:long
process("beam.smp").mark("bif__return") $arg1:long $arg2:long
process("beam.smp").mark("copy__object") $arg1:long $arg2:long
process("beam.smp").mark("copy__struct") $arg1:long
process("beam.smp").mark("dist__monitor") $arg1:long $arg2:long $arg3:long $arg4:long $arg5:long
process("beam.smp").mark("dist__output") $arg1:long $arg2:long $arg3:long $arg4:long
process("beam.smp").mark("dist__outputv") $arg1:long $arg2:long $arg3:long $arg4:long
process("beam.smp").mark("dist__port_busy") $arg1:long $arg2:long $arg3:long $arg4:long
...

約60個のトレースポイント(user-probe 系を除く)が確認できます。Linux Kernel のトレースポイントは 約 1200個、Qemu のトレースポイントが 約 900個なのと比較すると、若干少ないですね。

これらトレースポイントのうち、gc_major_start を例にトレースポイントの使い方を説明します。

まず、下記のような stap スクリプトを用意しましょう。(otp/lib/runtime_tools/examples より抜粋)
# cat garbage-collection.systemtap 
probe process("beam.smp").mark("gc_major-start")
{
    printf("GC major start pid %s need %d words\n", user_string($arg1), $arg2);
}

systemtap を有効にした Erlang Shell を起動し、下記のコマンドでsystemtap スクリプトを起動します。
# PATH=/home/eiichi/git/otp/bin/x86_64-unknown-linux-gnu/:$PATH stap garbage-collection.systemtap                    
Erlang Shell で適当に > "aaaaaaaaaa". などとコマンドを入力すると、stap スクリプトで下記のような出力が得られます。
GC major start pid <0 .33.0=""> need 9 words

GCの他にも、Message の送受信、プロセスの spawn、スケジュールなど、興味深く有用なトレースポイントがありあますので、お試しください。
systemtap スクリプトの例は lib/runtime_tools/example にあります。

dyntrace(user-probe)

Erlang/OTP で systemtap 連携を有効にすると、dyntrace モジュールを使って、 Erlang コードから動的にsystemtap スクリプトに情報を出力することができます。
Erlang/OTP に同伴されている、lib/runtime_tools/example/user-probe.systemtap を例にとって dyntrace モジュールの使いかたを説明します。

まず、user-probe.systemtap は beam 向けになっていますので、beam.smp 向けに直しましょう。user-probe.systemtap を開き、process('beam') となっているところを process('beam.smp') に書き換えます。
# vim user-probe.systemtap

書き換えたら、Erlang Shell を起動し、user-probe.systemtap を起動します。(beam に PATHを通しておいてください。)
# stap user-proeb.systemtap

この状態で、Erlang Shell にて、以下のように入力します。
2> dyntrace:p(1, 2, 3, 4, "a", "b", "c").
true
3> dyntrace:put_tag("test").
undefined
4> dyntrace:p(1, 2, 3, 4, "a", "b", "c").
true

すると、stap スクリプトでは、以下のような出力が得られます。
<0 .33.0="">  1 2 3 4 'a' 'b' 'c' 'c'
<0 .33.0=""> test 1 2 3 4 'a' 'b' 'c' 'c'

dyntrace:p/nで各種情報を出力します。dyntrace:put_tag/1 でトレース出力のプレフィックスを設定することができます。
詳しくはErlang User's Guide: dyntrace をご覧ください。

参考文献

Dtrace and Erlang: a new beginning
Erlang User's Guide: Systemtap and Erlang/OTP
Erlang User's Guide: dyntrace
Systemtap and Erlang: a tutorial
runtime_tools/src/dyntrace.erl