周其仁:停止改革,我们将面临三大麻烦

抛开立场观点不谈,且看周小平写一句话能犯多少语病

罗马尼亚的声明:小事件隐藏着大趋势——黑暗中的风:坚持做对的事相信未来的结果

布林肯突访乌克兰,为何选择去吃麦当劳?

中国不再是美国第一大进口国,贸易战殃及纺织业? 美国进一步延长352项中国商品的关税豁免期

生成图片,分享到微信朋友圈

自由微信安卓APP发布,立即下载! | 提交文章网址
查看原文

PostgreSQL与内存,剪不断理还乱

xiongcc PostgreSQL学徒 2023-01-21

缘起

生产环境碰到了一起故障,主机夯死,发现是该时间段的内存持续高压,无法回收,通过/proc/meminfo和sar -B,发现了大量的都是匿名页 (anonymous pages),同时%vmeff接近为0,引用官方的话就是

The %vmeff column shows the number of pages scanned (pgscand/s) in relation to the ones being reused from the main memory cache or the swap cache (pgsteal/s). It is a measurement of the efficiency of page reclaim. Healthy values are either near 100(every inactive page swapped out is being reused) or 0 (no pages have been scanned). The value should not drop below 30.

再加上环境配置了swappiness为0,不能回收匿名页,最终不断恶化,主机夯死。所以,有必要恶补一下内存知识了。

内存的三种分类

Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

  • ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。

  • ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。

  • ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。

内存回收的三种方法

我们知道,随着系统的不断运行,内存会逐渐被分配出去,剩余的空闲内存越来越少,不管是用户进程使用的,还是Kernel会使用到的。这个时候,新进来的进程没有内存可用了该如何处理呢?此时内核Kernel就要通过page reclaim机制回收一部分内存,Linux 在内存不足时获取内存的常见方法有:内存压缩、直接回收以及触发内存不足错误杀掉部分进程,也就是我们熟知的OOM killer。

内存压缩

说到压缩这个词,我们都可以想到是降低占用空间,使同样的空间可以存放更多的东西,比如PostgreSQL里面针对WAL日志提供了wal_compression参数,以及前阵子v14 beta版对于Heap表支持了lz4压缩算法,再到高端一点的ScaleFlux卡,内存压缩同样也是为了节省内存。关于CPU、内存和磁盘IO,是我们老生常谈的问题,毕竟三者的速度差距是好几个数量级,见下表:

所以那既然你CPU那么牛,那么快,与其让你干等着I/O慢吞吞的传输数据,不如花点时间把这些数据压缩一下,这样需要传输的数据量不就减小了么。在Linux的内存回收机制中,"dirty"的pages(包括anonymous page和可读写的page cache)需要经过I/O传输,swap out/writeback同步到外部存储介质(磁盘/Flash),之后这部分内存才可以被回收。当这个被换出到外部存储介质的page再次被使用时,将触发page fault,此时再通过I/O传输,换入内存中。如果把内存数据经过压缩后再传递到外部存储介质,将节省swap out和swap in时所需的I/O传输量。而如果把压缩后的pages直接放在内存里,发生page fault时再解压缩出来,那么从时间上,将只有CPU的压缩和解压缩的开销,而没有I/O传输的开销。当然代价就是,从空间上,需要占用一部分的内存资源。至少到今天为止,内存无论是多大,总是不够用的,因此,如果频繁做swap操作,不仅会影响本身存储的使用寿命,还会严重影响系统性能,所以也就存在了内存压缩这项技术。目前主流的内存压缩算法包括:ZSWAP、ZCACHE和ZRAM等,感兴趣的自行下来恶补知识。

内存回收

内核主要对进程使用的页进行回收,属于内核的大部分页框是不能够进行回收的,比如内核栈、内核代码段、内核数据段以及大部分内核使用的页,主要是两个方面:1、直接将一些页释放。2、将页回写保存到磁盘(也就是write back),然后再释放。对于第一种,最明显的就是进程代码段的页,这些页都是只读的,因为代码段是禁止修改的,对于这些页,直接释放掉就好,因为磁盘上对应的数据与页中的数据是一致的。那么对于进程需要回写的页,内核主要将这些页放到磁盘的两个地方,当进程使用的页中的数据是映射于具体文件的,那么只需要将此页中的数据回写到对应文件所在磁盘位置就可以了。而对于那些没有映射磁盘对应文件的页,内核则将它们存放到swap分区中,也就是匿名页这个东西。

  • 进程堆、栈、数据段使用的匿名页:存放到swap分区中

  • 进程代码段映射的可执行文件的文件页:直接释放

  • 打开文件进行读写使用的文件页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放

  • 进行文件映射mmap共享内存时使用的页:如果页中数据与文件数据不一致,则进行回写到磁盘对应文件中,如果一致,则直接释放

  • 进行匿名mmap共享内存时使用的页:存放到swap分区中

  • 进行shmem共享内存时使用的页:存放到swap分区中

针对此情况,Linux引进了LRU链表,实际上整个内存的回收,做的事情就是处理LRU链表。

系统中的每个区都会在内存中持有 active_list 和 inactive_list 两种链表,其中前者包含活跃的内存页,后者中存储的内存页都是回收的候选页面,除此之外,Linux 还会在将 lru_list 根据内存页的特性分成如下几种:

enum lru_list {
    LRU_INACTIVE_ANON = LRU_BASE,
    LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
    LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
    LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
    LRU_UNEVICTABLE,
    NR_LRU_LISTS
};

  • LRU_INACTIVE_ANON:称为非活动匿名页lru链表,此链表中保存的是此zone中所有最近没被访问过的并且可以存放到swap分区的页描述符,在此链表中的页描述符的PG_active标志为0。

  • LRU_ACTIVE_ANON:称为活动匿名页lru链表,此链表中保存的是此zone中所有最近被访问过的并且可以存放到swap分区的页描述符,此链表中的页描述符的PG_active标志为1。

  • LRU_INACTIVE_FILE:称为非活动文件页lru链表,此链表中保存的是此zone中所有最近没被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为0。

  • LRU_ACTIVE_FILE:称为活动文件页lru链表,此链表中保存的是此zone中所有最近被访问过的文件页的页描述符,此链表中的页描述符的PG_active标志为1。

  • LRU_UNEVICTABLE:此链表中保存的是此zone中所有禁止换出的页的描述符。

因此,我们可以对Linux中的内存分个类:

  1. file-backed pages(有文件背景的页面,比如代码段、比如read/write方法读写的文件、比如mmap读写的文件,它们有对应的硬盘文件,因此如果要交换,可以直接和硬盘对应的文件进行交换;比如读取一个文件,没有关闭,也没有修改,交换时,就可以将这个文件直接放回硬盘,代码处理其实就是删除这部分内容,只保留一个索引,让系统知道这个文件还处于打开状态,只是它的内容不在内存,还在硬盘上),此部分页面叫做page cache;

  2. anonymous pages(匿名页,如stack,heap,CoW后的数据段等;他们没有对应的硬盘文件,因此如果要交换,只能交换到swap分区),此部分页面,如果系统内存不充分,可以被swap到swapfile或者硬盘的swap分区。需要注意的是,Swap File是文件系统中的特殊文件,它与文件系统中的其他文件也没有太多的区别。

简单理解就是,ANON 匿名内存页存储了与文件无关的进程堆栈等内容,而包含 FILE 的表示与文件相关的内存,也就是程序文件或者数据对应的内存,而最后的 LRU_UNEVICTABLE 表示禁止回收的内存页。关于匿名页和page cache的回收,还和swappiness参数的配置有关,见下文。

OOM Killer

这个就熟知了,Linux是允许memory overcommit的,只要你来申请内存我就给你,寄希望于进程实际上用不到那么多内存,但万一用到那么多了呢?那就会发生类似“银行挤兑”的危机,现金(内存)不足了。Linux设计了一个OOM killer机制来处理这种危机:挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续杀…,所以我们可以看到类似如下,把postmaster进程给杀了:

Out of Memory: Killed process 12345 (postgres).

我们通过设置内核参数 vm.panic_on_oom 使得发生OOM时自动重启系统。这都是有风险的机制,重启有可能造成业务中断,杀死进程也有可能导致业务中断,所以Linux 2.6之后允许通过内核参数 vm.overcommit_memory 禁止memory overcommit,可以写到/etc/sys_ctl.conf。

  1. 0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。Heuristic的意思是“试探式的”,内核利用某种算法猜测你的内存申请是否合理,它认为不合理就会拒绝overcommit。

  2. 1 – Always overcommit. 允许overcommit,对内存申请来者不拒。

  3. 2 – Don't overcommit. 禁止超过阈值的overcommit。

PostgreSQL中我们一般设置为2。

还有一个办法就是调整oom_score_adj,在 oom_kill.c ()代码里可以看到,OOM killer 会给每个进程打分,根据points的高低来决定杀哪个进程,这个 points 可以根据 adj 调节,我们可以调整每个进程的 oom_adj 参数来决定哪些进程不这么容易被 OOM killer 选中杀掉。比如,如果不想 PostgreSQL 进程被轻易杀掉的话可以找到 PostgreSQL 运行的进程号后,调整 /proc/PID/oom_score_adj 为 -15,防止重要的系统进程触发(OOM)机制而被杀死,每个进程都有一个oom_score的属性,oom killer会杀死oom_score较大的进程,当oom_score为0时禁止内核杀死该进程。设置/proc/PID/oom_adj可以改变oom_score,oom_adj的范围为 -17 ~ 15,其中15最大,-16最小,-17为禁止使用OOM,至于为什么用-17而不用其他数值(默认值为0),这个是由linux内核定义的。

关于SWAP

swappiness参数的作用

这个参数涉及到了swap交换分区,通过调整该参数,可以避免OS突然换页给数据库带来的负面影响,swap的目的说白了,就是基于性能的考虑:

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.

程序运行的一个必要条件就是足够的内存,而内存往往是系统里面比较紧张的一种资源。为了满足更多程序的要求,操作系统虚拟了一部分内存地址,并将之映射到swap上。对于程序来说,它只知道操作系统给自己分配了内存地址,但并不清楚这些内存地址到底映射到物理内存还是swap。物理内存和swap在功能上是一样的,只是因为物理存储元件的不同(内存和磁盘),性能上有很大的差别。

在个人之前的认知中,除了PostgreSQL,Oracle、MySQL等也都会从性能方面考虑,去尽量避免使用swap,毕竟是一个磁盘一个内存,速度是两个量级的。

前面也提到匿名页这个东西,对于anonymous pages,总是需要先写入swap area才能回收。而对于page cache,有一些可以直接discard(比如text段对应的页面,data段对应的页面中clean的部分),有一些dirty的页面需要先write back同步到磁盘。由于有flusher thread定期的write back,回收时还是dirty的page cache页面不会太多,也就是玩PostgreSQL经常会调优的那一坨vm参数:

  • vm.dirty_background_ratio/vm.dirty_background_bytes

  • vm.dirty_ratio/vm.dirty_bytes

  • vm.dirty_expire_centisecs

  • vm.dirty_writeback_centisecs

而且,page cache中的页面有对应的文件和在文件中的位置信息,需要换入恢复的时候也更加容易。因此,内核通常更倾向于换出page cache中的页面,只有当内存压力变得相对严重时,才会选择回收 anonymous pages。

我们经常调整的swappiness参数, 此参数值越低,就会让Linux系统尽量少用swap分区,多用内存;参数值越高就是反过来,使内核更多的去使用swap空间。Linux在进行内存回收(memory reclaim)的时候,就是从前文提到的file-backed pages和anonymous pages李米娜回收数据,也就是指定内存回收时anonymous pages和page cache的比重。swappiness越大,越倾向于回收匿名页;swappiness越小,越倾向于回收file-backed的页面,在Linux的早期版本(2012年以前的版本,kernel 3.5-rc1),哪怕swappiness被设置为0,其实匿名页仍然有被交换出去的机会。2012年开始,这个细节有了变化。https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fe35004fbf9eaf67482b074a2e032abb9c89b1dd如果swappiness=0,除非系统的内存过小(nr_free + nr_filebacked < high watermark)这种恶劣情况发生,都只是考虑交换file-backed的pages,就不会考虑交换匿名页了。于是乎,现在的swappiness如果等于0的话,意味着哪怕匿名页占据的内存很大,哪怕swap分区还有很多的剩余空间,除非恶劣情况发生,都不会交换匿名页,因此这可能造成更大的OOM(Out Of Memory)压力。不像以前,平时会一直兼顾着回收page cache和匿名页。所以,现在如果想将swappiness设置为0,那是要好好想想的了。

swap cache的作用

As the name suggests ,关于cache的,swap cache 主要是针对匿名内存页,例如用户进程通过 malloc() 申请的内存页,当要发生 swapping 换页时,如果一个匿名页要被换出时,会先计入到 swap cache,但是不会立刻写入物理交换区,因为 Linux 的原则是除非绝对必要,尽量避免 IO。所以 swap cache 中包含的是被确定要 swapping 换页,但是尚未写入物理交换区的匿名内存页。

kswapd的作用

kswapd 是 Linux 负责页面置换(Page replacement)的守护进程,它也是负责交换闲置内存的主要进程,它会在空闲内存低于一定水位时,回收内存页中的空闲内存保证系统中的其他进程可以尽快获得申请的内存,kswapd进程会周期性对内存进行检测,达到一定阈值的时候开始进行内存回收。Linux内核使用水位标记(watermark)的概念来描述这个压力情况。

Linux为内存的使用设置了三种内存水位标记:high、low、min。他们 所标记的含义分别为:

  • 剩余内存在high以上表示内存剩余较多,目前内存使用压力不大;

  • high-low的范围表示目前剩余内存存在一定压力;

  • low-min表示内存开始有较大使用压力,剩余内存不多了;

  • min是最小的水位标记,当剩余内存达到这个状态时,就说明内存面临很大压力。

  • 小于min这部分内存,内核是保留给特定情况下使用的,一般不会分配。

如果内存消耗导致剩余内存达到了或超过了watermark[min]时,就会触发直接回收(direct reclaim)。

swappiness的双刃剑

在我的传统认知之中,如果配置了swap分区,当内存紧张时,一部分的匿名页就会被换出swap out到位于外部磁盘的swap分区上,当这部分内存之后再被使用时,又从swap分区换入。这个过程确实会触发相对慢速的磁盘I/O操作,但如果禁止了swap分区,当内存紧张时,系统就只能回收file page,匿名页是不能回收的,这另一方面加剧了磁盘I/O的压力,从anon page转嫁到了file page头上,治标不治本,掩耳盗铃。

或许很多人认为swap分区只是快要撑满了的内存的一个紧急避难所,把它看做是内存不够「阔气」时一个不得已的选择。而且关于swap分区究竟应该设置多大,一直广为流传着这么一个说法:应该设为系统内存的两倍,Linux官方的建议如下,

还有思否这篇提到的https://segmentfault.com/a/1190000008125116:

桌面环境

一般不会开什么监控功能,所以也没法提前预知内存使用异常,当内存被用光的时候,分两种情况:

  • 配置了swap:在系统变慢的时候能感觉到,可能还有机会杀掉一些进程和保存当前工作进度,当然也会出现慢的想砸电脑的情况,不过在磁盘如此廉价的情况下,浪费点磁盘空间换取这样的一个机会还是值得的。

  • 没有配置swap:内核的OOM killer被触发,可能连保存工作进度的机会都没有。

服务器环境

服务器一般都会配置监控程序,当内存用量达到一个阈值的时候告警或者会自动重启异常的进程。但如果没有监控呢?当内存被用光的时候,分两种情况:

  • 配置了swap:这时服务器还能提供服务,但性能会降低好几个档次,直到最终处于几乎死机状态,并且这一过程将持续很长一段时间,对服务器来说是个灾难;所以配置swap只能让服务再苟延残喘一会儿,然后就是长时间的服务中断(比如原来是每秒处理1000个请求的服务器,由于频繁使用swap,导致现在每秒只能处理50个请求,站在系统角度,进程还在运行,但是在业务角度服务已经几乎中断了)。

  • 没配置swap:这时内核的OOM killer被触发,在默认配置下,耗内存的进程会被优先kill掉,这种进程一般就是我们的业务进程,这时守护进程就会自动重启该业务进程(没有守护进程?开什么玩笑),这种情况只会造成服务中断一会会儿(取决于进程重启的时间),不会出现上面因配置了swap而导致性能很差且服务持续中断的情况。就算OOM killer没有kill掉预期的进程,我们通过测试也能发现,然后将OOM killer配置成重启系统,那也比配置了swap在那里苟延残喘的好。

回到vm.swappiness参数,其值越小,则越不倾向于回收anon page,因为我们知道,使用清空文件的方式将有利于减轻内存回收时可能造成的IO压力。因为如果file-backed中的数据不是脏数据的话,那么可以不用写回,这样就没有IO发生,而一旦进行交换,就一定会造成IO。所以系统默认将swappiness的值设置为60,这样回收内存时,对file-backed的文件cache内存的清空比例会更大,内核将会更倾向于进行缓存清空而不是交换。另外透过这层字面意思,其更本质的含义其实是:回收匿名页对当前系统来说代价到底有多大,内核一般认为,如果zonefile还有的话,就可以尽量通过清空文件缓存获得部分内存,而不必只使用swap方式对anon的内存进行交换。传统的机械硬盘,I/O偏慢自不用多说,此外它还有一个由其物理构成决定的特点:随机读写的性能较差。file page的读写可能还表现出一定的连续性(要不然要prefetch干嘛),此处又要提到pg_prewarm、pg_fincore、effective_io_cocurrency这些基于posix_fadvise()来做的机制了。而匿名页,则表现出更强的随机性。所以在spinning disk的环境下,anon page换入换出的代价是真的高,是真的不适合对其进行太多的swap。但现在SSD的使用越来越广泛,除了I/O的速度提升,作为基于flash单元的设备,其随机读写的性能也不再成为掣肘,所以为什么针对于random_page_cost针对SSD我们一般都会做调整,调至2.0 ~ 2.5左右,面对SSD的环境,回收anon page的代价可以说与回收file page的代价相差无几,就算把swappiness上调到100(意味着两者被一视同仁,平起平坐),也是很合理的。

所以合理设置swap分区的大小和"vm.swappiness"的值,不仅可以降低系统在走投无路时祭出OOM killer的概率,还可以提高系统性能的综合表现。

Buffer cache和Page cache的区别

简单来说,两者都是为了优化磁盘 IO 的读写速率,其中 PageCache 缓存了文件页用来优化文件 IO;而 BufferCache 缓存了磁盘块用来优化块设备的 IO 。很多的类 Unix 系统采用了与 Linux 2.4 之前版本类似的策略,也就是文件缓存在 PageCache 而磁盘块缓存在 BufferCache。而实际上,大部分文件是通过文件系统呈现,而且存储在磁盘上,这就会导致同一份文件保存了两份,不优雅也不高效,为此,在 Linux 2.4 版本之后,就将两者进行了统一。如果被缓存的数据即是文件数据又是块数据 (对于文件来说大部分的数据是的,元数据不是),此时 BufferCache 会有指针指向 PageCache ,这样数据就只需要在内存中缓存一份。当讨论磁盘缓存时,其实就是 PageCache ,它缓存了磁盘文件数据,从而提高 IO 的吞吐量。

当然,目前 BufferCache 仍然是存在的,因为还存在需要执行的块 IO。因为大多数块都是用来存储文件数据,所以大部分 BufferCache 都指向了 PageCache;但还是有一小部分块并不是文件数据,例如元数据、RawBlock IO,此时还需要通过 BufferCache 来缓存。

明白了这两套缓存系统的区别,就可以理解它们究竟都可以用来做什么了。

  • Page Cache:主要用来作为文件系统上的文件数据的缓存,常见的是针对文件的 read()/write() 操作,另外也包括了通过 mmap() 映射之后的块设备,也就是说,事实上 Page Cache 负责了大部分的块设备文件的缓存工作。

  • Buffer Cache:BufferCache 用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用,例如格式化文件系统时。

所以page cache对于PostgreSQL来说,是一个很友好的存在,这也是pgfincore、pg_prewarm扩展的好处,加载到page cache或者shared_buffer中,shared_buffer找不到了,我就去page cache去找,并且二者的换页算法是不同的,PostgreSQL是基于clock改良后的NFU算法,实际上两个缓存里面重复的块并不多。

不仅仅是PostgreSQL,对于ElasticSearch,日常运维中,也会强调page cache,ES的搜索引擎严重依赖于底层的page cache cache,你如果给 page cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。如下是pgfincore、pg_prewarm的差异,可以看到pgfincore最多只能到page cache这一层:

另外,page cache在PostgreSQL里面,也不失为进程间通信的一种好方法:

这对于XLOG来说,有好处也有坏处,如果你的文件在写入后要立即读取,那么无疑是写缓存更好。因此建议有流复制的主节点或上游节点不要使用DirectIO。同样对于standby节点来说,XLOG也是需要立即读取用来恢复的,因此standby节点也不需要开启DirectIO。

所以我们要知道这么多cache的存在,文件系统有Cache,存储有Cache,RAID控制器上有Cache,磁盘上也有Cache。很多人都误认为使用write就能将数据写入到磁盘上,然而这是错误的。一般情况下,对文件的write只会更新内存中的页缓存,这些页缓存不会立刻刷入磁盘,操作系统的 flusher线程会在满足以下条件时将数据落盘:

  1. 空闲内存下降到了特定的阈值,需要释放脏页占用的内存空间;

  2. 脏数据持续了一定时间,最老的数据就会被写入磁盘;

  3. 用户进程执行sync或者fsync系统调用;

如果我们想要将数据立刻刷入磁盘,就需要在执行write后立刻调用fsync等函数,当 fsync等函数返回后,数据库才会通知调用方数据已经成功写入。PostgreSQL认为系统提供的fsync调用是可靠的,即写到了持久化的存储。然后下面还有更底层的Raid Cache、Disk Cache等,虽说大多数Raid卡或企业级SSD可以通过电容残余的电量,将Disk Cache里的数据持久化下来,但是请不要相信所有磁盘都有这个功能。所以这个可以叫做"说谎的驱动器",虽然返回写成功了,但是不一定实际写入了。因此不要轻易使用易失缓存,可以使用有掉电保护的易失缓存。在个人工作过程中,还有幸暂未遇到因为回写缓存,系统崩溃导致PostgreSQL无法使用的例子。

回收cache

内核在内存将要耗尽时,会触发内存回收的工作,一般来说主要释放的是 Buffer/Cache 的内存,但是这种清缓存的操作也并不是没有成本。理解 page Cache 的作用,那么如果要清理缓存,那么必须要保证数据的一致性,所以一般在清理的时候同时会伴随这 IO 彪高。因为内核要对比内存中的数据和对应硬盘文件上的数据是否一致,如果不一致需要写回,之后才能回收。

在系统中除了内存将被耗尽的时候可以清缓存以外,还可以使用下面这个文件来人工触发缓存清除的操作。

# echo 1 > /proc/sys/vm/drop_caches

其中的取值可以是 1 2 3,代表的含义为:

  • 1 代表清除 PageCache;

  • 2 代表回收 slab 分配器中的对象 (包括目录项缓存和 inode 缓存),slab 是内核中管理内存的一种机制,其中很多缓存数据实现都是用的 PageCache;

  • 3 代表清除 PageCache 和 slab 分配器中的缓存对象。

分析内存

以上的基础知识铺垫了那么久,进入实战环节。

sar -B的使用

sar功能很多,sar -B可以反应内存的使用情况,看一下man对各个字段的解释

pgpgin/s和pgpgout/s很好理解,每秒内换进换出的内存大小,这两个指标越大说明物理内存越不够。

fault/s:也就是缺页异常,会产生一个中断,又分为几种:

  1. major page fault也称为hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap回到物理内存也是hard page fault。

  2. minor page fault也称为soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。(通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault,另外一种可能是,在MMU上注销了,但是还没来得及刷新到磁盘)。不论是哪种情况,可以肯定的是该页一定已经在物理内存中了,操作系统只需要付出少量的代价就能恢复过来;

  3. invalid fault也称为segment fault, 指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报segment fault错误。

pgfree/s:这个很好理解,每秒放到空闲链表的页的数量

pgscank/s:刚刚也介绍了kswapd这个守护进程的作用,keep the memory management system operating efficiently,他会努力维护好足够多的内存

pgscand/s:也就是直接内存回收的值

pgsteal/s:每秒从page cache和swap cache中回收的页数量,分别表示的是映射到磁盘的内存缓存和映射到交换区的内存缓存,这个值越多说明在拼命的从cache里面刷出回收内存

%vmeff:pgsteal / pgscan,表示内存回收的效率,100%意味着inactive list里面获取的page都被重用了

Calculated  as  pgsteal / pgscan, this is a metric of the efficiency of page reclaim. If it is near 100% then almost every page coming off the tail of the inactive list is being reaped. If it gets too low (e.g. less than 30%) then the virtual memory is having some difficulty.  This field is displayed as  zero  if  no  pages  have  been scanned during the interval of time

The %vmeff column shows the number of pages scanned (pgscand/s) in relation to the ones being reused from the main memory cache or the swap cache (pgsteal/s). It is a measurement of the efficiency of page reclaim. Healthy values are either near 100(every inactive page swapped out is being reused) or 0 (no pages have been scanned). The valueshould not drop below 30.

在SUSE的官方文档中,有如下一段话,也就是越小越恶劣。

03:40:01 PM pgpgin/s pgpgout/s   fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s   %vmeff
03:50:01 PM      0.00      9.57    132.07      0.00     55.49      0.00      1.82      1.78     97.90
04:00:01 PM      0.00      7.95     59.17      0.00     30.26      0.00      0.00      0.00      0.00
04:10:01 PM      0.00      8.43     97.50      0.00     40.73      0.00      0.00      0.00      0.00
04:20:01 PM      0.00      7.52     51.26      0.00     24.01      0.00      0.00      0.00      0.00
04:30:01 PM      0.00      8.00     52.04      0.00     26.00      0.00      0.00      0.00      0.00
04:40:01 PM      0.00     10.78     63.73      0.00     28.47      0.00      0.00      0.00      0.00
04:50:01 PM      0.00     10.86     75.45      0.00     33.92      0.00      0.00      0.00      0.00
05:00:01 PM      0.00      7.32     51.08      0.00     23.44      0.00      0.00      0.00      0.00
05:10:01 PM      0.00     11.64     77.17      0.00     32.82      0.00      0.00      0.00      0.00
05:20:01 PM      0.00      7.58     50.87      0.00     25.52      0.00      1.79      1.79    100.00
05:30:01 PM      0.00     10.71     67.98      0.00     29.70      0.00      0.00      0.00      0.00
05:40:01 PM      0.00      7.53     51.32      0.00     23.30      0.00      0.00      0.00      0.00
05:50:01 PM      0.00      7.74     50.74      0.00     23.39      0.00      0.00      0.00      0.00
06:00:01 PM      0.00      7.32     50.71      0.00     23.37      0.00      0.00      0.00      0.00
06:10:01 PM      0.00      7.87     59.08      0.00     25.44      0.00      0.00      0.00      0.00
06:20:01 PM      0.00     10.62     66.65      0.00     31.85      0.00      0.00      0.00      0.00
06:30:01 PM      0.00      7.57     50.85      0.00     22.96      0.00      0.00      0.00      0.00
06:40:01 PM      0.00      7.53     50.71      0.00     23.31      0.00      0.00      0.00      0.00
06:50:01 PM      0.00      7.57     50.82      0.00     23.44      0.00      0.00      0.00      0.00
07:00:01 PM      0.00      7.49     50.65      0.00     23.35      0.00      0.00      0.00      0.00
07:10:01 PM      0.00      8.03     59.42      0.00     25.74      0.00      0.00      0.00      0.00
Average:         0.00      8.55     59.57      0.00     26.88      0.00      0.08      0.08     99.56![]

/proc/meminfo

有了上面的知识铺垫,现在看下面的就轻松多了,比如我们关心的指标:

Active(anon)、Inactive(anon)会根据swappiness的配置决定你是否可以回收reclaim,

Inactive(file)也就是可以直接回收使用的,

Unevictable是所有禁止换出的页的描述符

/proc/meminfo中的Shmem统计的内容包括,关于tmpfs可以参照之前的文章《窥探PostgreSQL中的共享内存》:

  • shared memory

  • tmpfs和devtmpfs。

此处所讲的shared memory又包括,也参照之前的文章《窥探PostgreSQL中的共享内存》:

  • SysV shared memory [shmget etc.]

  • POSIX shared memory [shm_open etc.]

  • shared anonymous mmap [ mmap(…MAP_ANONYMOUS|MAP_SHARED…)]

[root@xiongcc ~]# cat /proc/meminfo
MemTotal:        3690232 kB
MemFree:          165500 kB
MemAvailable:    2866060 kB
Buffers:          136884 kB
Cached:          2762940 kB
SwapCached:            0 kB
Active:          2240840 kB
Inactive:        1106300 kB
Active(anon):     457316 kB
Inactive(anon):    36780 kB
Active(file):    1783524 kB
Inactive(file):  1069520 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        447316 kB
Mapped:            71660 kB
Shmem:             46780 kB
Slab:             116912 kB
SReclaimable:     101688 kB
SUnreclaim:        15224 kB
KernelStack:        3216 kB
PageTables:         6296 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     1845116 kB
Committed_AS:    1450268 kB
VmallocTotal:   34359738367 kB
VmallocUsed:       14088 kB
VmallocChunk:   34359715580 kB
HardwareCorrupted:     0 kB
AnonHugePages:    348160 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:       49848 kB
DirectMap2M:     3950592 kB
DirectMap1G:           0 kB

关于/proc/meminfo直接参照这篇文章,十分详尽:http://linuxperf.com/?p=142

NUMA的处理

这里又要提到NUMA了,NUMA又叫非一致性内存访问,NUMA 服务器的基本特征是 Linux 将系统的硬件资源划分为多个节点(Node),每个节点上有单独的 CPU、内存和 I/O 槽口等。CPU 访问自身 Node 内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致内存访问 NUMA 的由来。我们可以通过查看/proc/zoneinfo观察,可以看到刚刚提到的水位(watermark)了

[root@xiongcc ~]# cat /proc/zoneinfo
Node 0, zone     DMA
pages free     3676
      min      72
      low      90
      high     108
      scanned  0
      spanned  4095
      present  3998
      managed  3977
  nr_free_pages 3676
  nr_alloc_batch 18
  nr_inactive_anon 18
  nr_active_anon 53
  nr_inactive_file 58
  nr_active_file 149
  nr_unevictable 0
  nr_mlock     0
  nr_anon_pages 53
  nr_mapped    27
  nr_file_pages 225
  nr_dirty     2
  nr_writeback 0
  nr_slab_reclaimable 9
  nr_slab_unreclaimable 13
  nr_page_table_pages 0
  nr_kernel_stack 0
  nr_unstable  0
  nr_bounce    0
  nr_vmscan_write 44
  nr_vmscan_immediate_reclaim 22
  nr_writeback_temp 0
  nr_isolated_anon 0
  nr_isolated_file 0
  nr_shmem     18
  nr_dirtied   317953
  nr_written   317818
  numa_hit     54710
  numa_miss    0
  numa_foreign 0
  numa_interleave 0
  numa_local   54710
  numa_other   0
  workingset_refault 1017
  workingset_activate 408
  workingset_nodereclaim 0
  nr_anon_transparent_hugepages 0
  nr_free_cma  0
      protection: (0, 2798, 3586, 3586)
pagesets
  cpu: 0
            count: 0
            high:  0
            batch: 1
vm stats threshold: 4
  cpu: 1
            count: 0
            high:  0
            batch: 1
vm stats threshold: 4
all_unreclaimable: 0
start_pfn:         1
inactive_ratio:    1

NUMA 的 4 种内存分配策略:

  • 缺省(default):总是在本地节点分配(当前进程运行的节点上)

  • 绑定(bind):强制分配到指定节点上

  • 交叉(interleavel):在所有节点或者指定节点上交叉分配内存

  • 优先(preferred):在指定节点上分配,失败则在其他节点上分配

NUMA 的内存分配策略对于进程来说,并不是乐观的。因为 NUMA 默认是使用 CPU 亲和的内存分配策略,即请求进程会从当前所处的 CPU 的 Node 请求分配内存。当某个需要消耗大量内存的进程耗尽了所处的 Node 的内存时,就会导致产生 swap,不会从远程 Node 分配内存,这就是 swap insanity 现象。

MySQL

MySQL提供了innodb_numa_interleave参数来控制内存分配的问题

PostgreSQL

PostgreSQL没有参数可以控制这个,取0,系统倾向于从其他node分配内存;取1,系统倾向于从本地节点回收Cache。在PostgreSQL中,通常取0,因为PostgreSQL的double buffer机制,cache是很重要的,并且会在bios中将numa关闭:grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="numa=off"

Oracle

Oracle官方的建议是在大多数环境中,将vm.zone_reclaim_mode这个参数设置为6,也就是禁止本地节点分配远程内存。可以参照白鳝大师的文章《从一个案例看关闭NUMA的必要性》

因为这种运行ORACLE数据库,并且有OGG复制的环境来说,文件缓冲的使用十分频繁,当某个NUMA节点FREE空间不足的时候,进行大规模CACHE DROP操作时产生的影响十分大,平时更积极的回收INODE缓冲,可以缓解这种趋势。

备注

  • echo 0 > /proc/sys/vm/zone_reclaim_mode# # 意味着关闭zone_reclaim模式,可以从其他zone或NUMA节点回收内存

  • echo 1 > /proc/sys/vm/zone_reclaim_mode# # 表示打开zone_reclaim模式,这样内存回收只会发生在本地节点内

  • echo 2 > /proc/sys/vm/zone_reclaim_mode# # 在本地回收内存时,可以将cache中的脏数据写回硬盘,以回收内存。

  • echo 4 > /proc/sys/vm/zone_reclaim_mode# # 可以用swap方式回收内存。

注意:zone_reclaim_mode的只有低3位有效,所以还可以设置的值为,3、5、6、7。此外,内核中对该值没有限制,可以写入任意的值到该接口中。

小结

所以,关于swap的理解,不能简单的理解成,关闭了,就对性能有提高,还要考虑匿名页的问题,假如服务器内存在大量匿名页,你又关闭了swappiness,让他不能去回收这些匿名页,最后的后果可能就是不断恶化。有时我们希望swap大一些,可以在内存不够用的时候不至于触发oom-killer导致某些关键进程被杀掉,比如数据库业务。也有时候我们希望不要swap,因为当大量进程爆发增长导致内存爆掉之后,会因为swap导致IO跑死,整个系统都卡住,无法登录,无法处理。这时候我们就希望不要swap,即使出现oom-killer也造成不了太大影响,但是不能允许服务器因为IO卡死像多米诺骨牌一样全部死机,而且无法登陆,比如IO密集型的库。

今天一番恶补之后,打翻了很多之前的认知,对Linux的内存管理也有了一个初步的认知,真是一个庞大的知识体系。内存全景图:👇



参考

https://zhuanlan.zhihu.com/p/72998605

https://zhuanlan.zhihu.com/p/363359719

https://chrisdown.name/2018/01/02/in-defence-of-swap.html

https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/ch-swapspace

https://zhuanlan.zhihu.com/p/83945844

https://teawater.github.io/presentation/zram_zsmalloc.pdf

http://linuxperf.com/?p=142

https://www.slideshare.net/ennael/kernel-recipes-2017-20-years-of-linux-virtual-memory-andrea-arcangeli

https://www.codeleading.com/article/96255161874/

https://maggie262.github.io/2020/02/07/linux-memory-summary/

文章有问题?点此查看未经处理的缓存