ランタイム・メトリクス

コンテナのラインタイム・メトリクス(訳注;コンテナ実行時の、様々なリソース指標や数値データ)をライブ(生)で表示するには、 docker stats コマンドを使います。コマンドがサポートしているのは、CPU 、メモリ使用率、メモリ上限、ネットワーク I/O のメトリクスです。

以下は docker stats コマンドを実行した例です。

$ docker stats redis1 redis2
CONTAINER           CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O
redis1              0.07%               796 KB / 64 MB        1.21%               788 B / 648 B       3.568 MB / 512 KB
redis2              0.07%               2.746 MB / 64 MB      4.29%               1.266 KB / 648 B    12.4 MB / 0 B

docker stats コマンドのより詳細な情報は、 docker stats リファレンス・ページ をご覧ください。

コントロール・グループ

Linux はプロセス・グループの追跡だけでなく、CPU・メモリ・ブロック I/O のメトリックス表示は、 コントロール・グループ に依存しています。これらのメトリックスにアクセスできますし、ネットワーク使用量のメトリクスも同様に取得できます。これらは「純粋な」 LXC コンテナのためのものであり、Docker コンテナのためのものでもあります。

コントロール・グループは疑似ファイルシステム(pseudo-filesystem)を通して公開されています。最近のディストリビューションでは、 /sys/fs/cgroup 以下で見つけられるでしょう。このディレクトリの下に、device・freezer・blkio 等の複数のサブディレクトリがあります。各サブディレクトリは、それぞれ異なった cgroup 階層に相当します。

古いシステムでは、コントロール・グループが /cgroup にマウントされており、その下に明確な階層が無いかもしれません。そのような場合、サブディレクトリが見えるかわりに、たくさんのファイルがあるでしょう。あるいは、存在しているコンテナに相応するディレクトリがあるかもしれません。

どこにコントロール・グループがマウントされているかを調べるには、次のように実行します。

$ grep cgroup /proc/mounts

コントロール・グループの列挙

/proc/cgoups を調べると、システム上の様々に異なるコントロール・グループのサブシステムが見えます。それぞれに階層がサブシステムに相当しており、多くのグループが見えるでしょう。

コントロール・グループのプロセスに属する情報は、 /proc/<pic>/cgroup からも確認できます。コントロール・グループは階層のマウントポイントからの相対パス上に表示されます。たとえば、 / が意味するのは「対象のプロセスは特定のグループに割り当てられていない」であり、 /lxc/pumpkin が意味するのはプロセスが pumpkin と呼ばれるコンテナのメンバであると考えられます。

特定のコンテナに割り当てられた cgroup の確認

コンテナごとに、それぞれの階層に cgroup が作成されます。古いシステム上の古いバージョンの LXC userland tools の場合、cgroups の名前はコンテナ名になっています。より最近のバージョンの LXC ツールであれば、cgroup は lxc/<コンテナ名> になります。

Docker コンテナは cgroups を使うので、コンテナ名はフル ID か、コンテナのロング ID になります。 docker ps コマンドでコンテナが ae836c95b4c3 のように見えるのであれば、ロング ID は ae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79 のようなものです。この情報を調べるには、 docker inspectdocker ps --no-trunc を使います。

Docker コンテナが利用するメモリのメトリクスは、 /sys/fs/cgroup/memory/docker/<ロング ID>/ から全て参照できます。

cgroups からのメトリクス:メモリ、CPU、ブロックI/O

各サブシステム(メモリ、CPU、ブロック I/O)ごとに、1つまたは複数の疑似ファイル(pseudo-files)に統計情報が含まれます。

メモリ・メトリクス: memory.stat

メモリ・メトリクスは「memory」cgroups にあります。メモリのコントロール・グループは少々のオーバーヘッドを加えることを覚えておいてください。これはホスト上における詳細なメモリ使用情報を計算するためです。そのため、多くのディストリビューションではデフォルトでは有効にされていません。一般的に、有効にするためには、カーネルのコマンドライン・パラメータに cgroup_enable=memory swapaccount=1 を追加します。

メトリクスは疑似ファイル memory.stat にあります。次のように表示されます。

cache 11492564992
rss 1930993664
mapped_file 306728960
pgpgin 406632648
pgpgout 403355412
swap 0
pgfault 728281223
pgmajfault 1724
inactive_anon 46608384
active_anon 1884520448
inactive_file 7003344896
active_file 4489052160
unevictable 32768
hierarchical_memory_limit 9223372036854775807
hierarchical_memsw_limit 9223372036854775807
total_cache 11492564992
total_rss 1930993664
total_mapped_file 306728960
total_pgpgin 406632648
total_pgpgout 403355412
total_swap 0
total_pgfault 728281223
total_pgmajfault 1724
total_inactive_anon 46608384
total_active_anon 1884520448
total_inactive_file 7003344896
total_active_file 4489052160
total_unevictable 32768

前半( total_ が先頭にない )は、cgroup 中にあるプロセス関連の統計情報を表示します。サブグループは除外しています。後半( 先頭に total_ がある )は、サブグループも含めたものです。

いくつかのメトリクスは「gauges」(ゲージ;計測した値そのものの意味)であり、例えば、値が増減するものです(例:swap は cgroup のメンバによって使われている swap 領域の量です)。あるいは「counter」(カウンタ)は、特定のイベント発生後に増えた値のみ表示します(例:pgfault はページ・フォルトの回数を表しますが、cgroup が作成された後の値です。この値は決して減少しません。)。

  • cache: コントロール・グループのプロセスによって使用されるメモリ容量であり、ブロック・デバイス上のブロックと密接に関わりがあります。ディスクからファイルを読み書きすると、この値が増えます。値が増えるのは「通常」の I/O ( openreadwrite システムコール)だけでなく、ファイルのマップ( mmap を使用 )でも同様です。あるいは tmpfs マウントでメモリを使う場合も、理由が明確でなくともカウントされます。
  • rss: ディスクに関連 しない メモリ使用量です。例えば、stacks、heaps、アノニマスなメモリマップです。
  • mapped_file: コントロール・グループ上のプロセスに割り当てられるファイル容量です。 どれだけの メモリが使用されるかの情報は得られません。どれだけ使っているかを表示します。
  • pgfault と pgmajfault: cgroup のプロセスが「page fault」と「major fault」の回数を個々に表示します。page fault とは、存在しないかプロテクトされた仮想メモリスペースにプロセスがアクセスしたときに発生します。かつては、プロセスにバグがあり、無効なアドレスにアクセスしようとしたときに発生しました( SIGSEGV シグナルが送信されます。典型的なのは Segmentation fault メッセージを表示して kill される場合です )。最近であれば、プロセスがスワップ・アウトされたメモリ領域を読み込みに行くか、あるいはマップされたファイルに相当する時に発生します。そのような場合、カーネルはページをディスクから読み込み、CPU がメモリへのアクセスを処理します。これはまた、プロセスがコピー・オン・ライト(copy-on-write)のメモリ領域に書き込んだ時にも発生します。これはカーネルがプロセスの実行を阻止するのと同じであり、メモリページを複製し、プロセスが自身のページをコピーして書き込み作業を再開しようとします。「メジャー」な失敗がおこるのは、カーネルが実際にディスクからデータを読み込む時点です。読み込みによって、既存のページと重複するか、空のページが割り当てられると一般的な(あるいは「マイナー」な)エラーが発生します。
  • swap: 対象の cgroup にあるプロセスが、現在どれだけ swap を使っているかの量です。
  • active_anon と inactive_anon: カーネルによって activeinactive に区分される anonymous メモリ容量です。 anonymous メモリとは、ディスク・ページにリンクされないメモリです。言い換えると、先ほど説明した rss カウンタと同等なものです。実際、rss カウンタの厳密な定義は、 active_anon + inactive_anon - tmpfs です( tmpfs のメモリ容量とは、このコントロール・グループの tmpfs ファイルシステムがマウントして使っている容量です )。では次に、「active」と「inactive」の違いは何でしょうか? ページは「active」として始まりますが、一定の時間が経つと、カーネルがメモリを整理(sweep)して、いくつかのページを「inactive」にタグ付けします。再度アクセスがあれば、ただちに「active」に再度タグ付けされます。カーネルがメモリ不足に近づくか、ディスクへのスワップアウト回数により、カーネルは「inactive」なページをスワップします。
  • active_file と inactive_file: キャッシュメモリの activeinactive は、先ほどの anon メモリの説明にあるものと似ています。正確な計算式は、キャッシュ = active_file + inactive_file + tmpfs です。この正確なルールが使われるのは、カーネルがメモリページを active から inactive にセットする時です。これは anonymous メモリとして使うのとは違って、一般的な基本原理によるものと同じです。注意点としては、カーネルがメモリを再要求(reclaim)するするとき、直ちに再要求(anonymous ページや汚れた/変更されたページをディスクに書き込む)よりも、プール上のクリーンな(=変更されていない)ページを再要求するほうが簡単だからです。
  • unevictable: 再要求されないメモリの容量です。一般的に mlock で「ロックされた」メモリ容量です。暗号化フレームワークによる秘密鍵の作成や、ディスクにスワップさせたくないような繊細な素材に使われます。
  • memory と memsw の limits: これらは実際のメトリクスではありませんが、対象の cgroup に適用される上限の確認に使います。「memory」はこのコントロール・グループのプロセスによって使われる最大の物理メモリを示します。「memsw」 は RAM+swap の最大容量を示します。

ページキャッシュ中のメモリ計算は非常に複雑です。もし2つのプロセスが異なったコントロール・グループ上にあるなら、それぞれの同じファイル(結局はディスク上の同じブロックに依存しますが)を読み込む必要があります。割り当てられたメモリは、コントロール・グループ毎の容量に依存します。これは良さそうですが、cgroup が削除されると、メモリページとして消費していた領域は使わなくなるので、他の cgroup のメモリ容量を増加させることをも意味します。

CPU メトリクス: cpuacct.stat

これまではメモリのメトリクスを見てきました。メモリに比べると他のものは非常に簡単に見えるでしょう。CPU メトリクスは cpuacct コントローラにあります。

コンテナ毎に疑似ファイルが cpuacct.stat があり、ここにコンテナにあるプロセスの CPU 使用率を、 user 時間と system 時間に分割して記録されます。いずれも慣れていなければ、 user とはプロセスが CPU を直接制御する時間のこと(例:プロセス・コードの実行)であり、 system とはプロセスに代わり CPU のシステムコールを実行する時間です。

これらの時間は 100 分の 1 秒の周期(tick)で表示されます。実際にはこれらは「user jiffies」として表示されます。 USER_HZ 「jillies」が毎秒かつ x86 システムであれば、 USER_HZ は 100 です。これは1秒の「周期」で、スケジューラが実際に割り当てる時に使いますが、 tickless kernels にあるように、多くのカーネルで ticks は適切ではありません。まだ残っているのは、主に遺産と互換性のためです。

Block I/O メトリクス

Block I/O は blkio コントローラを算出します。異なったメトリックスが別々のファイルに散在しています。より詳細な情報を知りたい場合は、カーネル・ドキュメントのの blkio-controller をご覧ください。ここでは最も関係が深いものをいくつか扱います。

  • blkio.sectors: cgroups のプロセスのメンバが、512 バイトのセクタをデバイス毎に読み書きするものです。読み書きは単一のカウンタに合算されます。
  • blkio.io_service_bytes: cgroup で読み書きしたバイト数を表示します。デバイス毎に4つのカウンタがあります。これは、各デバイス毎に同期・非同期 I/O と、読み込み・書き込みがあるからです。
  • blkio.io_serviced: サイズに関わらず I/O 操作の実行回数です。こちらもデバイス毎に4つのカウンタがあります。
  • blkio.io_queued: このグループ上で I/O 動作がキュー(保留)されている数を表示します。言い換えると、cgroup が何ら I/O を処理しなければ、この値は0になります。ただし、その逆の場合は違うので気をつけてください。つまり、 I/O キューが発生していなくても、cgroup がアイドルだとは言えません。これは、キューが無くても、純粋に停止しているデバイスからの同期読み込みを行い、直ちに処理することができるためです。また、cgroup は I/O サブシステムに対するプレッシャーを、相対的な量に保とうとする手助けになります。プロセスのグループが更に I/O が必要になると、キューサイズが増えることにより、他のデバイスとの負荷が増えるでしょう。

ネットワーク・メトリクス

ネットワークのメトリクスは、コントロール・グループから直接表示されません。ここに良い例えがあります。ネットワーク・インターフェースは ネットワーク名前空間 (network namespaces) 無いのコンテキストとして存在します。カーネルは、プロセスのグループが送受信したパケットとバイト数を大まかに計算できます。しかし、これらのメトリックスは使いづらいものです。インターフェースごとのメトリクスが欲しいでしょう(なぜなら、ローカルの lo インターフェスに発生するトラフィックが実際に計測できないためです )。ですが、単一の cgroup 内のプロセスは、複数のネットワーク名前空間に所属するようになりました。これらのメトリクスの解釈は大変です。複数のネットワーク名前空間が意味するのは、複数の lo インターフェース、複数の eth0 インターフェース等を持ちます。つまり、コントロール・グループからネットワーク・メトリクスを簡単に取得する方法はありません。

そのかわり、他のソースからネットワークのメトリクスを集められます。

IPtables

IPtables を使うことで(というよりも、インターフェースに対する iptables の netfilter フレームワークを使うことにより)、ある程度正しく計測できます。

例えば、ウェブサーバの外側に対する(outbund) HTTP トラフィックの計算のために、次のようなルールを作成できます。

$ iptables -I OUTPUT -p tcp --sport 80

ここには何ら -j-g フラグはありませんが、ルールがあることにより、一致するパケットは次のルールに渡されます。

それから、次のようにしてカウンタの値を確認できます。

$ iptables -nxvL OUTPUT

技術的には -n は不要なのですが、今回の例では、不要な DNS 逆引きの名前解決をしないために付けています。

カウンタにはパケットとバイト数が含まれます。これを使ってコンテナのトラフィック用のメトリクスをセットアップしたければ、 コンテナの IP アドレス毎に(内外の方向に対する)2つの iptables ルールの``for`` ループを FORWARD チェーンに追加します。これにより、NAT レイヤを追加するトラフィックのみ計測します。つまり、ユーザ定義 proxy を通過しているトラフィックも加えなくてはいけません。

あとは通常の方法で計測します。 collectd を使ったことがあるのなら、自動的に iptables のカウンタを収集する 便利なプラグイン があります。

インターフェース・レベルのカウンタ

各コンテナは仮想イーサネット・インターフェースを持っているので、そのインターフェースから直接 TX・RX カウンタを取得したくなるでしょう。各コンテナが vethKk8Zqi のような仮想イーサネット・インターフェースに割り当てられているのに気をつけてください。コンテナに対応している適切なインターフェースを見つけることは、残念ながら大変です。

しかし今は、 コンテナを通さなくても 数値を確認できる良い方法があります、ホスト環境上で ip-netns magic を使い、ネットワーク名前空間内のコンテナの情報を確認します。

ip-netns exec コマンドは、あらゆるネットワーク名前空間内で、あらゆるプログラムを実行し(対象のホスト上の)、現在のプロセス状況を表示します。つまり、ホストがコンテナのネットワーク名前空間に入れますが、コンテナはホスト側にアクセスできないだけでなく、他のコンテナにもアクセスできません。次のサブコマンドを通すことで、コンテナが「見える」用になります。

正確なコマンドの形式は、次の通りです。

$ ip netns exec <nsname> <command...>

例:

$ ip netns exec mycontainer netstat -i

ip netns は「mycontainer」コンテナを名前空間の疑似ファイルから探します。各プロセスは1つのネットワーク名前空間、PID の名前空間、 mnt 名前空間等に属しています。これらの名前空間は /proc/<pid>/ns/ 以下にあります。例えば、PID 42 のネットワーク名前空間に関する情報は、疑似ファイル /proc/42/ns/net です。

ip netns exec mycontainer ... を実行すると、 /var/run/netns/mycontainer が疑似ファイルの1つとなるでしょう(シンボリック・リンクが使えます)。

言い換えると、私たちが必要であれば、ネットワーク名前空間の中でコマンドを実行できるのです。

  • 調査したいコンテナに入っている、あらゆる PID を探し出します
  • /var/run/netns/<何らかの名前> から /proc/<thepid>/ns/net へのシンボリック・リンクを作成します。
  • ip netns exec <何らかの名前> ....`` を実行します。

ネットワーク使用状況を調査したいコンテナがあり、そこで実行しているプロセスを見つける方法を学ぶには、 <enumerating_cgroups> を読み直してください。ここからは tasks と呼ばれる疑似ファイルを例に、コントロール・グループ(つまり、コンテナ)の中にどのような PID があるかを調べましょう。

これらを一度に実行すると、取得したコンテナの「ショートID」は変数 $CID に入れて処理されます。

$ TASKS=/sys/fs/cgroup/devices/docker/$CID*/tasks
$ PID=$(head -n 1 $TASKS)
$ mkdir -p /var/run/netns
$ ln -sf /proc/$PID/ns/net /var/run/netns/$CID
$ ip netns exec $CID netstat -i

高性能なメトリクス収集のための Tip

新しいプロセスごとに毎回メトリクスを更新するのは、(比較的)コストがかかるので注意してください。メトリクスを高い解像度で収集したい場合、そして/または、大量のコンテナを扱う場合(1ホスト上に 1,000 コンテナと考えます)、毎回新しいプロセスをフォークしようとは思わないでしょう。

ここでは1つのプロセスでメトリクスを収集する方法を紹介します。メトリクス・コレクションをC言語で書く必要があります(あるいは、ローレベルなシステムコールが可能な言語を使います)。 setns() という特別なシステムコールを使えば、任意の名前空間上にある現在のプロセスを返します。必要があれば、ほかにも名前空間疑似ファイルのファイル・ディスクリプタ(file descriptor)を開けます(思い出してください:疑似ファイルは /proc/<pid>/ns/net です)。

しかしながら、これはキャッチするだけです。ファイルをオープンにしておくことはできません。つまり、そのままにしておくと、コントロール・グループが終了しても名前空間を破棄できず、ネットワーク・リソース(コンテナの仮想インターフェース等)が残り続けるでしょう(あるいはファイル・ディスクリプタを閉じるまで)。

適切なアプローチで、各コンテナ毎の最初の PID と、都度、名前空間の疑似ファイルが開かれるたびに、追跡し続ける必要があります。

終了したコンテナのメトリクスを収集

時々、リアルタイムなメトリクス収集に気を配っていなくても、コンテナ終了時に、どれだけ CPU やメモリ等を使用したか知りたい時があるでしょう。

Docker は lxc-start に依存しており、終了時は丁寧に自分自身をクリーンナップするため困難です。しかし、他にも方法があります。定期的にメトリクスを集める方法(例:毎分 collectd LXC プラグインを実行)が簡単です。

しかし、停止したコンテナに関する情報を集めたいときもあるでしょう。次のようにします。

各コンテナで収集プロセスを開始し、コントロール・グループに移動します。これは対象の cgroup のタスクファイルに PID が書かれている場所を監視します。収集プロセスは定期的にタスクファイルを監視し、コントロール・グループの最新プロセスを確認します(先ほどのセクションで暑かったネットワーク統計情報も取得したい場合は、プロセスを適切なネットワーク名前空間にも移動します)。

コンテナが終了すると、 lxc-start はコントロール・グループを削除しようとします。コントロール・グループが使用中のため、処理は失敗しますが問題ありません。自分で作ったプロセスは、対象のグループ内に自分しかいないことが分かります。それが必要なメトリックスを取得する適切なタイミングです。

最後に、自分のプロセスをルート・コントロール・グループに移動し、コンテナのコントロール・グループを削除します。コントロール・グループの削除は、ディレクトリを rmdir するだけです。感覚的にディレクトリに対する rmdir は、まだ中にファイルのではと思うかもしれませんが、これは疑似ファイルシステムのため、通常のルールは適用されません。クリーンアップが完了すると、これで収集プロセスは安全に終了できました。