# 内存问题的排查

本周排查线上一次抖动，部署的mongo时不时会出现请求latency变高的问题；`leader`让我排查一下为什么会抖动的；因为是海外机房，其实我基本上已经确认是内存的问题的， 因为海外资源比较有限，很多进程都会部署在一个机器上的，相互影响比较严重的；当然基本确认是什么问题，但是还是需要去验证一下；

#### 1. 排查的方式

内存问题影响到业务层基本上的原因在于：

>  当出现内存极端不足的情况的时候,用户进程如果这个时候申请内存的时候，操作系统会因为没有足够的物理内存，从而触发操作系统的直接回收内存的模型，这个内存回收会在用户申请内存的上下文中直接触发的，也就是本来你去申请内存，这个时候却在做回收内存的；从而导致申请内存操作latency变高，从而引起了正常的业务请求抖动的；

通常要尽力避免进入这样的内存回收状态，因为这个时候在机器的所有的进程都是会被影响的，从而导致不可控的抖动的；那有哪几种的方式可以观察到这种情况的：

* `tsar`

```shell
tsar --mem -i 1 -l 
```

```shell
Time              -----------------------mem---------------------- ---------------swap-------------
Time                free    used    buff    cach   total    util    swpin  swpout   total    util
03/04/23-21:09:48   4.3G   84.0G    1.1G  162.4G  251.8G   33.35     0.00    0.00    0.00
03/04/23-21:09:49   4.2G   84.0G    1.1G  162.5G  251.8G   33.36     0.00    0.00    0.00
03/04/23-21:09:50   4.1G   84.0G    1.1G  162.6G  251.8G   33.35     0.00    0.00    0.00
03/04/23-21:09:51   4.0G   84.0G    1.1G  162.7G  251.8G   33.36     0.00    0.00    0.00

* mem.used: 已用内存大小（单位：KB）。
* mem.buff: Buffers占用的内存大小（单位：KB）。
* mem.cach: Cache占用的内存大小（单位：KB）。
* mem.free: 空闲内存大小（单位：KB）。
* mem.total: 总共的物理内存
* mem.util: 内存使用的百分比

* swap.swpin: 磁盘交换到内存的大小
* swap.swpout: 内存交换到磁盘的大小
* swap.total: 可用的swap的总共大小
* swap.util: 使用百分比

通常关注`free`和`used`两个指标，内存不足的时候你可能关注到free指标不断下降，时不时就还会慢慢涨回来
swap: 表示内存和磁盘之间的交换，通常线上机器都会将这个关闭，原因是磁盘的速度太慢，如果开启会在交换时候非常缓慢，并且对磁盘的压力也很大
```

`tsar`本身能看到的内存信息会比较少，尤其是`direct reclaim`这个指标不是很明确

* `sar`

```shell
sar -rRB 1 100000
-r: 内存的统计信息
-R: 缓存的统计信息
-B: 页面缓存的统计信息
```

```
Linux 4.18.5-041805-generic (jjh2972) 	04/03/2023 	_x86_64_	(48 CPU)

09:21:27 PM  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s    %vmeff
09:21:28 PM      0.00  87068.00  33798.00      0.00  32141.00      0.00      0.00      0.00      0.00

09:21:27 PM   frmpg/s   bufpg/s   campg/s
09:21:28 PM  -6708.00      0.00   6689.00

09:21:27 PM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit  kbactive   kbinact   kbdirty
09:21:28 PM   6127096 257892216     97.68   1120412 168752100 113597140     43.03 181323284  71838856     36776

指标解释：

* kbmemfree：空闲内存大小，单位是KB。
* kbmemused：使用中的内存大小，单位是KB。
* %memused：使用中的内存占比。
* kbbuffers：系统buffer的大小，单位是KB。
* kbcached：系统cache的大小，单位是KB。
* kbcommit：已经分配但未使用的内存大小，单位是KB。
* %commit：已经分配但未使用的内存占比。
* kbactive：正在使用的活动内存大小，单位是KB。
* kbinact：不活跃的内存大小，单位是KB。
* kbdirty：等待刷新到磁盘的内存大小，单位是KB。

* frmpg/s: 每秒被free的page的个数;如果是负值就表示有page被分配
* bufpg/s: 每秒被分配用于buffer的page的个数，如果是负值就表示很少有page被用于buffer
* campg/s: 每秒被分配用于cache的page的个数,如果是负值,表示几乎没有page用在cache中

* pgpgin/s: 每秒从磁盘进来的page的总kb
* pgpgout/s: 每秒从系统内存回退到磁盘的总kb
* fault/s: 缺页的次数,这个包含了major+minor,这个次数不是全部度会产生IO的; 有的只是虚拟地址与物理地址没有对应，缺页的解决方法只需要对应一下就好; 而有的则需要从磁盘中load上来;
* magflt/s: 会引起io操作的缺页处理
* pgfree/s: 每秒被放到free list的page的个数
* pgscank/s: kswapd每秒scan的page的个数
* pgscand/s: 直接回收每秒scan的page的个数
* pgsteal/s: 每秒被回收的page的个数
* vmeff: 计算方式(pgsteal/(pgscank+pgscand)), 这是反映内存回收的效率; 如果这个值很高就表示在inactive尾部的一些page都将被回收; 如果太小,就表示很难有page可以被回收回来

主要关注的指标有:
* kbmemfree: 表示当前空闲内存的大小，可以大概看一下，是不是空闲内存很不足; 
* kbdirty: 如果这个数值很大的话，就说明有大量的内存需要被回写到磁盘，也可能会造成一次回写过多导致磁盘压力很大的；可以通过内核参数进行配置；
* magflt/s: 表示重大缺页的次数，这个频率很高的话就表示内存不足的表现，因为需要不断的剔除一些old page，存放new page，这个过程对cpu来说很慢
* pgscand/s:表示应用进程申请内存，因为操作系统内存不足导致需要在当前context执行遍历page来释放内存的过程的；这个指标非常的重要的，如果这个指标频繁出现，那么就表示当前系统的可用内存严重不足，已经开始影响到了应用进程了；
* vmeff: 这是回收效率的体现，看计算方式可以看出: `真是回收/扫描的page的总数`，内存充足的情况下，通常为0,需要配合`pgscank pgscand`来一起使用;
```

* 连续内存不足的问题

有的时候，你会发现`free`内存比较多，但是`pgscand/s`依然频频出现，这个时候就需要思考是否是连续内存不足的问题；因为当用户申请连续内存的时候，虽然有虚拟内存地址，其实并不限制物理内存需要连续的，但是操作系统倾向于物理内存也是连续的，估计是性能上会更加好一些吧的；所以如果连续内存不足，其实也会导致操作系统进入类似模型进行内存回收，影响应用进程的;

```shell
cat /proc/buddyinfo

Node 0, zone      DMA      2      3      2      2      2      2      2      1      1      2      2
Node 0, zone    DMA32  12631  11776   4183   1428    118     40     17      8      3      5      0
Node 0, zone   Normal 580413      0      4      7      4      3      2      0      0      1      0
Node 1, zone   Normal 585174 170141  61788  18068   2506    533    124     32     12      0      0

只需要关注Normal两行即可：
* node: numa的概念, 通常在numa架构cpu申请内存会优先从自己节点的内存获得
* 后面的一列一列数据表示: `2^0`, `2^1`, `2^2`...连续n个page的空余数；比如`2^2`就是表示有连续4个page的内存的个数;
```

从`buddyinfo`中输出的信息中有`Node`的概念，这是numa里面的概念，随着服务器的cpu的核数越来越多，在硬件层面上`core`和内存之间就会存在远近的问题，比较正规的叫法: `NUMA节点`,每个 `NUMA` 节点都有自己的 CPU、内存和 I/O 等资源，是一个独立的计算资源集，而在不同节点的cpu访问`local`内存和`remote`内存的时候延迟是不一样的，几乎是double的差距；而上面的`buddyinfo`中的信息输出就表示当前系统有2个`NUMA`节点，当前`Node 0`的连续page的个数比较少；**可能**会存在内存不足的情况；

当然为什么是可能呢? 因为很多系统本质上是不开启numa的，不开启numa的话，本质上不同节点的内存对操作系统来说是统一调配的，所以如果`Node 0`不够就会从`Node 1`进行分配;

```shell
numactl --hardware

输出结果：
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
node 0 size: 96456 MB
node 0 free: 2724 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31
node 1 size: 96757 MB
node 1 free: 2520 MB
node distances:
node   0   1
  0:  10  21
  1:  21  10
  
这些信息表示:
1. 当前系统存在两个numa节点
2. node x size: 表示当前节点下面的总内存 ( cat /sys/devices/system/node/nodex/meminfo 也可以观察)
3. node x cpu: 哪些cpu是属于哪个node,这个比较重要，因为绑核操作的话，需要关注这些物理限制，尽可能将进程绑在相同的node下面的cpu
4. node x free: 表示当前node剩余的内存
5. node distances: 大致表示节点之间的距离，也表示相互调用之间的性能差距，这是一个相对比较的；比如上面的这个信息输出，基本可以认为调用本地内存和remote内存的latency基本在2倍左右，当然具体还是要根据实测为主; 
```

从上面的输出结果可以看到两个节点的`free`基本上差不多的；主要原因是因为启动程序的时候指定了`numactl --interleave=all`这个参数，这个参数会强制程序在多`Node`之间做内存的负载均衡，保证两个节点的内存使用率近乎相同；所以整体就非常均衡的；

偶尔需要关注一下`buddyinfo`连续内存是否足够，如果不够的情况下，也是会触发direct reclaim这个模型，影响到用户进程;

* 其他

还有哪些情况需要关注呢？

1. swap分区；正常情况下我们在服务器上会关闭swap分区，原因是磁盘的性能是跟不上内存的，如果有了swap分区就表示系统认为的内存大小要大于真实物理内存，而中间的取舍就是性能；遇到一次申请很大的内存的时候，系统为了获得足够的内存就会将内存写入到磁盘来获得更加多的空余内存，或者是从磁盘中加载之前换出的内存数据，这两个过程会很慢，触发很严重的抖动问题；通过上面的`tsar`可以看到对应的swap分区的使用率，优先关闭swap；

2. `dirty page`: 操作系统本身自己对io有一层缓冲，叫做`pagecache`, 大部分情况下，写文件基本都是在写内存，好处是速度快，问题是安全性比较差，如果突然关机就可能导致数据丢失；大部分的操作系统依然还是有pagecache，不然直接写磁盘设备性能会很差，那些被改变的数据的page就叫做`dirty page`, 说明与磁盘上的数据不一样，需要进行回写；策略基本上几个：

    * `dirty ratio`: 脏页比例
    * 定时: 按照固定频率来进行回写

    基本上会两者都包含的；Linux控制`dirty page`回写的参数有一些: 

    ```shell
    # dirty_background_bytes 表示当前系统所能容忍的最大的脏页的字节数，如果超过就会回写磁盘，0表示禁用
    vm.dirty_background_bytes = 0 
    # dirty page所占用比例，如果超过这个比例就进行回收
    vm.dirty_background_ratio = 10
    # 与dirty_background_bytes 不同的在于,当超过配置之后会停止用户进程，尽可能的将dirty写回到磁盘
    vm.dirty_bytes = 0
    # 每个dirty page在内存中保留的最长时间,单位为ms
    vm.dirty_expire_centisecs = 3000
    # 同dirty_background_ratio，会影响用户进程
    vm.dirty_ratio = 20
    # 表示回写dirty page到磁盘的最小时间间隔,定时回写(ms)
    vm.dirty_writeback_centisecs = 500
    ```

    ```shell
    /proc/vmstat中有一些数据:
    # 单位为page，触发直接dirty回写的阈值
    nr_dirty_threshold 4527190
    nr_dirty_background_threshold 2263595
    ```

    目前没找到对应比较好的指标能看到`direct page writeback`，只能从几个方面联合看看吧;

#### 2. 如何缓解内存问题

为什么算是缓解而不是彻底解决呢？因为本质上是一种内存资源不够的问题导致的，虚拟内存机制保证了每一个进程都认为自己有一个很大的内存空间，但是真实的物理内存是那么大，除了所有用户进程在共用还有操作系统本身也公用了，所以在真实物理内存有限的情况下，而内存需求方瞬间需要大量的内存的时候就会遇到各种问题的。虽然不能彻底解决这个问题，但是可以通过一些机制来提早将一些无用的内存进行回收留作后用，而这就是我们的解决方式。

Linux的内存管理挺复杂的，只能具体问题具体分析的；这次我们要解决的是减少内存的`direct reclaim`的频率，原因是进入这个状态之后会极大的影响用户进程的，用户进程会直接参与去回收内存，而这个过程就会增加请求的`latency`; 类似于:

> 本来我去外面的餐馆吃饭，但是由于现在米不够了，不能分配足够的饭给我；于是饭店就要求我去农田里帮他们割稻生产出米，直到足够满足我一开始的要求的饭为止(不确定是不是真的是这样的)；你可以思考一下用户进行用malloc申请内存之后就进入了这种状态，整个耗时还能控制吗？

##### 2.1 内存回收的基本模式

Linux的内存回收模式大概分为两种:

* `direct reclaim`: 这是一个同步回收内存的过程；当用户进程申请内存的时候，操作系统发现系统空余内存低于一个程度之后就会进入这个状态；这个状态极端影响性能
* `background` 内存回收: Linux中有一个内核线程叫做`kswapd`,这个线程按照一定的频率会工作，主要的工作是判断当前的空余内存是否低于一定的水平，如果低于的话就启动回收内存，将一些不活跃的page换出或者释放，来得到更多的空余内存，当空余内存高于某一个水平的时候，`kswapd`就会停止这个回收过程的；因为这个过程是异步的，所以对用户进程来说是没有感知的；

为什么要区分两个工作模式的？原因也很简单的，当`kswapd`已经无能为力的时候，就要`direct reclaim`介入，其实`kswapd`和申请内存的进程之前是一个生产者和消费者的过程，当生产者跟不上消费者的时候，消费者就只能等待的；与其等待还不如自力更生(`direct reclaim`)去生产呢；所以如果只有`kswapd`的话那么当内存不够的时候，整个系统也是被hang住的，因为这个时候内存就是不足;

既然知道了在不同的内存水平线的时候会有不一样的行为，那么这个水平线是如何确定的呢？

```shell
// 包函了不同numa node的当前内存的一些信息, 我们重点看Node 0和Node 1的Normal的信息,
cat /proc/zoneinfo

Node 0, zone   Normal
  pages free     670703
        min      385498
        low      481872
        high     578247
  
Node 1, zone   Normal
  pages free     759799
        min      393829
        low      492286
        high     590743
```

其中存在有`min|low|high`这几个指标，这几个指标就是控制内存的行为；

![image-20230409160449378](https://cdn.nomoshen.com/typora_img/image-20230409160449378.png)

1. 当内存低于`Low`的时候，`Kswapd`就会开始工作
2. 当内存高于`High`的时候，`Kswapd`就会停止工作
3. 当内存低于`Min`的时候，那么就进入`direct reclaim`

关于`min|low|high`是如何计算的，linux里面一段代码来解释: 主要的过程是:

1. 根据`vm.min_free_kbytes`这个参数获得`pages_min`
2. 按照zone的比例进行划分最小的`min`
3. `low = min + 1/4 * min`
4. `high = min + 1/2 * min`

当然是不是一定是这样的，会因为`min_free_kbytes`设置的value不一样而不一样，过小的时候会有其他的参数影响；

```c
void __setup_per_zone_wmarks(void)
{
    unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10);

    for_each_zone(zone) {
        tmp = (u64)pages_min * zone->managed_pages;
	do_div(tmp, lowmem_pages);

        zone->watermark[WMARK_MIN] = tmp;
        zone->watermark[WMARK_LOW]  = min_wmark_pages(zone) + (tmp >> 2);
        zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1);
    ...
}
```

所以要修改内存的行为的话，主要是修改`vm.min_free_kbytes`即可；通过提升这个数值，来提前让kswapd启动来进行回收内存，也可以增加`min`和`low`之间的差距；

##### 2.2 经验

```shell
# vim /etc/sysctl.conf 增加以下行，参数值根据上面计算所得

内核版本差异  配置有所区分
# 4.6之前
vm.min_free_kbytes=1048576 # 1G每64G

# 4.6之后
vm.min_free_kbytes=524228 # 0.5G每64G
# 这个参数也会影响low和high的value，但是不影响min
vm.watermark_scale_factor=100

# sysctl -p  使参数生效

# sysctl -a | grep vm.min_free_kbytes 查看当前配置值
```

##### 3.3 其他

之前聊到过连续内存问题，操作系统也会有对应的策略:`kcompactd`内核线程，也是会定期去扫描node对应的page，然后进行对应的page合并，从而得到连续的大内存; 

```shell
# 可以看到一些compact一些运行参数
cat /proc/vmstat |grep compact
```

因为现在我还没遇到对应的问题，所以不是很清楚出现了这种连续内存不足的情况下，系统会有怎么样的反应，理论上compact应该是会影响性能的，但是目前还没有`direct reclaim`那么明显指标来看；后面多关注一下;

#### 4. 总结

目前能理解的内存问题基本就是这样，基本都是内存不足引发的`direct reclaim`从而引发的性能问题的； 能调整的参数就上面说的`vm.min_free_kbytes`, 当然也可以关注一下`dirty page`的内核参数，通过频繁的刷盘也削峰填谷；其他的大概还有`NUMA node`的内存分配参数的`zone_reclaim_mode`这个，这个参数主要是当`node`本地的内存不够的时候优先选择本地回收内存还是从remote的node获得内存；

这文章写了一周多，中间靠`google + chatgpt`一起合作来完成的，因为chatgpt有的回答一半对一半错，真的很难搞的；现在我对未知领域学习的时候对chatgpt还是要保持怀疑的态度。


