LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第62回Linuxカーネルのコンテナ機能 - cgroup v2から使うメモリコントローラ(7)

第56回から続けてcgroup v2から使うメモリコントローラの機能を紹介してきました。今回も引き続き、まだ紹介していない機能を紹介していきます。

memory.reclaim

まずはmemory.reclaimファイルです。このファイルは5.19カーネルで導入されました。メモリ回収に関係するファイルです。

表1 memory.reclaimファイル
ファイル名 機能 可能な操作 実装されたバージョン
memory.reclaim このファイルに書き込んだバイト数分メモリを回収する 書き込み専用 5.19

メモリの回収については、第56回で説明していますので、ご参照ください。

Linuxカーネルには、メモリが不足した際にメモリを回収するための仕組みとして、kswapdとdirect reclaim(直接回収)が存在しています。これらについては、ここでは詳しく説明しません。いずれもシステム上のメモリが不足するような、メモリ負荷が高い状態でメモリを回収する仕組みです。つまり、空きメモリがすでに少なくなった状態で使われる機能です。

memory.reclaimファイルは、これらのカーネルに備わった仕組みを置き換える機能ではありません。すでにメモリが足りない状況ではなく、メモリが足りている状況でも、積極的にメモリを監視して、メモリを安定して供給する仕組みです。ユーザ空間でメモリを常時監視し、負荷が高いタスクや新たに起動するタスクがメモリを利用できるように、しばらくアクセスされていない(inactiveな)メモリを解放するようなユースケースが考えられています。

カーネルは、アクセスが発生している(activeな)メモリ領域なのか、inactiveなメモリ領域なのかをLRUリストで管理しています [1]。メモリ回収は、このリストを使って実行されます。このリストは、負荷への影響を考慮して、常に最新の状態に更新されているわけではありません。

LRUリストは、メモリへのアクセスや割り当て、回収処理が発生した際に更新されます。

そこで、ユーザ空間からメモリを監視し、memory.reclaimファイルを使って、少量のメモリを定期的に回収させることで、LRUリストを最新の状態に更新できます。

これにより、カーネルがより正確にメモリの使用状況を把握できるようになります。その結果、より多くのメモリを必要とするタスクや、新規タスクをスケジューリングするために、メモリを確保しやすくなります。

memory.reclaimファイルの使い方

memory.reclaimファイルは、書き込み専用のファイルで、ファイルに書き込んだ値だけメモリを回収する処理を起動します。次のように使います。

echo "1G" > memory.reclaim

この例では、メモリを1GB回収しようとする処理が起動します。もちろん、回収するメモリがない場合は指定した値より少ない量だけが回収される場合もあります。その場合はエラーEAGAINが返ります。メモリの使用状況によっては、指定した値以上に回収される可能性もあります。

メモリ使用量の確認方法

memory.reclaimファイルの動きを確認するには、cgroupに所属するプロセスのメモリ使用量を確認する必要があります。今回も、ここまでの連載で使ったようにmemory.currentを使います。そのほかに、メモリの統計情報を表示するmemory.statファイルを使います。

memory.statファイルは、プロセスが使用しているメモリの状況をメモリタイプごとに表示します。1行に1つ、エントリ名と値が表示されており、執筆時点で最新のLTSカーネルである6.18では70行ほどのエントリが並んでいます。

このうち、今回の説明で使用するエントリを表2で紹介します。

表2 memory.statファイルの今回使用するエントリ
エントリ名 説明
anon 匿名メモリが使っているメモリ量
file ページキャッシュが使っているメモリ量[2]
pgsteal cgroupが作成されてから回収されたページ数

匿名メモリやページキャッシュについてはあとで説明します。


それでは、実際にmemory.reclaimファイルを使って、動きを見ていきましょう。今回の実行例は、Ubuntu 24.04環境で確認しています。

ページキャッシュを使った実験

まずは、ファイルシステム上に保存されているファイルへのアクセスを高速化するために、ファイルのデータをメモリ上にコピーしているページキャッシュを使い、memory.reclaimファイルの動きを見ていきます。

ページキャッシュについては、第52回でもう少し詳しく説明していますので、そちらもご覧ください。

ストレージ上のファイルデータと、メモリ上のページキャッシュが完全に一致している場合、ページキャッシュを消しても、データはストレージにあるわけですから、ページキャッシュとして使われているメモリを回収しても問題はありません。

また、ページキャッシュのデータが更新されている場合は、ストレージへ書き戻せば、同様にページキャッシュを回収しても問題ありません。

このように、ページキャッシュとして使われているメモリは、回収の対象としやすいです。memory.reclaimファイルの動きを見る場合にも、簡単に動作を確認できます。

そこで実際に、ファイルのデータをページキャッシュに載せ、memory.reclaimファイルを使って回収させてみましょう。

まずは、確認に使うcgroupを作成し、プロセスを登録します。

$ sudo mkdir /sys/fs/cgroup/test01
$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
(子孫cgroupでメモリコントローラが使えるように設定)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs
(シェルをtest01に所属させる)
1164

Ubuntuでシステムを起動すると、root cgroupでメモリコントローラが利用できるように設定されている状態が多いと思いますが、設定されていない環境もあるかもしれません。そのため、念のため子孫でメモリコントローラが利用できるように操作しています。

この時点のメモリ消費量をmemory.currentmemory.statファイルで確認します。

$ cat /sys/fs/cgroup/test01/memory.current
532480
(cgroupのメモリ消費量の確認)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 446464
file 0
(cgroupのメモリ消費量の詳細の確認)

memory.currentファイルを見ると、500KBほどのメモリを消費していることがわかります。memory.statファイルを見ると、もう少し詳細にメモリがどのように使われているかを確認できます。fileは0ですので、ページキャッシュとしては使われていないことがわかります。

ここでファイルをddコマンドで作成してみましょう。ddコマンドでのファイル作成はページキャッシュ経由ですので、ページキャッシュとしての消費量が増えるはずです。

$ dd if=/dev/urandom of=/tmp/testfile bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 1.69457 s, 317 MB/s
(ファイルの作成)
$ cat /sys/fs/cgroup/test01/memory.current 
553488384
(cgroupのメモリ消費量の確認)
$ grep -E '(^file |^anon |^pgsteal )' /sys/fs/cgroup/test01/memory.stat
anon 454656
file 536944640
pgsteal 0
(cgroupのメモリ消費量の詳細とメモリ回収量の確認)

上の例では、500MBほどのファイルを作成しています。ファイルを作成したのち、memory.currentファイルを確認すると500MBほど増えていることがわかります。

そして、memory.statファイルを見ると、fileエントリの値が増えており、こちらも500MBほど増えています。anonはそれほど変化がないので、主にファイルがメモリ上にキャッシュされていることにより、cgroupに所属するプロセスのメモリ消費が増加したことがわかります。あとで比較するために、pgstealの値も確認しました。0ですので、この時点ではこのcgroupで回収されたメモリはありません。

ここで、memory.reclaimファイルを使って200MBほどメモリを回収させてみます。

$ echo "200M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 
200M

memory.reclaimファイルに200Mという文字列を書き込みました。

さきほどと同様にmemory.currentmemory.statファイルを確認してみましょう。

$ cat /sys/fs/cgroup/test01/memory.current 
338075648
(cgroupのメモリ消費量の確認)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 479232
file 327229440
(cgroupのメモリ消費量の詳細の確認)

memory.currentファイルの値は確かに200MBほど減少しています。そして、memory.statファイルを確認すると、さきほどと同様にanonは値が大きく変化しておらず、fileの値は200MBほど減少していますので、ページキャッシュからメモリが回収されたことがわかります。

ここで、memory.stat内でメモリ回収量を示すpgstealの値を見ると51200となっています。pgstealの値はページ数ですので、51200*4096でちょうど200MB回収されたことがわかります。

$ grep 'pgsteal ' /sys/fs/cgroup/test01/memory.stat
pgsteal 51200
(pgstealはページ数なので51200*4096=200MB)

memory.reclaimを使って、ページキャッシュからメモリが回収される様子が確認できました。

スワップがあるときの匿名メモリの実験

次に、ページキャッシュのようにファイルをキャッシュしているメモリではなく、mallocなどでプログラムから動的に確保されたメモリをmemory.reclaimファイルで回収させてみましょう。このようなメモリは匿名(anonymous)メモリと呼びます。

ファイルのキャッシュの場合は、ファイルという実体がありますのでメモリを解放しても問題ありません。

しかし、匿名メモリの場合は、他に同じデータはシステム上に存在しませんので、いきなりメモリを解放するわけにはいきません。回収する場合はスワップ領域へ書き出す必要があります。

スワップが存在するシステム上で、memory.reclaimファイルを使って、匿名メモリからメモリを回収させてみましょう。

まずは実験の準備をしたあと、メモリ消費量を確認します。

$ sudo mkdir /sys/fs/cgroup/test01
$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1011
$ grep SwapTotal /proc/meminfo 
SwapTotal:       3304444 kB
(スワップがある)
$ cat /sys/fs/cgroup/test01/memory.current 
401408
(cgroupのメモリ消費量の確認)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 323584
file 0
(cgroupのメモリ消費量の詳細の確認)

上のように、/proc/meminfoで確認したとおりスワップ領域が存在しています。そして、シェルのプロセスを登録した時点でのメモリ消費をmemory.currentmemory.statファイルで確認しました。

それでは、前回までの実行例で使用していたstress-ngコマンドを使って、匿名メモリを確保します。

$ stress-ng --vm 1 --vm-bytes 512M --vm-hang 0 > /dev/null 2>&1 &
[1] 1047
$ cat /sys/fs/cgroup/test01/memory.current 
564518912
(cgroupのメモリ消費量の確認)
$ cat /sys/fs/cgroup/test01/memory.swap.current 
0
(スワップの使用量は0)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 538857472
file 23613440
(cgroupのメモリ消費量の詳細の確認)

512MBのメモリを消費するようにstress-ngコマンドを実行しました。memory.currentファイルを確認すると、およそ512MBほどメモリ消費が増加していることがわかります。そのときの匿名メモリとページキャッシュの消費量もmemory.statファイルで確認しました。

ここでmemory.reclaimファイルを使って256MBのメモリを回収するように指示します。

$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 
256M
$ cat /sys/fs/cgroup/test01/memory.current 
295796736
(cgroupのメモリ消費量の確認)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 293531648
file 20480
(ページキャッシュと匿名メモリの両方から回収されている)
$ cat /sys/fs/cgroup/test01/memory.swap.current 
245645312
(スワップの使用量が増えている)

memory.reclaimファイルに書き込んだ直後に、memory.currentファイルを確認すると295,796,736(バイト)で、stress-ngコマンド実行直後との差分を取ると564,518,912-295,796,736=268,722,176(バイト)となり、ほぼ256MBのメモリが回収され、空きメモリが増えたことがわかります。

memory.statファイルを確認すると、ページキャッシュとして消費していた分はほぼすべて回収され、残りは匿名メモリから回収されていることがわかります。

このとき、スワップ消費量を示すmemory.swap.currentファイルを確認すると、memory.statファイル内のanonの値が減少した分と同じくらいスワップ消費量が増えており、匿名メモリで消費されていた分のメモリ領域がスワップに移動したことがわかります。

memory.reclaimファイルで回収を指示すると、匿名メモリに確保されていたメモリがスワップに移動することが確認できました。

スワップがないときの匿名メモリの実験

ここまでで、匿名メモリ上のデータがスワップに移動し、メモリが回収されることを確認しました。

それではスワップがない場合、memory.reclaimファイルを使うとどうなるのでしょうか? 確認してみましょう。

$ sudo mkdir /sys/fs/cgroup/test01
$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs
1036
$ sudo swapoff -a
(スワップの無効化)

これまでと同様にcgroupを準備し、スワップを無効にします。

$ cat /sys/fs/cgroup/test01/memory.current
454656
$ stress-ng --vm 1 --vm-bytes 512M --vm-hang 0 > /dev/null 2>&1 &
[1] 1040
$ cat /sys/fs/cgroup/test01/memory.current
564621312
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 538935296
file 23793664

先の実験と同様に、stress-ngコマンドで512MBのメモリを消費させます。memory.currentmemory.statファイルでメモリ消費量の増加と、匿名メモリ消費量の増加も確認できました。

ここで、memory.reclaimファイルを使って256MBのメモリを回収するように指示します。

$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim
256M
(返ってこない)

すると、実行したechoコマンドが返ってきません。

別シェルを起動してmemory.currentファイルを確認すると、メモリ消費量は減少していません。同様にmemory.statファイルを確認しても、匿名メモリの消費量は減少しておらず、メモリが回収されていないことがわかります。

$ cat /sys/fs/cgroup/test01/memory.current
559521792
(回収されていない)
$ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat
anon 540250112
file 17027072

このように匿名メモリが消費されており、データを移動させるスワップがない場合は、メモリが回収できないことがわかります。

実験では、応答が返ってこないechoコマンドをしばらく放置しました。しかし、返ってこないのでCtrl+Cで終了させました。このように、指定した量を回収するまで応答が返らない場合があります。先に説明したように、指定した量より少なく回収してEAGAINが返ることもあるようです。

ここまでで、memory.reclaimファイルの動きが確認できました。

memory.peak

memory.peakファイルは、5.19カーネルで追加されたファイルです。

このファイルには、cgroupが作成されてから、そのcgroupと子孫のcgroupで消費されたメモリの最大値が記録されています。

6.12カーネルからは、値がリセットできるようになりましたので、リセット後の最大値を確認できます。

また、同様にスワップ消費量の最大値もmemory.swap.peakというファイルから確認できます。

表3 memory.peakファイル
ファイル名 機能 可能な操作 デフォ
ルト値
実装されたバージョン
memory.peak cgroupとその子孫cgroupに対して、cgroup作成時またはリセット以降に使用されたメモリ消費の最大値 読み書き 0 5.19(書き込み可能は6.12)
memory.swap.peak cgroupとその子孫cgroupに対して、cgroup作成時またはリセット以降に使用されたスワップ消費の最大値 読み書き 0 6.5(書き込み可能は6.12)

さて、表3に示すとおり、memory.peakファイルの動きをすべて確認するには、6.12以降のカーネルが必要になります。

今回は、先に書いたようにUbuntu 24.04を利用しています。しかし、24.04の標準のカーネルは6.8ですので、memory.peakファイルのリセット機能は試せません。

実際、6.8カーネルの環境では、memory.peakmemory.swap.peakファイルは次のように読み取り専用となっています。

$ uname -r
6.8.0-110-generic
(24.04標準のカーネルは6.8.0)
$ ls -l /sys/fs/cgroup/user.slice/memory*peak
-r--r--r-- 1 root root 0 Apr 25 12:09 /sys/fs/cgroup/user.slice/memory.peak
-r--r--r-- 1 root root 0 Apr 25 12:09 /sys/fs/cgroup/user.slice/memory.swap.peak
(読み取り専用)

Ubuntuの場合は24.04にHWEカーネルを入れるか25.04以降を使うと、リセット機能に対応したバージョンになります。

次のように24.04でHWEカーネルを入れると、memory.peakmemory.swap.peakファイルも読み書き可能になりました。

$ uname -r
6.17.0-22-generic
(HWEカーネルをインストールしバージョンが上がった)
$ ls -l /sys/fs/cgroup/user.slice/memory*peak
-rw-r--r-- 1 root root 0 Apr 25 12:19 /sys/fs/cgroup/user.slice/memory.peak
-rw-r--r-- 1 root root 0 Apr 25 12:19 /sys/fs/cgroup/user.slice/memory.swap.peak
(読み書き可能になった)

それではこの環境でmemory.peakファイルの動きを確認していきましょう。

これまでと同様に準備したあと、memory.peakmemory.swap.peakファイルの中身を確認します。

$ sudo mkdir /sys/fs/cgroup/test01
$ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control 
+memory
$ cat /sys/fs/cgroup/test01/memory.peak 
0
$ cat /sys/fs/cgroup/test01/memory.swap.peak 
0
(いずれのファイルも初期値は0)

上の実行例のように、test01 cgroupを作成した直後は、いずれのファイルにも0が記録されています。

$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1058
$ cat /sys/fs/cgroup/test01/memory.peak 
524288

シェルをtest01 cgroupに登録すると、memory.peakファイルの値が増えます。

ここでstress-ngコマンドでメモリを消費させてみましょう。

$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 > /dev/null 2>&1 &
[1] 1096
$ cat /sys/fs/cgroup/test01/memory.peak 
311988224
$ cat /sys/fs/cgroup/test01/memory.current
311984128

256MBメモリを使用するようにstress-ngコマンドを実行すると、memory.peakファイルの値が増加しました。このとき当然、memory.currentファイルの値も増加しています。

stress-ng実行前後の値の差から、stress-ngコマンドの実行によって増加したメモリ消費量がわかります。

$ kill 1096
(stress-ngプロセスをkill)
$ cat /sys/fs/cgroup/test01/memory.peak 
312279040
$ cat /sys/fs/cgroup/test01/memory.current 
24150016

その後、stress-ngプロセスをkillしました。memory.currentファイルを確認すると、値は減少した一方で、memory.peakファイルの値は減少していません。

それでは、引き続き値のリセットを試していきましょう。

ここで注意が必要なのは、カーネル付属文書に記載されているこの一文です。

A write of any non-empty string to this file resets it to the current memory usage for subsequent reads through the same file descriptor.

訳すと「このファイルに空でない文字列を書き込むと、その後同じファイルディスクリプタを通して読み取る際、メモリ使用量が現在の値にリセットされます」です。

つまり、memory.peak自身の値がグローバルにリセットされるわけではなく、文字列を書き込んだファイルディスクリプタを通して読み込むときのみ、現在の値にリセットされます。

例としてPythonのスクリプトからリセットを試してみましょう。

$ cat reset.py 
#!/usr/bin/env python3
# test01 cgroupのmemory.peakに0という文字列を書き込み、値を読む
with open('/sys/fs/cgroup/test01/memory.peak', 'r+') as f:
    f.write('0')
    f.seek(0)
    print(f.read().strip())
(memory.peakの値をリセットし、その後値を表示するためのスクリプト)
$ cat /sys/fs/cgroup/test01/memory.peak
312279040
$ sudo python3 reset.py 
29687808
(スクリプトを実行し、リセットして値を表示)
$ cat /sys/fs/cgroup/test01/memory.current
23986176
$ cat /sys/fs/cgroup/test01/memory.peak 
312279040
(別のファイルディスクリプタで読むとリセットされる前の値が読み取れる)

上の実行例のように、Pythonのスクリプトからファイルをオープンして文字列を書き込んだあとに、そのまま同一のファイルディスクリプタから値を読み取ると、その時点のmemory.currentの値に近い値にリセットされていることがわかります[3]

一方で、catコマンドを実行し、別のファイルディスクリプタを使ってmemory.peakファイルを表示させると、リセットされていない最大値が表示されます。

このように、オープンしたファイルディスクリプタごとにピーク値を追跡できますので、複数のファイルディスクリプタをオープンし、リセットすることで任意のタイミングで独立したメモリ消費の最大値を追跡できます。

たとえば、

  • fd-A:ワークロード開始前にオープンしてリセット → ワークロード全体の最大値を観測
  • fd-B:ワークロードの途中でオープンしてリセット → その時点以降の最大値を観測

というようなことができます。

memory.peakファイルの動きは確認できましたので、次はmemory.swap.peakの動きを簡単に見ておきましょう。これまでの実行例と同じように準備し、stress-ngコマンドでメモリを消費させます。

$ sudo mkdir /sys/fs/cgroup/test01
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1130
$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 > /dev/null 2>&1 &
[1] 1162
$ cat /sys/fs/cgroup/test01/memory.swap.current 
0
$ cat /sys/fs/cgroup/test01/memory.swap.peak 
0

この時点で、memory.swap.currentmemory.swap.peakファイルを確認しました。この時点では、スワップにはデータがなく、両方とも0です。

では、今回最初に紹介したmemory.reclaimを使ってメモリを回収させてみましょう。stress-ngコマンドでメモリを消費させており、匿名メモリを使っていますので、メモリ上のデータはスワップに書き出されるはずです。

$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 
256M
$ cat /sys/fs/cgroup/test01/memory.swap.current 
246075392
$ cat /sys/fs/cgroup/test01/memory.swap.peak 
246136832

memory.reclaimを使って256MBのメモリを回収させると、memory.swap.currentmemory.swap.peakファイルともに同じくらいの値になっています。

ここで、stress-ngのプロセスをkillしてみます。stress-ngプロセスが消費していたメモリは不要になりますので解放されるはずです。

$ kill 1162
$ cat /sys/fs/cgroup/test01/memory.swap.current 
0
$ cat /sys/fs/cgroup/test01/memory.swap.peak 
246136832

killコマンド実行後は、memory.swap.currentファイルは0となりました。一方で、memory.swap.peakファイルは、killする前と値が変わっておらず、これまでに消費した最大の値が記録されたままであることがわかります。

ここまでで、memory.peakmemory.swap.peakファイルの動きが確認できました。

まとめ

今回は、指定したメモリ量を回収させるmemory.reclaimファイルと、cgroupで消費したメモリの最大値を記録するmemory.peakmemory.swap.peakファイルについて説明しました。

memory.reclaimファイルの説明では、実際にメモリ回収が起こる様子とともに、ページキャッシュと匿名メモリから回収する際の動きの違いも確認しました。memory.reclaimによるメモリ回収の動きを見ることで、ページキャッシュと匿名メモリから回収される際の動きの違いもわかりますね。

memory.peakファイルについては、値をリセットすることで、任意の間隔でのメモリ消費の差を見ることができることも確認できました。

さて、次回はcgroup v2でまだ紹介していない機能について紹介する予定です。

おすすめ記事

記事・ニュース一覧