Swap 与 Swappiness

Swap 与 Swappiness

原创 聂子腾 网易游戏运维平台 2019-07-20

 

聂子腾

网易游戏私有云平台萌新运维工程师。

开机!~   各种服务启动!~   各种进程骑上了心爱的系统小摩托,并希望它永远不会堵车。随着系统的运转,越来越多的程序进行使用内存,Linux 使用内存的原则是尽量使用,尽力满足,使得最大限度的使『热』资源存活在内存中,系统就可能面临内存告急的状况,所以内存的管理中有着至关重要的一个环节------回收。

在不关心 LRU 细节,不考虑 NUMA 架构对内存分配的影响的前提下,让我们将目光聚焦在 Swap 的使用。

回收的时机

内存的呼神护卫 Kswapd
Kswapd 的任务是试图尽力满足进程对内存的需求,为了满足需求,它必须保证:

  • 当分配页的时候发现满足不了,触发直接回收

  • 空闲低于 watermark[WMARK_MIN] 触发直接回收

  • kswapd 监控

  • 空闲低于 watermark[WMARK_LOW] kswapd 开始回收试图使空闲回归到 watermark[WMARK_HIGH]

  • 空闲达到 watermark[WMARK_HIGH] kswapd 休眠

回收的任务

内存的使用分为两个部分:

  • FILE_BASE 文件页

  • ANON_BASE   匿名页

内存回收就是将目前不活跃 (inactive) 的页清理出去,对于对应在磁盘上有真实文件的文件页,如果不是脏页,可以直接释放,如果是脏页就先回写再释放。

但对于没有真实文件对应的匿名页,我们只能使用 Swap 的方式把他们暂时『交换』到交换分区中区。

所以我们也可以看到本质上清理文件页和匿名页本质上都会触发 IO,举个可能不太严谨的栗子:

清理页的大小为 M

  • 文件页:

    大概率 M 为非脏页,直接释放,再次使用再读取。           

    代价:1M

    部分或全部为脏页,回写释放,再次使用再读取。           

    代价:1M-2M            

  • 匿名页:

    进程不结束,换出内存,再次使用换入内存。

    代价:2M

    换出内存,进程结束,不再换入。

    代价:1M

我们还可以观察得出,文件全部为脏页和换出内存后进程结束都是小概率事件,更重要的是匿名页包括代码段和数据段,『活跃』的可能性远远大于文件页『活跃』的可能性。

所以在系统层面使用 Swap 的代价大于释放文件页。

对于 Swap 和 Swappiness 的传统看法

我们可能已有的疑惑

  • 大佬说 Swap 猛于虎也!用到了说明系统有问题!  内存不够用!?  上内存!

  • 看文章说 Swap 应该设置成内存的 2 倍,妥妥的!什么?你服务器 256G 内存?打扰了

  • Swap 就是虚拟内存,将磁盘空间虚拟成内存来用,整大点好使,什么?频繁交换性能好差?

  • 我观察了,Swap 一直也没用到,啥用没有直接干掉!一波高峰…   啥玩意?OOM 了?开不了新进程了?

  • Swap 还是能不用就不用,Swappiness 就设 0!怎么还是用了!?

  • 不怂!就是干!Swappiness 设 100!  怎么还是没用!?

以下采集自 Google 关于 Swap 和 Swappiness 的前两页结果内容,部分错误观点看完应自有结论。

  • 加 Swap 不如加内存

  • Swappiness 默认值是 60

  • Swappiness 参数的值越高,内核将会更积极地交换

  • 参数值设置为 “60” 表示当 RAM 达到 40% 容量时,内核将交换

  • Swappiness 可以被设置为 0 到 100 之间的一个值(包括 0 和 100)

  • 故障描述:物理内存还比较充足,vm.swappiness 已经设置为 0,但系统还是用了 Swap 分区.    —此例把这类情况视作故障

  • Linux 会使用硬盘的一部分做为 Swap 分区,用来进行进程调度--进程是正在运行的程序--把当前不用的进程调成‘等待(standby)‘,甚至‘睡眠(sleep)’,一旦要用,再调成‘活动(active)’,睡眠的进程就躺到 SWAP 分区睡大觉,把内存空出来让给‘活动’的进程

来自 某社区(https://www.v2ex.com/t/149721)的总结

  • Swap 的用量应该恒为 0, 吃到就要报警

  • Swap 不是好东西,内存抖动不如直接杀进程

  • 尽可能关掉 Swap,宁愿挂掉也不能响应慢

  • 每次 Swap 一动,负载马上飞天

  • Swappiness 已经可以起到绝大多数时候避免 Swap 的作用,但是你还是需要 Swap 来处理极端情况。一个折中办法是使用 zswap 减少 io

  • 应用内部可以还调用 madvise 通知内核避免 Swap。简单的取消 Swap 其实是丢弃了一道防护。

  • 如果内存足够完全可以在开启 Swap 的情况下避免 swapping。如果内存不够可以用 Cgroup 管住那个狂用内存又不太重要的应用。

  • 跑着那种几天才会跑一次数据的后台进程,换出去腾内存没什么不好

  • 设成 0 很危险,最起码设置成 1

内核探秘

我们在 torvalds/linux 的源码(https://github.com/torvalds/linux)中尝试寻找答案

文件 linux/mm/vmscan.c

/*
 * From 0 .. 100.  Higher means more swappy.
 * 默认值为60硬编码
 */
int vm_swappiness = 60;

/*
 * 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)
{
    //cgroup触发返回cgroup设置的swappiness,否则返回全局swappiness
    int swappiness = mem_cgroup_swappiness(memcg); 

    struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
    u64 fraction[2];
    u64 denominator = 0;    /* gcc */
    struct pglist_data *pgdat = lruvec_pgdat(lruvec);
    unsigned long anon_prio, file_prio;
    enum scan_balance scan_balance;
    unsigned long anon, file;
    unsigned long ap, fp;
    enum lru_list lru;

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

    /*
     * 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.
     */
    /* 不是全局回收 && swappiness==0 SCAN_FILE! */
    if (!global_reclaim(sc) && !swappiness) {
        scan_balance = SCAN_FILE;
        goto out;
    }

    /*
     * 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).
     */
    /* 优先级为0 && swappiness !=0     SCAN_EQUAL! */
    if (!sc->priority && swappiness) {
        scan_balance = SCAN_EQUAL;
        goto out;
    }

    /*
     * 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)) {
            /*
             * Force SCAN_ANON if there are enough inactive
             * anonymous pages on the LRU in eligible zones.
             * Otherwise, the small LRU gets thrashed.
             */
            /* 不活跃的匿名页可以满足本优先级需要回收的页量 SCAN_ANON! */
            if (!inactive_list_is_low(lruvec, false, memcg, sc, false) &&
                lruvec_lru_size(lruvec, LRU_INACTIVE_ANON, sc->reclaim_idx)
                    >> sc->priority) {
                scan_balance = SCAN_ANON;
                goto out;
            }
        }
    }

    /*
     * 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.
     */
    /* 非活文件页可以满足需要回收的页量 SCAN_FILE! */
    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;
    }
    /* 以上都没满足 SCAN_FRACT! */
    scan_balance = SCAN_FRACT;

    /*
     * With swappiness at 100, anonymous and file have the same priority.
     * This scanning priority is essentially the inverse of IO cost.
     * 当 swappiness 等于 100 时 anon_prio == file_prio == 100 扫描比例五五开
     */
    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);

    spin_lock_irq(&pgdat->lru_lock);
    if (unlikely(reclaim_stat->recent_scanned[0] > anon / 4)) {
        reclaim_stat->recent_scanned[0] /= 2;
        reclaim_stat->recent_rotated[0] /= 2;
    }

    if (unlikely(reclaim_stat->recent_scanned[1] > file / 4)) {
        reclaim_stat->recent_scanned[1] /= 2;
        reclaim_stat->recent_rotated[1] /= 2;
    }

    /*
     * 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;
    spin_unlock_irq(&pgdat->lru_lock);

    fraction[0] = ap;
    fraction[1] = fp;
    denominator = ap + fp + 1;

 

四种扫描标记:

  • SCAN_FILE (只扫描文件页)

  • 没有 swap 可用

  • 不是 global_reclaim & swappiness 为 0

  • 非活文件页表满足回收页量

  • SCAN_ANON (只扫描匿名页)

    • 是 global_reclaim &   空闲页量 + 文件页量 仍不能满足 高水线 &非活列表够长  &   不活跃的匿名页可以满足本优先级需要回收的页量

  • SCAN_EQUAL(紧急!尽可能扫描所有文件页和匿名页)

    • 优先级为 0 & swappiness 不为 0

  • SCAN_FRACT (根据 swappiness 平衡)

    • 以上都没满足

global_reclaim 与 优先级:

优先级默认为 12,每次回收不足 -1,最紧急状态其值为 0

其中 global_reclaim 为 True 的概率很大!~   请看其定义

/* 没有开内存控制编译参数恒为真,开了参数看本次回收是否是cgroup触发 */
#ifdef CONFIG_MEMCG
static bool global_reclaim(struct scan_control *sc)
{
    return !sc->target_mem_cgroup;
}
#else
static bool global_reclaim(struct scan_control *sc)
{
    return true;
}

继续

out:
    *lru_pages = 0;
    for_each_evictable_lru(lru) {
        int file = is_file_lru(lru);
        unsigned long size;
        unsigned long scan;

        size = lruvec_lru_size(lruvec, lru, sc->reclaim_idx);
        scan = size >> sc->priority;
        /*
         * If the cgroup's already been deleted, make sure to
         * scrape out the remaining cache.
         */
        if (!scan && !mem_cgroup_online(memcg))
            scan = min(size, SWAP_CLUSTER_MAX);

        switch (scan_balance) {
        case SCAN_EQUAL:
            /* Scan lists relative to size */
            /* 不对scan做任何处理 */
            break;
        case SCAN_FRACT:
            /*
             * Scan types proportional to swappiness and
             * their relative recent reclaim efficiency.
             * Make sure we don't miss the last page
             * because of a round-off error.
             * 按之前计算的比例分配 文件页 和 匿名页
             */
            scan = DIV64_U64_ROUND_UP(scan * fraction[file],
                          denominator);
            break;
        case SCAN_FILE:
        case SCAN_ANON:
            /* Scan one type exclusively */
            /* 
             * 写的有点晦涩
             * 先看file,file = is_file_lru(lru) 本次for进来的链是不是文件页链
             * 假设 scan_balance 是 SCAN_FILE is_file_lru(lru)也是 true 
             * 则不会执行 size scan 清0,正常进入扫描
             * 假设 SCAN_ANON进来 (scan_balance == SCAN_FILE) 为 false
             * 与 is_file_lru(lru)是文件链表为 true,不等,进行清0
             * 总结起来就是 SCAN_FILE 只扫描文件页,SCAN_ANON 只扫描匿名页
             * 
            */
            if ((scan_balance == SCAN_FILE) != file) {
                size = 0;
                scan = 0;
            }
            break;
        default:
            /* Look ma, no brain */
            BUG();
        }

        *lru_pages += size;
        nr[lru] = scan;
    }
}

到此分析完了完整的内存计算扫描链大小的方式,以及 Swappiness 的使用方式,现在已经基本可以解答之前的怀疑,鉴别错误观点,以及印证来自社区的总结。

我们可以看出内核对于 Swap 的态度已经是避免使用了的。

在文件页的释放能够满足回收需求的时候不关心任何设置不会去使用 Swap,即便在平衡标记下最大限度的使用 Swap 页只是和文件页的比例 1:1,同时在文件页已经不能满足而匿名页可以满足需求的情况下也不会关心任何设置去使用 Swap,另外还可以确定 Swappiness 设置成 0 的风险在于断绝了 SCAN_EQUAL 紧急标记位的可能性。完全没有 Swap 更是只有 SCAN_FILE 一种可能性。

思考

Swap 的使用等于给系统开了一扇窗,使得系统在内存回收的过程中多了更多的选择和可能性,在一些情况下可以使用交换匿名页来缓解压力。

关于性能。

传统观念 Swap == Evil ,一些人认为使用到的 Swap 是性能问题,需要想办法让系统不使用 Swap,但其实通过上述分析可以看出系统在内存的使用上已经尽力为我们进行了比较高效率的权衡。

另外,使用 Swap 不是问题,频繁的换入换出才有可能引发性能问题,就如同频繁的释放读取文件页也一样会引发性能问题,所以当问题发生时很大可能是程序需要优化而不是系统需要优化。

通常降低 Swap 的使用能够改善系统状态的情况,只在满足恰好系统对非活匿名页存在误杀,导致刚换出的页又被换入,且降低匿名页的回收同时会增加对文件页的回收而其代价又低于对匿名页的回收时成立。

关于使用原则。

对于 Swap 的空间使用,取决于两点。

一是想要得到的结果:

①在集群下,不希望出现任何抖动/增加延迟/出现慢响应,系统有横向伸缩的能力,可以完全严格不使用 Swap。死掉了再排查原因。避免出现将死不死最为致命的场景,死掉还好说有集群其他节点提供服务,业务基本无感知,而阻塞缺会影响调用链的部分流程挂起,引发其他流程占用资源,甚至集群雪崩。

②是不希望系统发生 OOM,利用任何可利用的资源使系统能够维持运转,能够撑过一些小高峰,可以有选择的利用 Swap。

二是系统的目的:

①要求及时响应的系统趋向尽量避免使用 Swap。

②应用层原意使用内存加速尽量避免使用 Swap,例如:Mysql 内存索引、Redis,触发磁盘 IO 破坏原意。

③系统用来做大量计算,不需要及时响应,可适当利用 Swap 增加吞吐量。Swap 的大小可直接影响系统允许 overcommit 的大小,从而直接决定可以起更多的进程,可以申请更多的匿名页,overcommit 的大小也可以为增加内存提供参考。

关于优化。

加内存固然是解决问题的终极法门,但在不增加成本的前提下,我们也可以有许多种尝试。

①如果有多块磁盘,可以建立多个 Swap 分区挂载到系统,以提高换入换出的效率。

②ZSWAP 的做法是尝试把内核交换出去的页面压缩存放到一个内存池中。ZSWAP 空间也是有限的,ZSWAP 会智能地把其中一些它认为近期不会使用的页面解压缩, 写回到真正的磁盘外设中.   因此, 大部分情况下, 它能避免磁盘写操作。

③使用 Cgroup,为什么 Cgroup 是有效的措施,主要是一个系统的不同进程或进程组目的不尽相同,利用系统全局的设定并不一定合适全部进程,此外在上面代码中可以看到,扫描页的计算对 Cgroup 触发的回收做了多次例外判断能够更倾向于扫描文件页。

④如果你选择关闭 Swap,上述已经可以看到 Swappiness=0 也并不代表关闭 swap,请直接干掉 swap 分区。

番外

watermark

  • watermark[WMARK_MIN]:在快速分配失败后的慢速分配中会使用此阀值进行分配,如果慢速分配过程中使用此值还是无法进行分配,那就会执行直接内存回收和快速内存回收

  • watermark[WMARK_LOW]:也叫低阀值,是快速分配的默认阀值,在分配内存过程中,如果 zone 的空闲页框数量低于此阀值,系统会对 zone 执行快速内存回收

  • watermark[WMARK_HIGH]:也叫高阀值,是 zone 对于空闲页框数量比较满意的一个值,当 zone 的空闲页框数量高于这个值时,表示 zone 的空闲页框较多。所以对 zone 进行内存回收时,目标也是希望将 zone 的空闲页框数量提高到此值以上,系统会使用此阀值用于 oomkill 进行内存回收。

      这三个阀值的关系是:min 阀值 < low 阀值 < high 阀值。在系统初始化期间,根据系统中整个内存的数量与每个 zone 管理的页框数量,计算出每个 zone 的 min 阀值,然后 low 阀值 = min 阀值 + (min 阀值 / 4),high 阀值 = min 阀值 + (min 阀值 / 2)。这样就得出了这三个阀值的数值,我们可以通过 /proc/zoneinfo 中查看这三个阀值的数值。

其中 MIN 的值是依据 min_free_kbytes 计算得出,所以 min_free_kbytes 的值直接影响系统的稳定性, min_free_kbytes 设的越大,watermark 的线越高,同时三个线之间的 buffer 量也相应会增加。

这意味着会较早的启动 kswapd 进行回收,且会回收上来较多的内存(直至 watermark[high] 才会停止),这会使得系统预留过多的空闲内存,从而在一定程度上降低了应用程序可使用的内存量。

极端情况下设置 min_free_kbytes 接近内存大小时,留给应用程序的内存就会太少而可能会频繁地导致 OOM 的发生。min_free_kbytes 设的过小,则会导致系统预留内存过小。

kswapd 回收的过程中也会有少量的内存分配行为(会设上 PF_MEMALLOC )标志,这个标志会允许 kswapd 使用预留内存;另外一种情况是被 OOM 选中杀死的进程在退出过程中,如果需要申请内存也可以使用预留部分。

这两种情况下让他们使用预留内存可以避免系统进入 deadlock 状态。

鉴于加四分之一计算 LOW 再加四分之一计算 HIGH 这种方式过于死板现在又引入了 watermark_scale_factor

zone->watermark[WMARK_MIN] = tmp;

/*
    * Set the kswapd watermarks distance according to the
    * scale factor in proportion to available memory, but
    * ensure a minimum size on small systems.
    * 水位线等级系数,这个系数控制了kswapd进程的激进程度,控制了kswapd进程从唤醒到休眠,需要给系统(或NUMA节点)释放出多少内存。
    * 该值的单位是万分几。默认值是10,意思是0.1%的系统内存(NUMA节点内存)。该值的最大值是1000,意思是10%的系统内存(或NUMA节点内存)。
*/
tmp = max_t(u64, tmp >> 2,
              mult_frac(zone->managed_pages,
                    watermark_scale_factor, 10000));

zone->watermark[WMARK_LOW]  = min_wmark_pages(zone) + tmp;
zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;

CommitLimit

Memory Overcommit 的意思是操作系统承诺给进程的内存大小超过了实际可用的内存。

CommitLimit 就是 overcommit 的阈值,申请的内存总数超过 CommitLimit 的话就算是 overcommit。

在 vm.overcommit_memory   为默认值『试探式的』进行 overcommit 的分配方式下(http://linuxperf.com/?p=102),可以看出 Swap 的大小直接决定了可以超分的量。利用这个特性可以灵活增加系统吞吐量。

CommitLimit = ([total RAM pages] - [total huge TLB pages]) *
                             overcommit_ratio / 100 + [total swap pages]
              For example, on a system with 1G of physical RAM and 7G
              of swap with a `vm.overcommit_ratio` of 30 it would
              yield a CommitLimit of 7.3G.

Ref

tolimit Blog 内存源码分析
https://www.cnblogs.com/tolimit/tag/%E5%86%85%E5%AD%98/

Arnold Lu 内存管理
https://www.cnblogs.com/arnoldlu/category/1132616.html

现在的 Linux 内核和 Linux 2.6 的内核有多大区别?
https://www.zhihu.com/question/35484429

文中提到的V站关于Swap的讨论
https://www.v2ex.com/t/149721

往期精彩

 

IPv6 支持度报告和 IPv6 环境下 DNS 相关测试

 

人工智障入门

 

MongoDB Change streams 与数据订阅同步

 

网易游戏海外AWS动态伸缩实践

 

深入理解实时计算中的 Watermark

你可能感兴趣的:(Swap 与 Swappiness)