原稿の執筆が一段落して心に余裕が出てきた@cubicdaiyaです。
今回はサーバを運用しているとありがちなページキャッシュに関する問題とメルカリのアプローチについて解説します。
Fluentdによるログ転送
話は変わりますが、メルカリの各サーバ上ではプログラムが吐いたログデータをKibanaやNorikraといった各種コンポーネントに転送するためにFluentdが稼働しています。各ログデータは原則単一のファイルに追記されてFluentdのtailプラグインによって各所に転送されていきます。
ログデータのサイズはまちまちで、1日で数GB程度のログデータもあれば数十GB以上のログデータもあります。
ページキャッシュと巨大なログファイル
各サーバに吐かれるログデータのサイズはサーバに搭載されているメモリのサイズと比べると1日分だけでもかなりの量になります。そして、このように絶えず書き込まれる巨大なログファイルが存在する場合、ログファイルのページキャッシュがサーバのメモリを圧迫してしまうことがあります。これはログデータがファイルに追記されていく度にそのログデータの内容がページキャッシュに載るためです。
先述のとおりメルカリの各サーバのログデータのサイズは1日分だけでも数GBから数十GB以上になります。このため本来ほとんどキャッシュする必要のないログデータがメモリを圧迫し、サーバがスワップやスラッシングを起こしているケースが珍しくありませんでした。
ログデータはローテートのタイミングを除けば末尾のデータ以外はほぼ読まれることがないので、そんなものよりもっとほかのプログラムにメモリを割り当てくれよ、というのがシステム管理者の本音ですが、文句を言っても現実は変わりません。プログラムを書きましょう。
posix_fadviseとPOSIX_FADV_DONTNEED
巨大なログファイルのページキャッシュがメモリを圧迫しないようにするためのアプローチの一つにposix_fadvise
とPOSIX_FADV_DONTNEED
を利用して定期的に無駄なページキャッシュをメモリから追い出すという方法があります。
#include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> int main(void) { int fd; fd = open("/path/file", O_RDONLY); if(fd == -1) { return 1; } /* /path/fileに割り当てられたページキャッシュをメモリから追い出す */ if(posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) != 0) { close(fd); return 1; } close(fd); return 0; }
あるいはLinuxなら/proc/sys/vm/drop_caches
に1か3を書き込むことですべてのページキャッシュを削除することが可能ですが、必要なページキャッシュまで削除してしまうのが難点です。
# ページキャッシュをすべて削除 # echo 1 > /proc/sys/vm/drop_caches # ページキャッシュとdentryキャッシュをすべて削除 # echo 3 > /proc/sys/vm/drop_caches
また、/proc/sys/vm/swappiness
の値(default:60)を小さい値に調整することでスワップを起きにくくする方法もあります。
# echo 1 > /proc/sys/vm/swappiness
一方で/proc/sys/vm/swappiness
を0にしても完全にスワップしなくなるわけではないので注意しましょう。
posix_fadviseをラップしたツール
さきほどのCプログラムを見ればわかるようにposix_fadvise
を利用して特定のファイルのページキャッシュをメモリから追い出すプログラムを書くのはそんなに難しくありません。
一方でposix_fadvise
をラップしたツールは非常にたくさんあるにも関わらず、自分が求める要件を満たすツールが見つからないのが長い間悩みのタネでした。この手のツールで私が求める主な要件を挙げてみます。
- ページキャッシュの削除対象をファイルだけでなくディレクトリ単位で指定したい
- 指定したディレクトリ内でファイルの種類によって処理内容を変えたい
- ページキャッシュ削除の定期実行
- ↑の処理をcronとfindを組み合わせて同じことするのはもういやだ
- ページキャッシュの削除対象となるファイルやディレクトリの集中的な管理
ないなら自分でつくってしまおう、ということで開発したのが次に紹介するcachectlです。
cachectl
cachectlは特定ファイルのページキャッシュのサイズを確認したり、メモリから追い出すためのツールです。Goで書かれています。
例えばここに約1GBのアクセスログがあります。
$ du -m access.log 1025 access.log $
とりあえずこのファイルの内容を全部ページキャッシュに載せてみます。
$ cat access.log > /dev/null
この状態でaccess.log
に対してcachectl
を実行すると、(-f
の後にファイルあるいはディレクトリのパスを指定します)
$ cachectl -f access.log 2015/07/08 23:48:04 access.log 's pages in cache: 262144/262144 (100.0%) [filesize=1048576.0K, pagesize=4K] $
access.log
のすべての内容がページキャッシュに載っているのが確認できました。続いてページキャッシュをメモリから追い出します。(-op
の後にpurge
を指定するとページキャッシュの削除処理が走ります)
$ cachectl -f access.log -op purge 2015/07/08 23:50:12 purging access.log 's page cache $ cachectl -f access.log 2015/07/08 23:52:48 access.log 's pages in cache: 0/262144 (0.0%) [filesize=1048576.0K, pagesize=4K] $
無事特定のファイルのページキャッシュだけメモリから追い出すのに成功しました。なお、ログファイルの末尾部分は別のプログラムが頻繁に読むことが多いのでページキャッシュに載せたままにしておきたいことがあります。この場合は-r
オプションを利用することでファイルの先頭からファイルサイズの割合分だけページキャッシュを削除することができます。
$ # ファイルの先頭からファイルサイズの90パーセント分のページキャッシュを削除する $ cachectl -f access.log -op purge -r 0.9
ディレクトリ指定による実行
要件にもあったとおり、cachectl
はディレクトリを指定することも可能です。
$ ls /var/log/nginx access.log error.log $ $ cachectl -f /var/log/nginx 2015/07/09 00:02:29 /var/log/nginx/access.log 's pages in cache: 39521/39521 (100.0%) [filesize=158083.2K, pagesize=4K] 2015/07/09 00:02:29 /var/log/nginx/error.log 's pages in cache: 1/1 (100.0%) [filesize=0.3K, pagesize=4K] $ $ cachectl -f /var/log/nginx -op purge -r 0.99 2015/07/09 00:03:20 purging /var/log/nginx/access.log 's page cache 2015/07/09 00:03:20 purging /var/log/nginx/error.log 's page cache $ $ cachectl -f /var/log/nginx 2015/07/09 00:03:23 /var/log/nginx/access.log 's pages in cache: 395/39521 (1.0%) [filesize=158083.2K, pagesize=4K] 2015/07/09 00:03:23 /var/log/nginx/error.log 's pages in cache: 0/1 (0.0%) [filesize=0.3K, pagesize=4K] $
ディレクトリを指定するとcachectl
はそのディレクトリ配下のすべてのファイルに対して操作を行います。
操作対象のフィルタリング
操作対象にディレクトリを指定する場合、そのディレクトリ内の特定のファイルだけを対象に、あるいは無視したい場合があります。例えば以下のようなディレクトリでファイル名に日付が含まれている(すでにローテートされた)ファイルを無視する場合を考えてみましょう。
$ ls /var/log/nginx access.log access.log-20150707.gz access.log-20150708 error.log error.log-20150707.gz error.log-20150708 $
この場合、-filter
で操作対象のファイルパターンを指定することができます。
$ cachectl -f /var/log/nginx -filter "\\.log$" -op purge -r 0.9
cachectl
の基本的な使い方について解説しました。これだけでも十分便利ではありますが、cachectl
ではページキャッシュ削除のオペレーションを定期的に実行するという要件を満たすことができません。そこで登場するのが次に紹介するcachectld
です。
cachectldによるページキャッシュの定期的な削除
cachectld
はcachectl
が行うページキャッシュの削除処理を定期的に実行するデーモンプログラムです。まず、こんな感じのTOMLファイルを用意します。
[[targets]] path = "/var/log/nginx" filter = "\\.gz$" purge_interval = 3600 [[targets]] path = "/var/log/nginx" purge_interval = 600 filter = "[^g][^z]$" rate = 0.9
cachectld
の設定ファイル(以下cachectld.toml
)では操作対象をtarget
として定義し、パス(path
)やページキャッシュ削除の実行間隔(purge_interval
、単位は秒数)を記述します。各パラメータの詳細についてはcachectl/README.mdを参照してください。
-c
にTOMLファイルを与えることでcachectld
を起動することができます。
$ cachectld -c cachectld.toml
この場合は拡張子がgzのファイルは1時間毎に、それ以外のファイルは10分毎にページキャッシュの削除が実行されます。
シグナルトリガーでページキャッシュを削除する
上記の設定では1時間毎あるいは10分毎にページキャッシュの削除が実行されます。しかし、アクセスログやエラーログだとそもそも障害時の調査を除けばログローテートなどの特定のオペレーション時くらいにしか読み込まれなかったりします。
こういったケースのためにcachectld
はUSR1シグナルを受け取るとpurge_interval
を無視して各target
の処理を実行するようになっています。
# pkill -USR1 cachectld
このようにUSR1やUSR2のシグナルで特定の動作をさせるのはUNIX系のプログラムではよく見られる光景です。(例えばdd
にはUSR1シグナルを受け取ると進捗が表示される機能があります)
また、USR1シグナルを受け取ったときのみtarget
の処理を実行するにはpurge_interval
を-1に設定します。
# USR1シグナルを受け取ったときのみ実行する [[targets]] path = "/var/log/nginx" filter = "\\.gz$" purge_interval = -1
メルカリとcachectld
メルカリの各サーバ上では各種ログデータがサーバのメモリを圧迫しないようにcachectld
を稼働させて定期的にページキャッシュを削除するようになっています。昨年の9月に私が入社した頃はあちこちのサーバが頻繁にスワップやスラッシングを起こしていましたが、現在はほとんど起こっていません。
まとめ
ログファイルへの絶え間ない書き込みが起こす無駄なページキャッシュの増加とそれを解消するためのツールであるcachectl
とcachectld
を用いたメルカリのアプローチについて解説しました。
cachectl
とcachectld
は以下の特徴を持ったposix_fadvise
のラッピングツールです。
- 実行対象にファイルだけでなくディレクトリも指定できる
- 実行対象がディレクトリの際に
-filter
で指定したパターンにマッチしたファイルだけを実行対象にできる - 単体でページキャッシュ削除の定期実行が可能
- TOMLによるページキャッシュの削除対象となるファイルやディレクトリの集中的な管理
cachectl
やcachectld
がやってることは地味ではありますが、サーバリソースの無駄遣いを防ぐにはもってこいのツールで、自分がつくったツールの中でもかなりお気に入りです。