ランタイム・メトリクス

docker stats

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 のコンテナにも関連します。

コントロール・グループは擬似ファイルシステムを通じて提供されます。 最近のディストリビューションでは、このファイルシステムは /sys/fs/cgroup にあります。 このディレクトリの下には devices、freezer、blkio などのサブディレクトリが複数あります。 これらのサブディレクトリが、独特の cgroup 階層を構成しています。

かつてのシステムでは、コントロール・グループが /cgroup にマウントされていて、わかりやすい階層構造にはなっていませんでした。 その場合、サブディレクトリそのものを確認していくのではなく、サブディレクトリ内にある数多くのファイルを見渡して、そのディレクトリが既存のコンテナーに対応するものであろう、と確認していくしかありません。

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

$ grep cgroup /proc/mounts

cgroups の確認

v1 と v2 では cgroups のファイル レイアウトが著しく異なります。

システム上に /sys/fs/cgroup/cgroup.controllers があれば v2 を使っており、そうでなければ v1 を使っています。自分の cgroup のバージョンに対応したサブセクションをご覧ください。

以下のディストリビューションでは、デフォルトで cgroup v2 が使われミズ合う。

  • Fedora (31以降)

  • Debian GNU/Linux (11以降)

  • Ubuntu (21.10以降)

cgroup v1

/proc/cgroups を覗いてみるとわかりますが、システムが利用するコントロール・グループのサブシステムには実にさまざまなものがあり、それが階層化されていて、数多くのグループが含まれているのがわかります。

また /proc/<pid>/cgroup を確認してみれば、1 つのプロセスがどのコントロール・グループに属しているかがわかります。 そのときのコントロール・グループは、階層構造のルートとなるマウント・ポイントからの相対パスで表わされます。 / が表示されていれば、そのプロセスにはグループが割り当てられていません。 一方 /lxc/pumpkin といった表示になっていれば、そのプロセスは pumpkin という名のコンテナのメンバであることがわかります。

cgroup v2

cgroup v2 ホスト上では、 /proc/cgroups の内容は意味がありません。利用可能なコントローラは /sys/fs/cgroup/cgroup.controllers をご覧ください。

cgroup バージョンの変更

cgroup バージョンを変更するには、システム全体の再起動が必要です。

systemd をベースとするシステムでは、cgroup v2 を有効にするには、kernel コマンドラインで systemd.unified_cgroup_hierarchy=1 を追加します。cgroup バージョン v1 へと戻すには、かわりに systemd.unified_cgroup_hierarchy=0 を指定します。

システム上で grubby コマンドが利用可能な場合は(例: Fedora 上)、以下のようにコマンドラインで変更できます。

$ sudo grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=1"

grubby コマンドが利用できなければ、 /etc/default/grubGRUB_CMDLINE_LINUX を編集し、 sudo update-grub を実行します。

cgroup v2 上で Docker を実行

Docker が cgroup v2 をサポートするのは Docker 20.10 からです。group v2 上で Docker を動かすには、以下の条件を満たすのも必要です。

  • containerd: v1.4 以上

  • runc: v1.0.0-rc91 以上

  • Kernel: v4.15 以上(v5.2 以上を推奨)

cgroup v2 モードの挙動は cgroup v1 モードと著しく異なりますので注意してください。

  • デフォルトの cgroup ドライバ( dockerd --exec-opt native.cgroupdriver )は、 v2 上は「systemd」、v1 上は「cgroupfs」

  • デフォルトの cgroup 名前空間モード( docker run --cgroupns )は、 v2 上は「private」、v1 上は「host」

  • docker run フラグの --oom-kill-disable--kernel-memory は、 v2 では放棄

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

各コンテナでは、各階層内に 1 つの cgroup が生成されます。 かつてのシステムにおいて、ユーザーランド・ツール LXC の古い版を利用している場合、cgroup 名はそのままコンテナ名になっています。 より新しい LXC ツールでの cgroup 名は lxc/<コンテナー名> となります。

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

Docker コンテナが利用するメモリの全メトリクスは、 以下のパスから参照できます。

  • /sys/fs/cgroup/memory/docker/<longid>/ cgroup v1 の cgroupfs ドライバ

  • /sys/fs/cgroup/memory/system.slice/docker-<longid>.scope/ cgroup v1 の systemd ドライバ

  • /sys/fs/cgroup/docker/<longid/> cgroup v2 の cgroupfs ドライバ

  • /sys/fs/cgroup/system.slice/docker-<longid>.scope/ cgroup v1 の systemd ドライバ

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

注釈

このセクションは、まだ cgroup v2 用に更新されていません。cgroup v2 に関する詳しい情報は、 Kernel ドキュメント を参照ください。

各サブシステム(メモリ、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_ がある )は、サブグループも含めたものです。

メトリクスの中には「メータ」つまり増減を繰り返す表記になるものがあります。 たとえば swap は、cgroup のメンバによって利用されるスワップ容量の合計です。 この他に「カウンタ」となっているもの、つまり数値がカウントアップされていくものがあります。 これは特定のイベントがどれだけ発生したかを表わします。 たとえば pgfault は cgroup の生成以降に、どれだけページ・フォルトが発生したかを表わします。

  • cache: このコントロール・グループのプロセスによるメモリ使用量です。ブロック・デバイス上の各ブロックに細かく関連づけられるものです。ディスク上のファイルと読み書きを行うと、この値が増加します。ふだん利用する I/O(システムコールの openreadwrite )利用時に発生し、(mmap を用いた)マップ・ファイルの場合も同様です。tmpfs によるメモリ使用もここに含まれますが、理由は明らかではありません。

  • rss: ディスクに関連 しない メモリ使用量です。例えば、stacks、heaps、アノニマスなメモリマップです。

  • mapped_file: このコントロール・グループのプロセスによって割り当てられるメモリの使用量です。メモリを どれだけ 利用しているかの情報は得られません。ここからわかるのは どのように 利用されているかです。

  • pgfault, pgmajfault: cgroup のプロセスにおいて発生した「ページ・フォルト」、「メジャー・フォルト」の回数を表わします。ページ・フォルトは、プロセスがアクセスした仮想メモリ・スペースの一部が、存在していないかアクセス拒否された場合に発生します。存在しないというのは、そのプロセスにバグがあり、不正なアドレスにアクセスしようとしたことを表わします(SIGSEGV シグナルが送信され、Segmentation fault といういつものメッセージを受けたとたんに、プロセスが停止されます)。アクセス拒否されるのは、スワップしたメモリ領域、あるいはマップ・ファイルに対応するメモリ領域を読み込もうとしたときに発生します。この場合、カーネルがディスクからページを読み込み、CPU のメモリ・アクセスを成功させます。またコピー・オン・ライト・メモリ領域へプロセスが書き込みを行う場合にも発生することがあります。同様にカーネルがプロセスの切り替え(preemption)を行ってからメモリ・ページを複製し、ページ内のプロセス自体のコピーに対して書き込み処理を復元します。「メジャー・フォルト」はカーネルがディスクからデータを読み込む必要がある際に発生します。既存ページを複製する場合や空のページを割り当てる場合は、通常の(つまり「マイナー」の)フォルトになります。

  • 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 は、先ほどの anonymou メモリの説明にあるものと似ています。正確な計算式は、キャッシュ = active_file + inactive_file + tmpfs です。この正確なルールが使われるのは、カーネルがメモリページを active から inactive にセットする時です。これは anonymous メモリとして使うのとは違って、一般的な基本原理によるものと同じです。注意点としては、カーネルがメモリを再要求(reclaim)するとき、直ちに再要求(anonymous ページや汚れた/変更されたページをディスクに書き込む)よりも、プール上のクリーンな(=変更されていない)ページを再要求するほうが簡単だからです。

  • unevictable: 取り出し要求ができないメモリ容量のことです。一般には mlock によって「ロックされた」メモリとされます。暗号フレームワークにおいて利用されることがあり、秘密鍵や機密情報がディスクにスワップされないようにするものです。

  • memory_limit, memsw_limit: これは実際のメトリクスではありません。この cgroup に適用される上限を確認するためのものです。memory_limit は、このコントロール・グループのプロセスが利用可能な物理メモリの最大容量を示します。memsw_limit は RAM+スワップの最大容量を示します。

ページキャッシュ内のメモリの計算は非常に複雑です。 コントロール・グループが異なるプロセスが 2 つあって、それが同一のファイル(最終的にディスク上の同一ブロックに存在)を読み込むとします。 その際のメモリの負担は、それぞれのコントロール・グループに分割されます。 これは一見すると良いことのように思えます。 しかし一方の cgroup が停止したとします。 そうすると他方の cgroup におけるメモリ使用量が増大してしまうことになります。 両者のメモリ・ページに対する使用コストは、もう共有されていないからです。

CPU メトリクス: cpuacct.stat

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

コンテナごとに疑似ファイル cpuacct.stat があり、ここにコンテナにあるプロセスの CPU 使用率を、 user 時間と system 時間に分割して記録されます。それぞれの違いは:

  • user とはプロセスが CPU を直接制御する時間のことであり、CPU によるプロセス・コードの実行

  • system とはプロセスに代わり CPU のシステムコールを実行する時間

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

Block I/O メトリクス

ブロック 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 フレームワークを使うことにより)、ある程度正しく計測できます。

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

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

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

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

$ iptables -nxvL OUTPUT

技術的なことだけで言えば -n は必要ありません。 DNS の逆引きを避けるためのものですが、ここでの作業ではおそらく不要です。

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

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

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

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

今のところ、メトリクスを確認する一番の方法は、そのコンテナ内部から 確認することです。 これを実現する方法は、ip netns を巧みに 利用します。 これを使えば、コンテナのネットワーク名前空間内に、ホスト環境からモジュールを実行させることができます。

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/<somename> から /proc/<pid>/ns/net へのシンボリック・リンクを生成します。

  • ip netns exec <somename> .... を実行します。

ネットワーク使用量の計測を行おうとしているコンテナ内部において、実行されているプロセスがどの cgroup に属しているかを探し出すには cgroups の確認 を参照してください。 その方法に従って、tasks という名前の擬似ファイルを調べます。 その擬似ファイル内には cgroup 内の(つまりコンテナ内の) PID がすべて示されています。 そのうちの 1 つを取り出して扱います。

環境変数 $CID にはコンテナの「短めの ID」が設定されているとします。 これまで説明してきたことをすべてまとめて、以下のコマンドとして実行します。

$ 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

詳細なメトリクスを収集するためのヒント

新しいプロセスを起動するたびに、メトリクスを最新のものにすることは(比較的)面倒なことです。 高解像度のメトリクスが必要な場合、しかもそれが非常に多くのコンテナ(1 ホスト上に 1000 個くらいのコンテナ)を扱わなければならないとしたら、毎回の新規プロセス起動は行う気になれません。

1 つのプロセスを作り出してメトリクスを収集する方法をここに示します。 メトリクスを収集するプログラムを C 言語(あるいは低レベルのシステムコールを実行できる言語)で記述する必要があります。 利用するのは特別なシステムコール setns() です。 これはその時点でのプロセスを、任意の名前空間に参加させることができます。 そこでは、その名前空間に応じた擬似ファイルへのファイル・ディスクリプターをオープンしておくことが必要とされます。 (擬似ファイルは /proc/<pid>/ns/net にあることを思い出してください。)

ただしこれは本当のことではありません。 ファイル・ディスクリプターはオープンのままにしないでください。 オープンにしたままであると、コントロール・グループの最後の 1 つとなるプロセスがある場合に、名前空間は削除されず、そのネットワーク・リソース(コンテナの仮想インターフェースなど)がずっと残り続けてしまいます。 (あるいはそれは、ファイル・ディスクリプターを閉じるまで続きます。)

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

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

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

Docker は lxc-start によって処理を行うため、リアルタイムなメトリクス収集は困難です。 lxc-start が自身の処理の後に、まわりをきれいにしてしまうためです。 メトリクスの収集は、一定間隔をおいて取得するのが、より簡単な方法と言えます。 collectd にある LXC プラグインは、この方法により動作しています。

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

各コンテナにおいて情報収集用のプロセスを実行し、コントロール・グループに移動させます。 このコントロール・グループは監視対象としたいものであり、cgroup のタスクファイル内に PID を記述しておきます。 情報収集のプロセスは、定期的にそのタスクファイルを読み込み、そのプロセス自体が、コントロールグループ内で残っている最後のプロセスであるかどうかを確認します。 (前節に示したように、ネットワーク統計情報も収集したい場合は、そのプロセスを適切なネットワーク名前空間に移動することも必要になります。)

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

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