Linux
kernel
swappiness

Linuxのswappinessは本当にスワップしにくさを設定できるのか

いきなり結論

Q. swappinessって本当に「スワップしにくさ」を設定できるの?
A. はい、本当です。
 ただそれが本当にあなたの望む動作なのかについては注意が必要です。

swappinessは、0から100までの値を設定することができ、それはメモリを回収(scan)するときにどこから回収するかに効いて、200分率で下記のように使われる

  • (swappiness)の割合でANONYMOUSEをscanする
  • (200-swappiness)の割合でfile_backedをscanする

Red Hatのマニュアルにある通り、小さくするにしても10あたりが無難。1だとかなり攻めている。下記で見ていくが、よほどわかっている場合を除いて、0にするのはやめておいたほうが良い。

はじめに

Linuxには「スワップのしにくさ」を設定できるswappinessというパラメータがある。

[rarul@tina ~]$ sysctl vm.swappiness
vm.swappiness = 60

「スワップなんてできるだけ発生させたくない」という思いからか、swappiness0とか1とか10とかに設定するという例がググるとたくさん見つかる。(例えばスワップされて困っちゃうのでswappinessを設定する) が、本当にそれでいいの?そもそも何を設定しているの?という疑問が出てきたので、詳しく調べてみることにした。

ちなみにLinux-4.12くらいを見ています。また今回はCONFIG_MEMCGに関連するところはあえて無視しています。

Documentation

kernel/Documentation/sysctl/vm.txtにドキュメントがある。

swappiness

This control is used to define how aggressive the kernel will swap
memory pages.  Higher values will increase agressiveness, lower values
decrease the amount of swap.  A value of 0 instructs the kernel not to
initiate swap until the amount of free and file-backed pages is less
than the high water mark in a zone.

The default value is 60.

0に設定すると、「freeメモリ」と「file-backedメモリ」が「high water」を下回るまでスワップを使わなくなる、と言っている。

デフォルト値は60だけど、有効な値の範囲はいくつなのかドキュメントだけではわからない。後に見ていくけど、有効な値は0から100となる。

ソースコードを見る

swappinessの設定箇所

sysctlとして見えるswappinessの定義は、kernel/kernel/sysctl.cのvm_table[]より、

sysctlc
    {
        .procname   = "swappiness",
        .data       = &vm_swappiness,
        .maxlen     = sizeof(vm_swappiness),
        .mode       = 0644,
        .proc_handler   = proc_dointvec_minmax,
        .extra1     = &zero,
        .extra2     = &one_hundred,
    },

proc_dointvec_minmax()でsetter/getterがなされ、グローバル変数のvm_swappinessに入ることがわかる。なんとなく.extra1がminで.extra2がmaxを指定している気がするけどちゃんと確認する。kernel/kernel/sysctl.cより、

sysctl.c
/**
 * proc_dointvec_minmax - read a vector of integers with min/max values
 * @table: the sysctl table
 * @write: %TRUE if this is a write to the sysctl file
 * @buffer: the user buffer
 * @lenp: the size of the user buffer
 * @ppos: file position
 *
 * Reads/writes up to table->maxlen/sizeof(unsigned int) integer
 * values from/to the user buffer, treated as an ASCII string.
 *
 * This routine will ensure the values are within the range specified by
 * table->extra1 (min) and table->extra2 (max).
 *
 * Returns 0 on success.
 */
int proc_dointvec_minmax(struct ctl_table *table, int write,
          void __user *buffer, size_t *lenp, loff_t *ppos)
{
    struct do_proc_dointvec_minmax_conv_param param = {
        .min = (int *) table->extra1,
        .max = (int *) table->extra2,
    };
    return do_proc_dointvec(table, write, buffer, lenp, ppos,
                do_proc_dointvec_minmax_conv, &param);
}

というわけで、swappinessには0から100までの値を設定できることがわかる。do_proc_dointvec()以下は、ユーザランドからの要求を適切に処理する部分なので今回は省略する。

vm_swappinessの使われる箇所

値の入れられたグローバル変数vm_swappinessmem_cgroup_swappiness()がgetterとして使う。kernel/include/linux/swap.hより、

swap.h
#ifdef CONFIG_MEMCG
static inline int mem_cgroup_swappiness(struct mem_cgroup *memcg)
{
    /* Cgroup2 doesn't have per-cgroup swappiness */
    if (cgroup_subsys_on_dfl(memory_cgrp_subsys))
        return vm_swappiness;

    /* root ? */
    if (mem_cgroup_disabled() || !memcg->css.parent)
        return vm_swappiness;

    return memcg->swappiness;
}

#else
static inline int mem_cgroup_swappiness(struct mem_cgroup *mem)
{
    return vm_swappiness;
}
#endif

なので、mem_cgroup_swappiness()を使っている箇所を追う。っといっても、使っているのは実は1箇所で、kernel/mm/vmscan.cより、

vmscan.c
/*
 * Determine how aggressively the anon and file LRU lists should be
 * scanned.  The relative value of each set of LRU lists is determined
 * by looking at the fraction of the pages scanned we did rotate back
 * onto the active list instead of evict.
 *
 * nr[0] = anon inactive pages to scan; nr[1] = anon active pages to scan
 * nr[2] = file inactive pages to scan; nr[3] = file active pages to scan
 */
static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
               struct scan_control *sc, unsigned long *nr,
               unsigned long *lru_pages)
{
    int swappiness = mem_cgroup_swappiness(memcg);

うん、mmのど真ん中のメモリ回収あたりに来て、読むのがつらく思いやられる気がしてきました。get_scan_count()は下記2つの系から来る(CONFIG_MEMCGCONFIG_NUMAは除く)

  • try_to_free_pages()->do_try_to_free_pages()->shrink_zones()->shrink_node()->shrink_node_memcg()->get_scan_count()
  • kswapd()->balance_pgdat()->kswapd_shrink_node()->shrink_node()->shrink_node_memcg()->get_scan_count()

前者が主に「メモリほしいときになかったら回収」場合、後者が「kswapdが定期的にチェックししきい値を下回ったら回収」という感じか。

shrink_node_memcg()でのswappinessの働き

shrink_node_memcg()の中でswappinessがどう使われるのかを順を追っていく。

vmscan.c
    /*
     * Global reclaim will swap to prevent OOM even with no
     * swappiness, but memcg users want to use this knob to
     * disable swapping for individual groups completely when
     * using the memory controller's swap limit feature would be
     * too expensive.
     */
    if (!global_reclaim(sc) && !swappiness) {
        scan_balance = SCAN_FILE;
        goto out;
    }

CONFIG_MEMCGが無効の場合は、global_reclaim()は常にtrueとなり、上記のif文に入らない。

vmscan.c
    /*
     * Do not apply any pressure balancing cleverness when the
     * system is close to OOM, scan both anon and file equally
     * (unless the swappiness setting disagrees with swapping).
     */
    if (!sc->priority && swappiness) {
        scan_balance = SCAN_EQUAL;
        goto out;
    }

sc->priorityについては、同じファイルの先頭に定義があり、

vmscan.c
struct scan_control {
(-----snip-----)
    /* Scan (total_size >> priority) pages at once */
    int priority;

どのくらい一度にscan(メモリ回収)するかを表し、小さいほどいっぱいscanすることになる。priorityは、do_try_to_free_pages()でなかなかメモリ回収が進まないときにデクリメントされる。priorityがゼロかつswappinessがゼロじゃないときにSCAN_EQUALに倒れる。

ここまで来て、ようやくswappinessが意味のある形で参照され、

vmscan.c
    /*
     * With swappiness at 100, anonymous and file have the same priority.
     * This scanning priority is essentially the inverse of IO cost.
     */
    anon_prio = swappiness;
    file_prio = 200 - anon_prio;

    /*
     * OK, so we have swap space and a fair amount of page cache
     * pages.  We use the recently rotated / recently scanned
     * ratios to determine how valuable each cache is.
     *
     * Because workloads change over time (and to avoid overflow)
     * we keep these statistics as a floating average, which ends
     * up weighing recent references more than old ones.
     *
     * anon in [0], file in [1]
     */

    anon  = lruvec_lru_size(lruvec, LRU_ACTIVE_ANON, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_ANON, MAX_NR_ZONES);
    file  = lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
        lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, MAX_NR_ZONES);

swappinessは、200分率として解釈され、swappinessで指定した値がANONYMOUSEメモリのscan率、(200-swappiness)file_backedメモリのscan率、として使われる。ただそう単純ではなくて、サイズによる重み付けであったり、

vmscan.c
    /*
     * The amount of pressure on anon vs file pages is inversely
     * proportional to the fraction of recently scanned pages on
     * each list that were recently referenced and in active use.
     */
    ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1);
    ap /= reclaim_stat->recent_rotated[0] + 1;

    fp = file_prio * (reclaim_stat->recent_scanned[1] + 1);
    fp /= reclaim_stat->recent_rotated[1] + 1;

recent scannedによる重み付けであったりがなされる。この値は最終的には、

vmscan.c
    fraction[0] = ap;
    fraction[1] = fp;
    denominator = ap + fp + 1;
vmscan.c
        case SCAN_FRACT:
            /*
             * Scan types proportional to swappiness and
             * their relative recent reclaim efficiency.
             */
            scan = div64_u64(scan * fraction[file],
                     denominator);
            break;

SCAN_FRACTなscanが選ばれたときのscanの値の計算に用いられ、

vmscan.c
        nr[lru] = scan;

どのくらいscanすべきかのページの数として返され、上位のメモリ回収関数で使われる。・・・というあたりで、SCAN_FRACTって結局何やねんという疑問が残る。

shrink_node_memcg()でのscan_balance

shrink_node_memcg()では、nr[]の値(関数の返り値のように使われる)を決めるのに、大きく4種類のポリシーがある。

scan.c
enum scan_balance {
    SCAN_EQUAL,
    SCAN_FRACT,
    SCAN_ANON,
    SCAN_FILE,
};

ほとんどコメントがないんだけど、実際にこれを使って処理が別れる箇所より、

scan.c
        switch (scan_balance) {
        case SCAN_EQUAL:
            /* Scan lists relative to size */
            break;
        case SCAN_FRACT:
            /*
             * Scan types proportional to swappiness and
             * their relative recent reclaim efficiency.
             */
            scan = div64_u64(scan * fraction[file],
                     denominator);
            break;
        case SCAN_FILE:
        case SCAN_ANON:
            /* Scan one type exclusively */
            if ((scan_balance == SCAN_FILE) != file) {
                size = 0;
                scan = 0;
            }
            break;
  • SCAN_EQUAL: サイズ(ページ数?)に比例したscanをする
  • SCAN_FRACT: swappinessによる重み付けが入ったscanをする
  • SCAN_FILE: file_backedなものをscanする(ANONYMOUSEはscanしない)
  • SCAN_ANON: ANONYMOUSEなものをscanする(file_backedなものはscanしない)

となる。で、それぞれがどう選ばれるかを改めてshrink_node_memcg()の先頭から追っておく。

scan.c
    /* If we have no swap space, do not bother scanning anon pages. */
    if (!sc->may_swap || mem_cgroup_get_nr_swap_pages(memcg) <= 0) {
        scan_balance = SCAN_FILE;
        goto out;
    }

スワップがなかったらSCAN_FILEを選ぶ。

scan.c
    /*
     * Global reclaim will swap to prevent OOM even with no
     * swappiness, but memcg users want to use this knob to
     * disable swapping for individual groups completely when
     * using the memory controller's swap limit feature would be
     * too expensive.
     */
    if (!global_reclaim(sc) && !swappiness) {
        scan_balance = SCAN_FILE;
        goto out;
    }

個別のMEMCGを考えるときは、スワップさせないようにするため、SCAN_FILEを選ぶ。(CONFIG_MEMCGが有効な場合は、swappinessもMEMCGごとに個別化される)

scan.c
    /*
     * Do not apply any pressure balancing cleverness when the
     * system is close to OOM, scan both anon and file equally
     * (unless the swappiness setting disagrees with swapping).
     */
    if (!sc->priority && swappiness) {
        scan_balance = SCAN_EQUAL;
        goto out;
    }

なかなかメモリが回収できないとき(繰り返しscanされる場合)かつswappinessがゼロじゃないときは、OOMに陥ってしまう一歩手前で厳しいので、どっちを優先とかは考えずに、SCAN_EQUALを選ぶ。

scan.c
    /*
     * Prevent the reclaimer from falling into the cache trap: as
     * cache pages start out inactive, every cache fault will tip
     * the scan balance towards the file LRU.  And as the file LRU
     * shrinks, so does the window for rotation from references.
     * This means we have a runaway feedback loop where a tiny
     * thrashing file LRU becomes infinitely more attractive than
     * anon pages.  Try to detect this based on file LRU size.
     */
    if (global_reclaim(sc)) {
        unsigned long pgdatfile;
        unsigned long pgdatfree;
        int z;
        unsigned long total_high_wmark = 0;

        pgdatfree = sum_zone_node_page_state(pgdat->node_id, NR_FREE_PAGES);
        pgdatfile = node_page_state(pgdat, NR_ACTIVE_FILE) +
               node_page_state(pgdat, NR_INACTIVE_FILE);

        for (z = 0; z < MAX_NR_ZONES; z++) {
            struct zone *zone = &pgdat->node_zones[z];
            if (!managed_zone(zone))
                continue;

            total_high_wmark += high_wmark_pages(zone);
        }

        if (unlikely(pgdatfile + pgdatfree <= total_high_wmark)) {
            scan_balance = SCAN_ANON;
            goto out;
        }
    }

スラッシングを防ぐため、file_backedなメモリのページ数がしきい値以下なら、SCAN_ANONにする。

scan.c
    /*
     * If there is enough inactive page cache, i.e. if the size of the
     * inactive list is greater than that of the active list *and* the
     * inactive list actually has some pages to scan on this priority, we
     * do not reclaim anything from the anonymous working set right now.
     * Without the second condition we could end up never scanning an
     * lruvec even if it has plenty of old anonymous pages unless the
     * system is under heavy pressure.
     */
    if (!inactive_list_is_low(lruvec, true, memcg, sc, false) &&
        lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, sc->reclaim_idx) >> sc->priority) {
        scan_balance = SCAN_FILE;
        goto out;
    }

inactiveなメモリが十分にある(ちょっと説明怪しいかもしれない)なら、SCAN_FILEにする。

scan.c
    scan_balance = SCAN_FRACT;

上記のどれにも当てはまらないなら、SCAN_FRACTにする。

と、長くなってしまったが、結局のところ、inactiveなメモリも減ってきていよいよactiveなメモリにも手をかけ始めようとするときに、SCAN_FRACTが選ばれ、swappinessにより重み付けされたscanをすることになる。

ちなみに、

vmscan.c
        default:
            /* Look ma, no brain */
            BUG();
        }

「Look ma, no brain」は、「ママ、見て、あの人、変だよ」みたいな感じか?普通のママなら「しー!見ちゃいけません!」てとこで、panic(BUG())を見なかったことにする・・・と。

swappinessをゼロにしたらどうなるの?

先のコードリーディングより、swappinessがゼロだと、SCAN_EQUALが選ばれなくなり、またSCAN_FRACTが選ばれたときもANONYMOUSEのnr[]はゼロになるので、file_backedばかりscanされることになる。唯一の例外が、スラッシングを防ぐ最低限のfile_backedのみになってしまった場合のSCAN_ANONが選ばれる場合で、これが先に提示したDocumentationに書かれている箇所の意味となる。

スワップを減らすことはいいことなのか

少し知っている人ほど「スワップが発生するのは何が何でも防がなければいけないもの」と固定的に考えているフシがある。スワップとはANONYMOUSEなメモリをディスクに書き出すことでメモリの空きを確保する処理なわけで、スワップさせないようにした結果犠牲になるものがあり、それがfile_backedなメモリである。

Linuxはメモリに置かないと実行できないOSのモデルを取っているので、readしているだけのファイルでも、writeしてdirtyなファイルも、実行すべきプログラムであっても、すべてメモリに一旦載せないといけない。file_backedなメモリを犠牲にするとは、これらをメモリから追い出す側に倒しているということになる。これらが再び必要になったらまたディスクから読み直さないといけない。ディスクから読みなさないといけないという点で、スワップさせているのとコストが変わらない。厳密には、スワップはディスクに書かないとメモリを空けられないのに対し、file_backedはメモリを破棄するだけで良い(dirtyな場合を除く)ので、その分の差はある。

じゃどっちがいいのかというと、ユースケースによるとしか言えない。「いっぱいメモリ使うんだけどほとんどSLEEPしていて1時間に1回だけ起きる」ようなプログラムが典型的で、そんなプログラムはどうせ1時間に1回しかディスクアクセスしなくて良いんだからANONYMOUSEであろうがディスクに書き出したほうが良い、つまりスワップさせたほうがよいとなり、他の頻繁に起きるプログラムが使うファイルをメモリに残しておいたほうがよいとなる。

近年は、SSDでディスクが近くなったので積極的にスワップさせても以前よりコストが小さいという話もある。それを積極的に享受しようと、スワップでやっている処理の改善がLinux-4.11で導入されていたりする。

他にはzramなんていうのもあり、RAM上にメモリを圧縮した状態でスワップしてしまおうというなかなかアグレッシブな考えのものもある。RAM帯域が厳しい割にCPUが遊んでるような場合には使い勝手があるのかもしれない。

あとがき

ソースコードを読んだわけだけど、ヒューリスティックな処理ばかりで、なんか理解できたような理解できなかったようなそんな感じだった。Active, Inactive, Anon, file_backedな話とか、kernel内でkmalloc()したものは別扱いされる(==slab)とか、そのへんの背景も理解していないとホント何やってるのかわからない箇所である気はする。

/proc/meminfoの解説をする的な話が必要になってくるわけだけど、これを語り始めるとmm沼に延々と沈んでいくわけで、この業界は闇が多いですね、はい。

参考サイト