2016/06/28

Memcached 1.4.19 以降: Eviction 不具合による Slab OOM

はじめに



Memcached を運用中に、Request の傾向は変わっていないにもかかわらず、徐々に Item 数が増加し始め、 ある時を境に Item が 一切 Eviction/Expire されなくなり、Memory が枯渇し Slab OOM Error が起こる、という不具合に遭遇しました。不具合の原因については特定済みで、修正方法についてメンテナの方と議論中(対象 Pull-Request: fix zero hash items eviction) です。不具合が発生する条件、原因、回避策を簡単にまとめておきます(Pull-Request にはより詳しく書いてあります)。

不具合が発生する条件


  • Memcached Version : 1.4.19 以降
  • SET した Key を GET しない場合がある  
  • Item を入れ替える Command (APPEND, PREPEND, INCR, DECR) を使用していない

不具合の原因


Memcached は Item を双方向リストで管理しています。GET, SET などの Command を処理するタイミングで各 Slab Class の双方向リストの末尾から 5 つの Item を捜査し、 Eviction/Expire させます(1.4.26 時点 lru_pull_tail@items.c)。この Item Eviction/Expire 処理に不具合があり、 Item の Key の hash 値が偶然に 0 の時に、Item が Eviction/Expire されず、双方向リストの末尾に滞留し続けます。 

再現スクリプト

# set the items whose jenkins hash(ENDIAN_LITTLE) values are zero with exptime 1 sec
printf "set 0wYuLiaUdfgTZCUsz8mRR1WJVk 0 1 4\r\ndata\r\n" | nc localhost 11211
printf "set 9NxjWkBnSfD0LShqUBZAqv3jKI 0 1 4\r\ndata\r\n" | nc localhost 11211
printf "set wRL2lvUMyPnbiImZdi9CTXbsJ6 0 1 4\r\ndata\r\n" | nc localhost 11211
printf "set vOgpzFNo2pGpYLKxUWXfI6PDXq 0 1 4\r\ndata\r\n" | nc localhost 11211
printf "set aTogASrWNRQSA1uh2ySgv6HwaU 0 1 4\r\ndata\r\n" | nc localhost 11211

# wait for exptime
sleep 2

# another innocent key set
# normally, the five items expire here
printf "set innocentkey 0 1 4\r\ndata\r\n" | nc localhost 11211

echo "stats items" | nc localhost 11211

スクリプト実行結果

STORED
STORED
STORED
STORED
STORED
STORED
STAT items:1:number 6
STAT items:1:age 2
STAT items:1:evicted 0
STAT items:1:evicted_nonzero 0
STAT items:1:evicted_time 0
STAT items:1:outofmemory 0
STAT items:1:tailrepairs 0
STAT items:1:reclaimed 0
STAT items:1:expired_unfetched 0
STAT items:1:evicted_unfetched 0
STAT items:1:crawler_reclaimed 0
STAT items:1:crawler_items_checked 0
STAT items:1:lrutail_reflocked 0
END

本来であれば、6度目の SET が完了した時点で、 Expire すべき Item は全て回収されるはずですが、滞留してしまっています。
さらに後続の SET が続くと、 Item が一切回収されないまま Memory を消費し続け、-M option で指定した上限にあたるところで OOM Error(SET 不能) となります。

不具合特定の方法


Coredump を取得し、コードを読んで可能性を絞りつつ gdb で追いました。

現状の回避策


  • 提案している Pull-Request のように、なんらかの方法で hash 値 0 の Item も回収対象にする
  • Memcached 1.4.18 以前に Version Down
  • [再起動不要, コード理解している人向け] Item 滞留を起こしうる全ての Slab Class を対象に、時折 Item を入れ替える Command (APPEND, PREPEND, INCR, DECR) を発行する
    • lru_pull_tail の引数 cur_hv が 0 では無くなるため

まとめ


  • Memcached 1.4.19 以降には突然 Slab OOM を引き起こす不具合が存在します
  • 新しい Version の Memcached は精力的に新しい機能を実装しているため、実運用時には注意



2015/09/27

Hadoop : CPU system 使用率高騰 "zone_reclaim_mode = 1" 編

はじめに


 会社で PB 級の Hadoop クラスタを運用していますが、ある日から Datanode の CPU system (Kernel 内での CPU 使用率) が高騰し、Job が遅延するという症状が発現しました。Hadoop で CPU system 高騰というと、 Transparent HugePage 設定が有名ですが、そちらについては既に特定し、対策済みでした。 THP と Hadoop に関係については下記 Blog が詳しいです。
Transparent Huge Pages and Hadoop Workloads

 今回は THP ではなく、 "zone_reclaim_mode" の設定による性能劣化について、現象から原因特定に至るまでの経緯と、推奨する設定について解説します。

現象


 観測された現象について簡単に箇条書きします。
  1. CPU user が 5% 程度の時でも CPU system が30% を超えるなど、 Kernel 内での CPU 使用率が異常に高かった
  2. CPU 使用率高騰により、いくつかの Job 実行時間が、問題発生前と比較して 1.5 倍に増えた
  3. 一部のマシンで発生し、他のモデルのマシンでは発生しなかった

perf による原因調査


Kernel 内での CPU 使用率が高騰した際には perf と呼ばれる Linux Profiling Tool が非常に有用です。特別な準備をする必要なく、簡単に Profiling を取得できます。
 今回は Profiling により、 "どの関数で CPU を使用しているのか" 、 "どの処理で問題の関数が呼ばれるのか(Call Graph)" を調査します。

perf での Profiling 取得

 CPU system が高騰したタイミングを見計らい、下記コマンドでプロファイルを取得します。
perf record -F 99 -a -g -- sleep 30
取得した結果を表示。Call Graph が取得できます。
perf report

結果の一部:
-  33.01%             java  [kernel.kallsyms]                     [k] _spin_lock_irq
   - _spin_lock_irq
      - 98.89% shrink_inactive_list
           shrink_mem_cgroup_zone
           shrink_zone
           zone_reclaim
           get_page_from_freelist
         - __alloc_pages_nodemask
            - 89.56% alloc_pages_current
               - 79.46% __page_cache_alloc
                  - 99.76% grab_cache_page_write_begin
                       ext4_da_write_begin
                       generic_file_buffered_write
                       __generic_file_aio_write
                       generic_file_aio_write
                       ext4_file_write
                       do_sync_write
                       vfs_write
                       sys_write
                       system_call_fastpath
                     + 0x3fda40e6fd
               + 17.54% tcp_sendmsg
               + 1.75% __get_free_pages
               + 1.25% pte_alloc_one
            + 9.29% alloc_pages_vma
            + 1.15% kmem_getpages
+  14.24%             java  [kernel.kallsyms]                     [k] _spin_lock
+   4.75%             java  libjvm.so                             [.] SpinPause
+   4.03%             java  perf-1947.map                         [.] 0x00007fd9550209cd
+   2.64%             java  libsnappy.so.1.1.3                    [.] snappy::internal::CompressFragment(char const*, unsigned long, char*,
+   2.01%             java  libjvm.so                             [.] ParallelTaskTerminator::offer_termination(TerminatorTerminator*)
+   1.84%             java  [kernel.kallsyms]                     [k] __isolate_lru_page
+   1.58%             init  [kernel.kallsyms]                     [k] intel_idle
...

 ちなみに、上記の Call Graph を可視化した FlameGraph の画像は以下です。

perf 結果からわかること

  1. CPU system を高騰させているのは "spin_lock*" 関数であること
  2. "spin_lock*" 関数は "メモリ回収処理" の延長で呼ばれていること

 つまり、メモリが足りなくなったために、メモリ回収処理があまりに頻繁に呼ばれ、spin_lock のオーバヘッドが高騰したことが予想されます。さて、メモリ使用量について改めて観測すると、半分程度しか使用していません。にもかかわらずメモリ回収処理が頻繁に呼ばれるのは何故でしょうか...

 Linux のメモリ解放に関連するパラメータを洗い出してみると、一つの気になるパラメータがありました。"zone_reclaim_mode" です。有効になっている場合、NUMA 環境で zone 毎のメモリ回収が積極的に行われるようになります。デフォルトでは無効のはずですが、今回の該当マシン(CentOS 6系)で調べてみると、なんと有効になっていました。
zone_reclaim_mode について詳細: https://www.kernel.org/doc/Documentation/sysctl/vm.txt

zone_reclaim_mode 無効設定の結果


/proc/sys/vm/zone_reclaim_mode に 0 を設定したところ、問題の CPU system 高騰は収まりました。Job の実行時間も元の水準に戻りました。

なぜ zone_reclaim_mode が有効になっていたか


zone_reclaim_mode は一部のマシンで有効になっており、 CPU system が高騰していない別のマシンでは無効になっていました。同じ OS を使っていたのに、なぜ設定に違いが出たのでしょうか。Kernel のソースコードを読んで調べてみましょう。Kernel の Version は CentOS6(2.6.32-431.11.2.el6) とします。

 zone_reclaim_mode は default = 0 ですが、NUMA 環境における Node 間の距離(RECLAIM_DISTANCE) の値によっては、Kernel 起動時に 1 に修正されてしまうようです。
  mm/pagealloc.c
3096         /*
3097          * If another node is sufficiently far away then it is better
3098          * to reclaim pages in a zone before going off node.
3099          */
3100         if (distance > RECLAIM_DISTANCE)
3101             zone_reclaim_mode = 1;
 その閾値は "20" とあります。
 57 /*       
 58  * If the distance between nodes in a system is larger than RECLAIM_DISTANCE
 59  * (in whatever arch specific measurement units returned by node_distance())
 60  * then switch on zone reclaim on boot.
 61  */      
 62 #define RECLAIM_DISTANCE 20

 有効になっていたマシンの NUMA Node 距離を調べると... 21
$ numactl --hardware
...
node distances:
node   0   1 
  0:  10  21
  1:  21  10 

 無効になっていたマシンでは 20 でした。
 要するに、ハードウェア構成によっては zone_reclaim_mode が自動的に有効になってしまう様です。

最新の Kernel ではどうなっているか


ちなみに最新の Kernel では 下記 Commit により、 "ハードウェア構成によっては zone_reclaim_mode を自動で有効とする" 挙動が無効に変更されています。
http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=4f9b16a64753d0bb607454347036dc997fd03b82

他のソフトウェアでの推奨値


メモリを大量に使う DB 等のソフトウェアでの推奨値はやはり "無効" のようです。

まとめ

  • Hadoop Datanode での CPU system 高騰原因を perf を使って調査した
  • 原因: zone_reclaim_mode = 1 によるメモリ回収処理多発
  • ハードウェア構成によってい zone_reclaim_mode のデフォルト設定は変わる
  • Hadoop 含め、NUMA 環境でメモリを大量に使うソフトウェアで CPU system が高騰していた場合、 zone_reclaim_mode 設定を確認する

2015/07/16

FreakOut DSP 入札サーバの CPU 使用率を 30% 削減する Performance Tuning

はじめに


 勤務先の FreakOut 社では RTB で広告枠を買い付ける DSP の開発・運用を行っています。RTB とは、インターネット広告のインプレッションが生じる毎に、広告枠の競争入札を行う仕組みです。 DSP とは、 RTB において、競争入札をする側のシステムになります。広告枠/広告を見ている人 に対し、最適な広告を、最適なタイミングで届ける機能を広告主に提供する仕組みです。
 FreakOut DSP は最適な広告探索・入札価格調整のため、非常に多くのデータを参照し、沢山の演算処理を行います。広告を見ている人が過去にアクセスした Web ページの情報や検索ワード、さらに 広告がクリックされる予測確率(過去のログから機械学習で算出) などを参照し、入札価格を決定するのです。そのため、DSP で入札を担当するサーバは CPU がボトルネックになっており、台数も数百台に嵩んでいます。インフラコストの大部分を占めるのが入札サーバなのです。

 今回の記事では、入札サーバの CPU の使用率を 30% 程度削減した Performance Tuning 手法についてお伝えします。


入札サーバ実装概要


 ユーザに広告を素早く届けるため、入札サーバではレスポンスタイム 50ms 程度が求められます(50ms or die)。高速に動作する必要があるシステムですが、FreakOut DSP は Perl で実装されています。リクエストは Nginx + Starlet で処理されます。詳しくは下記のリンク先、 @myfinder さんの資料をご参照ください。3年ほど前の資料ですが、大きくは変化していません。


 Starlet は Prefork 型の Web App サーバであり、多数の worker process がリクエストを順次捌く構造を持っています。

Tuning の要点


htop、mpstat -P ALL 等のコマンドで Starlet の worker process の挙動を観察すると、すべてのコアを公平に使えていないことが分かりました。また worker process が CPU コア間を移動する(migration) 頻度も多いのです。これらは Linux Kernel の Process Scheduler の仕様によるもので、多くのユースケースでは特にチューニングせずとも良好な動作をするのですが、 入札サーバのように、Prefork 型でかつ CPU バウンドなワークロードでは、チューニングを施す方が性能が改善するケースがあります。
 CPU コアを公平に使えず、コア間の負荷に偏りがあると、負荷の高いコアではプロセスの Context Switch が頻繁に発生してしまいます。Context Swtich が頻発すると、 CPU Cache を有効に使えないため、オーバヘッドが嵩みます。
 また、プロセスが CPU コア間を移動すると、これまた CPU Cache を有効に使えないため、オーバヘッドが同上、となってしまうのです。

 上記の観測から、CPU Cache を最大限有効活用する Performance Tuning を下記2点、実施しました。

Tuning1. worker process のコア固定化


 worker process が生成された直後、プロセス内で sched_setaffinity システムコールを発行し、動作する CPU コアが一様に分かれるよう CPU コアを固定化しました。これにより、worker process が CPU コア間を移動することは無くなります。

Tuning2. Linux Kernel Scheduler Parameter 調整


 Linux Kernel にはプロセス Scheduler の挙動を調整する Parameter があります。
 代表的なものに sched_min_granularity_ns があります。これは、ざっくり言ってしまうと Process Scheduler が1つのプロセスを動作させ、次のプロセスに切り替えるまでの時間の最小単位です。つまり、sched_min_granularity_ns が小さいと、頻繁に Context Switch が起こり、逆に大きいと、Context Switch の頻度が少なくなります。
 類似の Parameter で重要なものに sched_wakeup_granularity_ns があります。プロセスが頻繁に wakeup と sleep を繰り替えすケースで Context Switch の頻度を調整する Parameter で、小さくすると Context Switch が頻繁になり、逆に大きくすると、Context Switch の頻度が少なくなるのは sched_min_granularity_ns と同様です。入札サーバは CPU バウンドとはいえ、memcached へのアクセスもある程度生じるため、wakeup/sleep による Context Switch も考慮する必要があります。

 一般にリアルタイム性の求められるシステムでは、プロセスの応答時間を短くするため、 これらの値を小さくします。

 今回実施した Tuning では これらの値を大きくしています。
「"50ms or die" なんだったらリアルタイム性が要求されるのでは?」 と思う方もいらっしゃるかもしれませんが、逆です。"50ms" は OS Scheduler から見れば非常長い時間であり、Context Switch が頻発すればそのオーバヘッドがバカにならないのです。オーバヘッドは切り替えのみにかかる時間だけではなく、一度 Context Switch が発生すると、CPU Cache Hit Rate が落ち、多くの CPU 時間を無為に使ってしまうのです。

 Tuning の実施にあたっては tuned を活用しました。tuned-adm の Profile: throughput-performance で設定しました。tuned-adm は 上記の Scheduler Parameter を含め、諸々良しなに設定してくれます。上記の Parameter は以下です。

  sched_min_granurarity_ns : 10000
  sched_wakeup_granurarity_ns : 15000

Scheduler Parameter について詳しくは doc/Documentation/scheduler/sched-design-CFS.txt 、Linux Kernel Watch の記事、及び Kernel のソースコードをご参照ください。

 他にも細かいところで Tuning している箇所がありますが、今日のところはこのあたりで。


Tuning 結果


 Tuning 1. の実施により 20%、Tuning 2. の実施により 10% 、合計 30% 程度の CPU 使用率を削減することができました。
 また、入札のレスポンスタイムも、平均 30ms 程度のものを 20ms まで低減することが出来ました。

余談: Tuning 1. の着想 Erlang VM(BEAM)


 Starlet の worker process コア固定化のチューニングは実は Erlang VM(BEAM) のオプションパラメータに着想を得ています。Erlang VM には Scheduler Thread をコア固定するオプション(+sbt) があり、以前このパラメータの有無で何らかのベンチマークを取ったところ、5% 程度性能が向上した経験が元になっています。(何のベンチマークだったかは忘れました)

補足


 「この記事で紹介した Tuning を実施するこどで Starlet が動作するサーバの CPU 使用率を 30% 削減できる」ということは保証されません。入札サーバのように、極端に CPU バウンドなサーバに対して効果のある Tuning です。

まとめ


- 入札サーバがインフラコストの大部分を占めていた
- CPU Cache を有効活用するため、プロセスの CPU コア間の移動や Context Switch によるオーバヘッドを削減する Tuning を施した
- 入札サーバの CPU 使用率を 30% 程度削減出来た


2014/07/24

Linux システムコールのブロック・ノンブロックまとめ

はじめに

Linux にはブロックするシステムコールとノンブロックなシステムコールがあります。さて、システムコールが「ブロックする」とはどういうことでしょうか。よく、ブロックするシステムコールとは「処理が完了するまでプロセスの動作が中断され待たされること」という説明を見ますが、より詳細に、どういう処理の場合に待たされるのか、整理してみましょう。

「ブロックする」とは

Linux において、システムコールがブロックするとは、「プロセスが、システムコール呼び出しの延長で待状態(TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE) に遷移し、CPU時間を消費せずにあるイベントが完了するのを待つようになる」、ことを指します。ブロックするシステムコールのうち代表的なものと、完了待ち対象イベントをまとめると、以下のようになります。


システムコール待ち対象イベント
read, write, fsyncディスク I/O
read, write, send, recv, accept, connect, select, poll, epoll FIFO読み書き可能
futex, flock ロック取得
nanosleep タイマ発火

待ち対象イベントをそれぞれ解説します。

ディスクI/O

ディスクをバックエンドとするファイルへの読み書きで、ディスクアクセスが生じる場合にはディスクへのI/O発行が完了するまでプロセスは待ち状態になります。例えば、読み込み対象のファイルがページキャッシュに乗っていない場合や、キャッシュを介さないダイレクトI/Oを発行した場合、fsync(2) でメモリ上の内容をディスクと同期させた場合、です。
なお、細かい話ですが、この場合の待ち状態は TASK_UNINTERRUPTIBLE という、シグナルを受け付けない待ち状態です。ディスクI/O以外の待ち対象イベントでは、TASK_INTERRUPTIBLEで待ちます。

キュー読み書き可能

パイプやソケットなど、キュー(FIFO)の構造を持つファイルを読み書きしようとした時に、キューが空で読み取れるデータがない場合と、キューが満杯でこれ以上書き込めない場合には、読み書きできる状態になるまでプロセスは待ち状態になります。キューに新しくデータが到着すると、キューが読み込み可能になります。キューに空きが出来ると、キューは書き込み可能状態になります。
read(2)、write(2)、select(2) については上記の説明で良いのですが、accept(2) は少し状況が違うので補足して説明します。
accept(2) はクライアントからの接続要求が キューにない場合にプロセスが待状態に遷移します。複数のプロセスが同じファイルに対し accept(2) を発行している時にクライアントからの接続要求があった場合、待状態にあるプロセスたちのうち、一つのプロセスのみが起床され、accept(2) 処理を実行します。一つのプロセスのみが起床されるのは所謂 Thundering Herd 問題への対処です。日本語の解説では、id:naoya さんによるprefork サーバーと thundering herd 問題 が詳しいです。

ロック取得

futex(2)は指定したアドレスに対応するキューでプロセスを待状態にしたり(FUTEX_WAIT)、指定したアドレスに対して待ち状態にあるプロセスを起床する(FUTEX_WAKE)同期機構です。
分かりやすくいうと、pthread_mutex_lock(3) などを通してロックを取得しようとしたが、すでに他のプロセスがロックを取得していた場合に、プロセスはFUTEX_WAIT で待ち状態に遷移します。flock(2) も同様です。

タイマ発火

所謂タイムアウトです。指定時間が経過するとプロセスが起床します。


「ノンブロック」とは

ブロックと対をなす概念:ノンブロックについても触れておきます。
ノンブロックなシステムコールとは、ブロックしないシステムコールのことです。システムコールをノンブロックにするには、対象とするファイルにfcntl(2)  でノンブロッキングフラグ(O_NONBLOCK)を付与します。ノンブロッキングフラグを付与されたファイルに対して、完了待ち対象イベントが「キュー読み書き可能」なシステムコールを発行すると、キューが読み書き可能でない場合、システムコールは即座に失敗し(return -1)、errno に EAGAIN が設定されます。キューが読み書き可能になるのを待ちはしません。
完了待ち対象イベントが「ディスクI/O」なシステムコールについては、ノンブロックには出来ませんが、その代わり非同期I/Oシステムコール io_submit(2) が用意されています。

まとめ

システムコールがどのような場合にブロックするのか、完了待ち対象イベント別に分けて説明しました。待ち対象イベントには大きくわけて「ディスクI/O」「キュー読み書き可能」「ロック取得」「タイマ発火」があります。「キュー読み書き可能」なシステムコールについては、ノンブロッキングフラグを付与することで、ノンブロックにできます。

ブロックするシステムコールは上記ですべてではありません。他にもちょくちょくありますが、細かいので省略します。

参考文献

各システムコール Man Page
Linux Kernel 3.15 ソースコード

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)