cachectld〜無駄なページキャッシュの削除を自動化〜

原稿の執筆が一段落して心に余裕が出てきた@cubicdaiyaです。

今回はサーバを運用しているとありがちなページキャッシュに関する問題とメルカリのアプローチについて解説します。

Fluentdによるログ転送

話は変わりますが、メルカリの各サーバ上ではプログラムが吐いたログデータをKibanaやNorikraといった各種コンポーネントに転送するためにFluentdが稼働しています。各ログデータは原則単一のファイルに追記されてFluentdのtailプラグインによって各所に転送されていきます。

ログデータのサイズはまちまちで、1日で数GB程度のログデータもあれば数十GB以上のログデータもあります。

ページキャッシュと巨大なログファイル

各サーバに吐かれるログデータのサイズはサーバに搭載されているメモリのサイズと比べると1日分だけでもかなりの量になります。そして、このように絶えず書き込まれる巨大なログファイルが存在する場合、ログファイルのページキャッシュがサーバのメモリを圧迫してしまうことがあります。これはログデータがファイルに追記されていく度にそのログデータの内容がページキャッシュに載るためです。

先述のとおりメルカリの各サーバのログデータのサイズは1日分だけでも数GBから数十GB以上になります。このため本来ほとんどキャッシュする必要のないログデータがメモリを圧迫し、サーバがスワップやスラッシングを起こしているケースが珍しくありませんでした。

ログデータはローテートのタイミングを除けば末尾のデータ以外はほぼ読まれることがないので、そんなものよりもっとほかのプログラムにメモリを割り当てくれよ、というのがシステム管理者の本音ですが、文句を言っても現実は変わりません。プログラムを書きましょう。

posix_fadviseとPOSIX_FADV_DONTNEED

巨大なログファイルのページキャッシュがメモリを圧迫しないようにするためのアプローチの一つにposix_fadvisePOSIX_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

github.com

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によるページキャッシュの定期的な削除

cachectldcachectlが行うページキャッシュの削除処理を定期的に実行するデーモンプログラムです。まず、こんな感じの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月に私が入社した頃はあちこちのサーバが頻繁にスワップやスラッシングを起こしていましたが、現在はほとんど起こっていません。

まとめ

ログファイルへの絶え間ない書き込みが起こす無駄なページキャッシュの増加とそれを解消するためのツールであるcachectlcachectldを用いたメルカリのアプローチについて解説しました。

cachectlcachectldは以下の特徴を持ったposix_fadviseのラッピングツールです。

  • 実行対象にファイルだけでなくディレクトリも指定できる
  • 実行対象がディレクトリの際に-filterで指定したパターンにマッチしたファイルだけを実行対象にできる
  • 単体でページキャッシュ削除の定期実行が可能
  • TOMLによるページキャッシュの削除対象となるファイルやディレクトリの集中的な管理

cachectlcachectldがやってることは地味ではありますが、サーバリソースの無駄遣いを防ぐにはもってこいのツールで、自分がつくったツールの中でもかなりお気に入りです。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加