ランタイムメトリックス

読む時間の目安: 6 分

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 の確認

The file layout of cgroups is significantly different between v1 and v2.

If /sys/fs/cgroup/cgroup.controllers is present on your system, you are using v2, otherwise you are using v1. Refer to the subsection that corresponds to your cgroup version.

cgroup v2 is used by default on the following distributions:

  • Fedora (since 31)
  • Debian GNU/Linux (since 11)
  • Ubuntu (since 21.10)

cgroup v1

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

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

cgroup v2

On cgroup v2 hosts, the content of /proc/cgroups isn’t meaningful. See /sys/fs/cgroup/cgroup.controllers to the available controllers.

Changing cgroup version

Changing cgroup version requires rebooting the entire system.

On systemd-based systems, cgroup v2 can be enabled by adding systemd.unified_cgroup_hierarchy=1 to the kernel cmdline. To revert the cgroup version to v1, you need to set systemd.unified_cgroup_hierarchy=0 instead.

If grubby command is available on your system (e.g. on Fedora), the cmdline can be modified as follows:

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

If grubby command is not available, edit the GRUB_CMDLINE_LINUX line in /etc/default/grub and run sudo update-grub.

Running Docker on cgroup v2

Docker supports cgroup v2 since Docker 20.10. Running Docker on cgroup v2 also requires the following conditions to be satisfied:

  • containerd: v1.4 or later
  • runc: v1.0.0-rc91 or later
  • Kernel: v4.15 or later (v5.2 or later is recommended)

Note that the cgroup v2 mode behaves slightly different from the cgroup v1 mode:

  • The default cgroup driver (dockerd --exec-opt native.cgroupdriver) is “systemd” on v2, “cgroupfs” on v1.
  • The default cgroup namespace mode (docker run --cgroupns) is “private” on v2, “host” on v1.
  • The docker run flags --oom-kill-disable and --kernel-memory are discarded on v2.

特定コンテナーに対応する cgroup の検索

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

cgroup を利用する Docker コンテナーにおいて、コンテナー名は、コンテナーの完全 ID か、あるいは長めの ID となります。 docker psによってコンテナーが ae836c95b4c3 のように示されていたら、長めの ID はたとえばae836c95b4c3c9e9179e0e91015512da89fdec91612f63cebae57df9a5444c79のようなものになります。 これはdocker inspectを用いるか、あるいはdocker 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 v2、systemdドライバー利用時。

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

Note

This section is not yet updated for cgroup v2. For further information about cgroup v2, refer to the kernel documentation.

各サブシステム(メモリ、CPU、ブロック I/O)に対しては、擬似ファイルシステムが存在し、そこに統計情報が含まれます。

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

メモリメトリックスは cgroup の「memory」にあります。 メモリコントロールグループには多少のオーバーヘッドがあります。 ホスト上のメモリ利用量をきめ細かく算出しているためです。 したがって各種ディストリビューションの多くでは、デフォルトでこれを無効にしています。 これを有効にする方法は、一般的にはカーネルのコマンドラインパラメーター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 内のプロセスに対応する統計情報であり、サブ crgoup は除くものです。 後半部分(total_が先頭につくもの)は同様ですが、ただしサブ cgroup を含むものです。

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

メトリックス 内容説明
cache このコントロールグループのプロセスによるメモリ使用量です。ブロックデバイス上の各ブロックに細かく関連づけられるものです。ディスク上のファイルと読み書きを行うと、この値が増加します。ふだん利用する I/O(システムコールの openreadwrite)利用時に発生し、(mmapを用いた)マップファイルの場合も同様です。tmpfsによるメモリ使用もここに含まれますが、理由は明らかではありません。
rss ディスク上の操作に対応づかないメモリ使用量です。たとえばスタック、ヒープ、匿名メモリマップなどです。
mapped_file このコントロールグループのプロセスによって割り当てられるメモリの使用量です。メモリを どれだけ 利用しているかの情報は得られません。ここからわかるのは どのように 利用されているかです。
pgfault, pgmajfault cgroup のプロセスにおいて発生した「ページフォールト」、「メジャーフォールト」の回数を表わします。ページフォールトは、プロセスがアクセスした仮想メモリスペースの一部が、存在していないかアクセス拒否された場合に発生します。存在しないというのは、そのプロセスにバグがあり、不正なアドレスにアクセスしようとしたことを表わします(SIGSEGVシグナルが送信され、Segmentation faultといういつものメッセージを受けたとたんに、プロセスが停止されます)。アクセス拒否されるのは、スワップしたメモリ領域、あるいはマップファイルに対応するメモリ領域を読み込もうとしたときに発生します。この場合、カーネルがディスクからページを読み込み、CPU のメモリアクセスを成功させます。またコピーオンライトメモリ領域へプロセスが書き込みを行う場合にも発生することがあります。同様にカーネルがプロセスの切り替え(preemption)を行ってからメモリページを複製し、ページ内のプロセス自体のコピーに対して書き込み処理を復元します。「メジャーフォールト」はカーネルがディスクからデータを読み込む必要がある際に発生します。既存ページを複製する場合や空のページを割り当てる場合は、通常の(つまり「マイナー」の)フォールトになります。
swap この cgroup 内のプロセスによって現時点利用されているスワップ総量です。
active_anoninactive_anon カーネルによって アクティブ非アクティブ のいずれかに特定される 匿名 メモリの使用量です。「匿名」 メモリとは、ディスクページにひもづいて いない メモリのことです。別の表現でいえば、上で示した rss カウンターと同等のものです。正確な rss カウンターの定義式は、active_anoninactive_anontmpfs です。(このコントロールグループによってマウントされているtmpfsファイルシステムが利用するメモリ使用量のことです。)では “アクティブ” と “非アクティブ” の違いは? ページは初めは “アクティブ” です。一定間隔でカーネルがメモリを走査し、一部に “非アクティブ” というタグをつけます。再度アクセスが行われると、すぐに “アクティブ” というタグにつけかえられます。カーネルがほぼメモリ不足に陥って、ディスクへのスワップが必要になると、カーネルは “非アクティブ” ページをスワップします。
active_file, inactive_file 上で示した anon メモリと同様、アクティブ非アクティブ の状態があるキャッシュメモリのこと。正確な式で表現すると、cacheactive_fileinactive_filetmpfs です。カーネルが採用する規則として、アクティブ、非アクティブなメモリページを移動させる方法は、匿名メモリのときとは異なります。ただしその一般的な原理は同じです。カーネルがメモリを要求するとき、プール上からクリーンな(修正がかかっていない)ページを取り出すことの方が簡単に済みます。取り出すことがすぐにできるからです。(一方、匿名ページや、修正のかかった汚れたページでは、その前にディスクに書き出すことが必要になるからです。)
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 を直接制御して、プロセスコードを実行している時間のことです。
  • system時間は、カーネルがプロセスのためにシステムコールを実行している時間のことです。

この時間は 1/100 秒の tick という周期で表わされます。 別名「user jiffies」ともいいます。 1 秒にはUSER_HZ分の「jiffies」があり、x86 システムではUSER_HZは 100 です。 これまでの経緯として、これは 1 秒に割り当てられるスケジューラー「ticks」の数です。 ただしそれ以上に頻繁にスケジューリングされることや、tickless kernels があり、これらは ticks 数は関係がなくなります。

ブロック I/O メトリックス

ブロック I/O はblkioコントローラーにおいて計算されます。 さまざまなメトリックスが、さまざまなファイルにわたって保持されています。 より詳細は、カーネルドキュメント内にある blkio-controller ファイルに記述されていますが、以下では最も関連のあるものを簡潔に示します。

メトリックス 内容説明
blkio.sectors 512 バイトのセクター数。cgroup のプロセスメンバーによって、デバイスごとに読み書きされます。読み書きは 1 つのカウンターに合計されます。
blkio.io_service_bytes cgroup によって読み書きされるバイト数を表わします。デバイスごとに 4 つのカウンターがあります。1 つのデバイスつき、同期、非同期 I/O の別、読み込み、書き込みの別があるからです。
blkio.io_serviced 処理された I/O 操作の数。そのサイズとは無関係です。デバイスごとに、やはり 4 つのカウンターがあります。
blkio.io_queued この cgroup において、その時点でキューに入っている I/O 操作の数を表わします。言い換えると cgroup に I/O が発生していなければ、この値はゼロになります。一方、この逆は正しくなりません。I/O がキューに入っていなかったとしても、それは cgroup が(I/O 的に)アイドルであるとは言えません。普段は静止しているデバイスが、純粋に同期読み込み処理を行っているかもしれないからです。その場合には、I/O 操作をすぐに処理できるわけであり、キューに入れることなく扱うことができます。またこのメトリックスは I/O サブシステム上のどの cgroup に負荷がかかっているかがわかります。ただし示される値は相対的な量にすぎません。プロセスグループがこれ以上に I/O を処理しない場合であっても、他のデバイスの影響によりデバイス負荷が増加するため、キューサイズも増加することになります。

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

ネットワークメトリックスは、コントロールグループによって直接表わされるものではありません。 わかりやすく説明します。 ネットワークインターフェースは、ネットワーク名前空間 コンテキストの中に存在します。 カーネルは、プロセスグループとの間で送受信されるパケットやバイトに関して、メトリックスを収集します。 ただこのメトリックスはあまり役に立つものではありません。 欲しいのはインターフェースごとのメトリックスであるはずです。 (なぜならメトリックスではloインターフェースに発生するトラフィックはカウントされません。) もっとも 1 つの cgroup は、複数のネットワーク名前空間に属することができるため、そのメトリックスを計算することは、より難しくなります。 複数のネットワーク名前空間になるということは、loインターフェースが複数あるということであり、場合によっては複数のeth0インターフェースを持つこともあります。 コントロールグループを用いてネットワークメトリックスを簡単に集めることができないのは、こういった理由によります。

そのかわり、ネットワークメトリックスは別の情報から収集することができます。

IPtables

iptables (むしろ iptables がインターフェースとなる netfilter フレームワーク)から重要な情報が得られます。

たとえばウェブサーバー上におけるアウトバウンド HTTP トラフィックを計算するルールを設定することができます。

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

ここでは-jフラグや-gフラグは用いません。 このルールがパケットをカウントし、後続のルールの処理を行います。

このカウンター値は以下のようにして確認できます。

$ iptables -nxvL OUTPUT

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

カウンターにはパケット数とバイト数があります。 このトラフィックのようなメトリックスを設定したい場合は、forループを実行して、コンテナー IP アドレスに対して 2 つのiptablesルールをFORWARDチェーンに追加します(1 方向に対して 1 つ)。 これにより 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 つのネットワーク名前空間、1 つの PID 名前空間、1 つの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はコントロールグループを削除しようとします。 削除に失敗しますが、これはコントロールグループがまだ利用されているからです。 でも問題ありません。 情報収集用のプロセスはこのとき、コントロールグループ内にはただ 1 つのプロセスしか残っていないことが検出できるはずです。 このときこそ、メトリックスをすべて収集するタイミングとなります。

最後にそのプロセスを root コントロールグループに戻して、コンテナーのコントロールグループを削除します。 コントロールグループの削除は、単にそのディレクトリをrmdirで削除するだけです。 ディレクトリ内にファイルが残っているのに、ディレクトリを削除するというのは、やってはいけないことのように思えます。 しかしこれは擬似ファイルシステムです。 普通の取り扱いをする必要のないものです。 すべてをきれいにした後であれば、情報収集用のプロセスを安全に終了させることができます。

docker, metrics, CPU, memory, disk, IO, run, runtime, stats