Linuxサーバーを長時間運用するとどんどんpagecacheが溜まってきてfreeメモリが涸渇し、Kernelが不要なpagecacheを纏めて解放する際、その上での動作するサービスのレイテンシーが低下するという、あちこちで良く聞く話。
drop_cachesでpagecache丸ごと解放するのは知っていたし、実際に使った事もあるのですが、pagecache全部解放すると、使用頻度の高いcacheまで解放するので若干解放直後は性能劣化するんですよね。
で、今更なんですが下記のエントリでposix_fadvise()で特定のファイルのみpagecacheから解放出来る事を知って試してました。
[linux]linux-ftoolsをUbuntu18.04にインストール
で、色々ググッていたらlinux-ftoolsというものを使うと、ファイル単位にキャッシュ状況が可視化出来ると判ったので、これをUbuntuに入れてみます。
github.com
- githubからリポジトリをクローン
$ git clone https://github.com/david415/linux-ftools.git
- クローンしたディレクトリに入って、./configureを実行
$ cd linux-ftools $ ./configure
- make
なんですが、なぜかエラーになります。
最初はこんなエラー。cd . && /bin/bash /home/toy/linux-ftools/missing --run aclocal-1.10 /home/toy/linux-ftools/missing: line 54: aclocal-1.10: command not found WARNING: `aclocal-1.10' is missing on your system. You should only need it if you modified `acinclude.m4' or `configure.ac'. You might want to install the `Automake' and `Perl' packages. Grab them from any GNU archive site. cd . && /bin/bash /home/toy/linux-ftools/missing --run automake-1.10 --gnu /home/toy/linux-ftools/missing: line 54: automake-1.10: command not found WARNING: `automake-1.10' is missing on your system. You should only need it if you modified `Makefile.am', `acinclude.m4' or `configure.ac'. You might want to install the `Automake' and `Perl' packages. Grab them from any GNU archive site. cd . && /bin/bash /home/toy/linux-ftools/missing --run autoconf configure.ac:7: error: possibly undefined macro: AM_INIT_AUTOMAKE If this token and others are legitimate, please use m4_pattern_allow. See the Autoconf documentation. Makefile:203: recipe for target
とりあえずaptでautomakeをインストール。
$ sudo apt update $ sudo apt upgrade $ sudo apt install automake
で、もう一度configureからやり直し。
$ ./configure ./configure: line 2214: syntax error near unexpected token `linux-ftools,' ./configure: line 2214: `AM_INIT_AUTOMAKE(linux-ftools, 1.0.0)'
むむ〜?と思ってググると
github.com
に答えが。
https://raw.githubusercontent.com/david415/linux-ftools/700823b9fabb28dffb0ed4862eb238ea3e04a6c2/configure
をDLして新しいconfigureとして使うとエラーは消えました。
再度、$ ./configure $ make $ sudo make install
でインストールに成功。
これでfincore,fadvise,fallocateが使える様になります。fincoreがファイルを指定してそのファイルのキャッシュ状況を調べるコマンドです。
[linux]drop_cachesではdirtyなpagecacheは解放されない
さて、以前に組み込み系でLinuxを使っていて長時間評価等をする際に、動作を安定させる目的でcron等で定期的に
(10分毎とか)
echo 3 > /proc/sys/vm/drop_caches
とやるスクリプトとか動かしていました。ログ取りながら繰り返し試験とかやっていると、数時間毎にfreeメモリ涸渇と思われる応答遅延が発生するためこれを回避することが目的です。実際、それなりに効果はあったのですが、自分は特に何も思わずにdrop_cachesへの書き込みでpagecacheが「全部」解放されるものだと思っていました。
例えばredhatの下記のページにも「システムはすべてのページキャッシュメモリを無効にして、解放します。」と書いてあります。
access.redhat.com
が、qiitaの下記の記事を読むと、
qiita.com
https://elixir.bootlin.com/linux/v4.12/source/Documentation/sysctl/vm.txt#L191に、dirtyなエントリは解放されない。解放するなら先にsyncしろと書いてあるよ...と書かれています。
何?..ということで実験。
実験環境は、以下の通りUbuntu 18.04.4 LTSです。
$ uname -a Linux ubuntu01 4.15.0-88-generic #88-Ubuntu SMP Tue Feb 11 20:11:34 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/os-release NAME="Ubuntu" VERSION="18.04.4 LTS (Bionic Beaver)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 18.04.4 LTS" VERSION_ID="18.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=bionic UBUNTU_CODENAME=bionic
echoコマンドで適当なファイルを作った後にdrop_cachesに1を書き込み、fincoreコマンド(先ほどインストールしたlinux-ftools)でキャッシュ状況を確認します。
(各行、//以下は後から付けたコメントです
$ echo "hello world" > test.txt // test.txtに書き込み $ sudo sh -c "echo 1 > /proc/sys/vm/drop_caches" // drop_cachesに1を書き込んでpagecacheを解放 $ fincore --only-cached test.txt // fincoreコマンドでtest.txtのキャッシュ状況を確認。100%キャッシュされている。 filename size total pages cached pages cached size cached percentage 0 test.txt 12 1 1 4096 100.000000 $ sync // syncを掛けてdirtyなpagecacheをディスクに書き出す $ fincore --only-cached test.txt // 再度fincoreコマンドで確認。まだ100%キャッシュされている filename size total pages cached pages cached size cached percentage 0 test.txt 12 1 1 4096 100.000000 $ sudo sh -c "echo 1 > /proc/sys/vm/drop_caches" // もう一度drop_cachesに1を書き込んでpagecache解放 $ fincore --only-cached test.txt // fincoreで確認、キャッシュされていない filename size total pages cached pages cached size cached percentage
こんな感じでtest.txtに書き込み後、sync無しでpagecache解放を実行しても、test.txtの内容はキャッシュされたままです。
sync掛けて、再度pagecache解放を実施するとtest.txtも解放されます。いや、これは知らなかったです。
(ちなみにtest.txtに書き込み後、一定時間(5分程度)待ては定期的なdirty pageの書き込みが走るので、その後ならsync無しでもpagecache解放されます。)
[linux][C言語]posix_fadvise()+POSIX_FADV_DONTNEEDの動作を確認する(サンプルソース付き)
まずはファイル書き込み時にposix_fadvise(POSIX_FADV_DONTNEED)を実行した際の挙動を確認します。
3つのファイルをサイズ4096Bで書き込みます。
- normal.dat - posix_fadvise()を実行しない
- fafirst.dat - open()直後にposix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)
を実行
- falast.dat - write()とclose()の間にposix_fadvise(fd,0,4096,POSIX_FADV_DONTNEED)
を実行
ファイル書き込み時にposix_fadvise(POSIX_FADV_DONTNEED)を実施するサンプルプログラム。
(fadvise_test.c)
/* posix_fadvise test */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #define FILE1 "./normal.dat" #define FILE2 "./fafirst.dat" #define FILE3 "./falast.dat" #define DATSIZE 4096 const char write_data[DATSIZE]; ssize_t my_write(int fd,const void *buf,size_t count){ ssize_t written_size = 0; ssize_t write_res; while(count != 0){ write_res = write(fd,buf,count); if(write_res == -1){ if(write_res == EAGAIN || write_res == EWOULDBLOCK || write_res == EINTR){ continue; } else{ break; } } else{ count -= write_res; written_size += write_res; buf += write_res; } } return written_size; } int main(int argc,char *argv[]){ int fd; ssize_t written_size; int fadvise_res; /* FILE1 write: no posix_fadvise */ fd = open(FILE1,O_CREAT|O_WRONLY,S_IRWXU); if(fd == -1){ fprintf(stderr,"Open %s failed with errno %d.\n",FILE1,errno); return 1; } written_size = my_write(fd,write_data,DATSIZE); printf("%ld bytes are written to %s.\n",written_size,FILE1); close(fd); /* FILE2 write: posix_fadvise(POSIX_FADV_DONTNEED) first */ fd = open(FILE2,O_CREAT|O_WRONLY,S_IRWXU); if(fd == -1){ fprintf(stderr,"Open %s failed with errno %d.\n",FILE2,errno); return 1; } fadvise_res = posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED); if(fadvise_res == -1){ fprintf(stderr,"Fail to set fadvise to %s,errno %d",FILE2,errno); } written_size = my_write(fd,write_data,DATSIZE); printf("%ld bytes are written to %s.\n",written_size,FILE2); close(fd); /* FILE3 write: posix_fadvise(POSIX_FADV_DONTNEED) lat */ fd = open(FILE3,O_CREAT|O_WRONLY,S_IRWXU); if(fd == -1){ fprintf(stderr,"Open %s failed with errno %d.\n",FILE3,errno); return 1; } written_size = my_write(fd,write_data,DATSIZE); printf("%ld bytes are written to %s.\n",written_size,FILE3); fadvise_res = posix_fadvise(fd,0,written_size,POSIX_FADV_DONTNEED); if(fadvise_res == -1){ fprintf(stderr,"Fail to set fadvise to %s,errno %d",FILE3,errno); } close(fd); return 0; }
実行結果ですが、下記の通りで 3パターンともpagecacheに残っています。
最後のfalast.dat(write()後、close()直前にposix_fadvise(POSIX_FADV_DONTNEED)を実行した場合もpagecacheに残る)
falast.datは残らないと思ったのですが想定外です。
$ ./fadvise_test 4096 bytes are written to ./normal.dat. 4096 bytes are written to ./fafirst.dat. 4096 bytes are written to ./falast.dat. $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000
既に存在するファイルをopen()してposix_fadvise(POSIX_FADV_DONTNEED)を実行するサンプルプログラムを書きます。
(fadvise_dontneed.c: ちなみにlinux-ftoolsのfadviseコマンドで同じ事が出来るので車輪の再発明だけど、自分でposix_fadvise()の動きを確認するために敢えて書いた)
/* posix_fadvise test */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> void printUsage(const char *progname){ fprintf(stderr,"Usage: %s <target filename>\n",progname); } int main(int argc,char *argv[]){ int fd; int fadvise_res; if(argc != 2){ printUsage(argv[0]); return 1; } fd = open(argv[1],O_RDONLY); if(fd == -1){ fprintf(stderr,"Error opening file %s,errno %d\n",argv[1],errno); return 1; } fadvise_res = posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED); if(fadvise_res == -1){ fprintf(stderr,"Fail to set fadvise to %s,errno %d",argv[1],errno); } close(fd); return 0; }
まず最初に書いたfadvise_testを使って、3つのファイルを書き込み、pagecacheに残します。
その上で1つに対して上記のfadvise_dontneed.cでposix_fadvise(fd,0,4096,POSIX_FADV_DONTNEED)を実行します。
一見上手く行く様に見えるのですが、1回ではpagecacheから追い出されないケースがある様です。単に時間がかかるだけでしょうか?
もう少し追ってみたいと思います。
$ ./fadvise_test // 最初のサンプルプログラムでファイルを3つ書く 4096 bytes are written to ./normal.dat. 4096 bytes are written to ./fafirst.dat. 4096 bytes are written to ./falast.dat. $ fincore --only-cached *.dat // 全てのファイルがキャッシュされている事を確認 filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ ./fadvise_dontneed normal.dat // normal.datをRDONLYでopen()して posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)を実行 $ fincore --only-cached *.dat // 再度キャッシュ状態を確認,normal.datがキャッシュから消えている filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 $ ./fadvise_dontneed fafirst.dat // 今度はfafirst.datにposix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)を実行 $ fincore --only-cached *.dat // 再度キャッシュ状態を確認,fafirst.datがキャッシュから消えている filename size total pages cached pages cached size cached percentage 0 falast.dat 4096 1 1 4096 100.000000
posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)1回実行では上手く解放されなかったケース。
$ ./fadvise_test 4096 bytes are written to ./normal.dat. 4096 bytes are written to ./fafirst.dat. 4096 bytes are written to ./falast.dat. $ ./fadvise_dontneed normal.dat // normal.datの解放を試みる $ fincore --only-cached *.dat // ここから数回fincoreで確認するがnormal.datがキャッシュされたまま filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ fincore --only-cached *.dat filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 0 normal.dat 4096 1 1 4096 100.000000 $ ./fadvise_dontneed normal.dat // もう一度normal.datの解放にトライ $ fincore --only-cached *.dat // 今度は解放された filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000 0 falast.dat 4096 1 1 4096 100.000000 $ ./fadvise_dontneed falast.dat // 続いてfalast.datを解放 $ fincore --only-cached *.dat // 解放されたことを確認 filename size total pages cached pages cached size cached percentage 0 fafirst.dat 4096 1 1 4096 100.000000
fadvise_dontneed.cを少し改造します。
パターン1: fstat()で対象ファイルのファイルサイズを調べて、posix_fadvise(fd,0,<ファイルサイズ>,POSIX_FADV_DONTNEED)を実行
パターン2: syncfs()で対象ファイルのみsyncした後、、posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)を実行
試した限りではパターン1ではやはりすぐにpagecacheが解放されないケースがある。
パターン2は数回試行したレベルだとかならず解放されています。
パターン2のソース。
/* posix_fadvise test */ #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> void printUsage(const char *progname){ fprintf(stderr,"Usage: %s <target filename>\n",progname); } int main(int argc,char *argv[]){ int fd; int fadvise_res; if(argc != 2){ printUsage(argv[0]); return 1; } fd = open(argv[1],O_RDONLY); if(fd == -1){ fprintf(stderr,"Error opening file %s,errno %d\n",argv[1],errno); return 1; } syncfs(fd); fadvise_res = posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED); if(fadvise_res == -1){ fprintf(stderr,"Fail to set fadvise to %s,errno %d",argv[1],errno); } close(fd); return 0;