ローリングコンバットピッチなう!

AIとか仮想化とかペーパークラフトとか

【Linux】posix_fadvise()で特定のファイルのpagecacheを解放する+linux-ftoolsでdrop_cachesの動作を確認する

Linuxサーバーを長時間運用するとどんどんpagecacheが溜まってきてfreeメモリが涸渇し、Kernelが不要なpagecacheを纏めて解放する際、その上での動作するサービスのレイテンシーが低下するという、あちこちで良く聞く話。
drop_cachesでpagecache丸ごと解放するのは知っていたし、実際に使った事もあるのですが、pagecache全部解放すると、使用頻度の高いcacheまで解放するので若干解放直後は性能劣化するんですよね。
で、今更なんですが下記のエントリでposix_fadvise()で特定のファイルのみpagecacheから解放出来る事を知って試してました。

qiita.com

[linux]linux-ftoolsをUbuntu18.04にインストール


で、色々ググッていたらlinux-ftoolsというものを使うと、ファイル単位にキャッシュ状況が可視化出来ると判ったので、これをUbuntuに入れてみます。
github.com

  1. githubからリポジトリをクローン

    $ git clone https://github.com/david415/linux-ftools.git

  2. クローンしたディレクトリに入って、./configureを実行

    $ cd linux-ftools
    $ ./configure

  3. 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で書き込みます。

  1. normal.dat - posix_fadvise()を実行しない
  2. fafirst.dat - open()直後にposix_fadvise(fd,0,0,POSIX_FADV_DONTNEED)
    を実行
  3. 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;