Linux优化

java系统优化参数查看

jps

jstat -gcutil pid 100(滚动时间)

jstack -l pid

#堆信息
jmap -histo:live pid

jmap -heap pid

jcmd pid help

jcmd pid VM.version

jcmd pid GC.run

Arthas jvm调优工具

Linux系统资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ps aux --sort -%mem | head -n 7

ps aux --sort -%cpu | head -n 7

df -h

du -h

查看每个目录大小
sudo du -sh *

统计目录多少
du -sm * | sort -n

free -h

free -m

free -g

进程实时情况:

1
watch -n 1 free -h

堆栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
sudo vi gstack



#!/bin/sh
if test $# -ne 1; then
echo "Usage: `basename $0 .sh` <process-id>" 1>&2
exit 1
fi

if test ! -r /proc/$1; then
echo "Process $1 not found." 1>&2
exit 1
fi

# GDB doesn't allow "thread apply all bt" when the process isn't
# threaded; need to peek at the process to determine if that or the
# simpler "bt" should be used.

backtrace="bt"
if test -d /proc/$1/task ; then
# Newer kernel; has a task/ directory.
if test `/bin/ls /proc/$1/task | /usr/bin/wc -l` -gt 1 2>/dev/null ; then
backtrace="thread apply all bt"
fi
elif test -f /proc/$1/maps ; then
# Older kernel; go by it loading libpthread.
if /bin/grep -e libpthread /proc/$1/maps > /dev/null 2>&1 ; then
backtrace="thread apply all bt"
fi
fi

GDB=${GDB:-/usr/bin/gdb}

if $GDB -nx --quiet --batch --readnever > /dev/null 2>&1; then
readnever=--readnever
else
readnever=
fi

# Run GDB, strip out unwanted noise.
$GDB --quiet $readnever -nx /proc/$1/exe $1 <<EOF 2>&1 |
set width 0
set height 0
set pagination no
$backtrace
EOF
/bin/sed -n \
-e 's/^\((gdb) \)*//' \
-e '/^#/p' \
-e '/^Thread/p'
#end


sudo chmod 777 gstack
sudo cp gstack /usr/bin/


堆栈信息:sudo gstack pid

线程:top -Hp pid

内核

1
https://www.kernel.org/doc/html/v4.10/dev-tools/kmemleak.html

查看内存占用最多的进程:

1
ps -aux | sort -k4nr | head -n 5

内存使用

1
2
3
4
sudo su
cat /proc/meminfo
cat /proc/zoneinfo

树状以及块存储结构

1
2
3
pstree -h
sudo dumpe2fs /dev/sda1
dumpe2fs /dev/sda1 | grep -i "block size"

句柄查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sudo su

总文件句柄数
cat /proc/sys/fs/file-max

修改总:sysctl -w fs.file-max=(成倍增加)
当前文件句柄数
cat /proc/sys/fs/file-nr

pmap ${pid}

ulimit -n

ulimit -HSn 2048

lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more

lsof |grep -i deleted

sudo lsof -p 9268

cd /proc/9268/fd

ls | wc -l

ls -al /proc/9689/fd/ | wc -l

更新本地软件库

1
2
3
更新本地软件列表
sudo apt update
sudo apt search 名称

查看IO、CPU利用率

1
2
top -Hp pid
vmstat 2 10000
1
2
3
4
5
虽然同是写数据,写磁盘跟写文件的现象还是不同的。写磁盘时(也就是 bo 大于  0 时),Buffer 和 Cache 都在增长,但显然 Buffer 的增长快得多。

这说明,写磁盘用到了大量的Buffer,这跟我们在文档中查到的定义是一样的。

对比两个案例,我们发现,写文件时会用到 Cache 缓存数据,而写磁盘则会用到 Buffer来缓存数据。所以,回到刚刚的问题,虽然文档上只提到,Cache是文件读的缓存,但实际上,Cache 也会缓存写文件时的数据。

swap

Swap分区(也称交换分区)是硬盘上的一个区域,被指定为操作系统可以临时存储数据的地方,这些数据不能再保存在RAM中。 基本上,这使您能够增加服务器在工作“内存”中保留的信息量,但有一些注意事项,主要是当RAM中没有足够的空间容纳正在使用的应用程序数据时,将使用硬盘驱动器上的交换空间。 写入磁盘的信息将比保存在RAM中的信息慢得多,但是操作系统更愿意将应用程序数据保存在内存中,并使用交换旧数据。 总的来说,当系统的RAM耗尽时,将交换空间作为回落空间可能是一个很好的安全网,可防止非SSD存储系统出现内存不足的情况。

内存不够用?在Linux上使用swapfile配置交换空间

查看系统是否有交换分区:

1
sudo swapon --show

临时修改方法如下:

1
2
3
4
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap -f /swapfile
sudo swapon /swapfile

经过测试,OpenSuSE系统要使用以下命令才能成功创建swapfile

1
sudo dd if=/dev/zero of=/swapfile count=4096 bs=1MiB

使用以下命令查看是否正确创建。

1
ls -lh /swapfile

结果应该类似下面这样:

1
-rw-r--r-- 1 root root 4.0G Apr 26 17:04 /swapfile

修改swapfile权限

1
sudo chmod 600 /swapfile

查看效果

1
ls -lh /swapfile

结果应该类似下面这样:

1
-rw------- 1 root root 8.0G Apr 26 17:04 /swapfile

激活交换空间

1
2
sudo mkswap /swapfile
sudo swapon /swapfile

之后使用以下命令查看使用成功开启交换空间:

1
sudo swapon --show

结果类似下面这样:

1
2
NAME      TYPE SIZE USED PRIO
/swapfile file 8G 0B -1

添加到fstab

这样每次开机系统就会自动吧swapfile挂载为交换空间。 首先请自行备份fstab文件。 然后把以下配置添加到fstab文件末尾。

1
/swapfile none swap sw 0 0

或者直接使用以下命令:

1
2
3
4
5
6
7
8
9
10
sudo cp /etc/fstab /etc/fstab.bak

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

#关闭swapfile区
sudo swapoff /swapfile

sudo rm /swapfile


缓存释放

如果你使用过 drop_cache 来释放 inode 的话,应该会清楚它有几个控制选项,我们可以通过写入不同的数值来释放不同类型的 cache(用户数据 Page Cache,内核数据 Slab,或者二者都释放),这些选项你可以去看Kernel Documentation(https://www.kernel.org/doc/Documentation/sysctl/vm.txt) 的描述。

image.png

于是这样就引入了一个容易被我们忽略的问题:当我们执行 echo 2 来 drop slab 的时候,它也会把 Page Cache 给 drop 掉,很多运维人员都会忽视掉这一点。

在系统内存紧张的时候,运维人员或者开发人员会想要通过 drop_caches 的方式来释放一些内存,但是由于他们清楚 Page Cache 被释放掉会影响业务性能,所以就期望只去 drop slab 而不去 drop pagecache。于是很多人这个时候就运行 echo 2 > /proc/sys/vm/drop_caches,但是结果却出乎了他们的意料:Page Cache 也被释放掉了,业务性能产生了明显的下降。

由于 drop_caches 是一种内存事件,内核会在 /proc/vmstat 中来记录这一事件,所以我们可以通过 /proc/vmstat 来判断是否有执行过 drop_caches。

1
2
3
$ grep drop /proc/vmstat
drop_pagecache 3
drop_slab 2

如上所示,它们分别意味着 pagecache 被 drop 了 3 次(通过 echo 1 或者 echo 3),slab 被 drop 了 2 次(通过 echo 2 或者 echo 3)。如果这两个值在问题发生前后没有变化,那就可以排除是有人执行了 drop_caches;否则可以认为是因为 drop_caches 引起的 Page Cache 被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#清理文件页、目录项、Inodes等各种缓存
echo 3 > /proc/sys/vm/drop_caches

#然后运行dd命令随机读取设备,向磁盘分区/dev/sdb1写入2G数据
dd if=/dev/urandom of=/dev/sdb1 bs=1M count=2048


sudo su

vi /root/cache.sh


#! /bin/bash
#v1.0
sync
echo 3 > /proc/sys/vm/drop_caches
swapoff -a && swapon -a



crontab -e
*/2 * * * * /root/cache.sh

内存泄露排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
占用线程的内存文件
cat /proc/31108/smaps
sudo su
cat /proc/meminfo

其中 vmalloc 申请的内存会体现在 VmallocUsed 这一项中,即已使用的 Vmalloc 区大小;而 kmalloc 申请的内存则是体现在 Slab 这一项中,它又分为两部分,其中 SReclaimable 是指在内存紧张的时候可以被回收的内存,而 SUnreclaim 则是不可以被回收只能主动释放的内存。

如果 /proc/meminfo 中内核内存(比如 VmallocUsed 和 SUnreclaim)太大,那很有可能发生了内核内存泄漏;

另外,你也可以周期性地观察 VmallocUsed 和 SUnreclaim 的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏。

这也可以通过 /proc 来查看,所以再次强调一遍,当你不清楚该如何去分析时,你可以试着去查看 /proc 目录下的文件。以上面的程序为例,安装 kmem_test 这个内核模块后,我们可以通过 /proc/vmallocinfo 来看到该模块的内存使用情况:


$ cat /proc/vmallocinfo | grep appName

vmstat 2 10000中看free的变化速度(迅速下降,但buffer、cache没发生变化,存在泄露)

strace -t -f -p 31108 -o 31108.strace

cat 31108.strace | grep 10489856


image.png

内存回收

(1)代码区——–主要存储程序代码指令,define定义的常量。

(2)全局数据区——主要存储全局变量(常量),静态变量(常量),常量字符串。

(3)栈区——–主要存储局部变量,栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但内存大小有限。

(4)堆区——–由malloc,calloc分配的内存区域,其生命周期由free决定。堆的内存大小是由程序员分配的,理论上可以占据系统中的所有内存。

当发生了内存泄漏时,或者运行了大内存的应用程序,导致系统的内存资源紧张时,系统又会如何应对呢?

这其实会导致两种可能结果,内存回收和OOM杀死进程。我们先来看后一个可能结果,内存资源紧张导致的 OOM(Out OfMemory),相对容易理解,指的是系统杀死占用大量内存的进程,释放这些内存,再分配给其他更需要的进程。

这一点我们前面详细讲过,这里就不再重复了。接下来再看第一个可能的结果,内存回收,也就是系统释放掉可以回收的内存,比如我前面讲过的缓存和缓冲区,就属于可回收内存。它们在内存管理中,通常被叫做文件页(File-backed Page)。

大部分文件页,都可以直接回收,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。

这些脏页,一般可以通过两种方式写入磁盘。

  • 可以在应用程序中,通过系统调用 fsync ,把脏页同步到磁盘中;
  • 也可以交给系统,由内核线程 pdflush 负责这些脏页的刷新。(内核使用pdflush线程刷新脏页到磁盘,pdflush线程个数在2和8之间,可以通过/proc/sys/vm/nr_pdflush_threads文件直接查看,具体策略机制参看源码函数__pdflush。)

kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:

1
2
3
4
5
vm.dirty_background_bytes=0
vm.dirty_background_ratio=10
vm.dirty_bytes=0
vm.dirty_expire_centisecs=3000
vm.dirty_ratio=20

调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。

至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果你可以通过 /proc/vmstat 来查看:

1
2
3
grep "nr_dirty_" /proc/vmstat
nr_dirty_threshold 366998
nr_dirty_background_threshold 183275

你可以观察一下调整前后这两项的变化。这里我要给你一个避免踩坑的提示,解决该方案中的设置项如果设置不妥会触发一个内核 Bug,这是我在 2017 年进行性能调优时发现的一个内核 Bug,我给社区提交了一个 patch 将它 fix 掉了,具体的 commit 见 writeback: schedule periodic writeback with sysctl , commit log 清晰地描述了该问题,我建议你有时间看一看。

Page Cache是怎样产生和释放的

Page Cache 的产生有两种不同的方式:

  • Buffered I/O(标准 I/O);
  • Memory-Mapped I/O(存储映射 I/O)。

标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read(2)) 话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。

对于存储映射 I/O 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接读写 Pagecache Page 中内容。

1
2
3
4
cat /proc/vmstat | egrep "dirty|writeback"

nr_dirty 40
nr_writeback 2

如上所示,nr_dirty 表示当前系统中积压了多少脏页,nr_writeback 则表示有多少脏页正在回写到磁盘中,他们两个的单位都是 Page(4KB)。

释放

free 命令中的 buff/cache 中的这些就是“活着”的 Page Cache,那它们什么时候会“死亡”(被回收)呢?我们来看一张图:

应用在申请内存的时候,即使没有 free 内存,只要还有足够可回收的 Page Cache,就可以通过回收 Page Cache 的方式来申请到内存,回收的方式主要是两种:直接回收和后台回收

那它是具体怎么回收的呢?你要怎么观察呢?其实在我看来,观察 Page Cache 直接回收和后台回收最简单方便的方式是使用 sar:

1
2
3
4
5
6
7
8
9
10
11
12
$ sar -r 1

$ sar -B 1
02:14:01 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff


02:14:01 PM 0.14 841.53 106745.40 0.00 41936.13 0.00 0.00 0.00 0.00
02:15:01 PM 5.84 840.97 86713.56 0.00 43612.15 717.81 0.00 717.66 99.98
02:16:01 PM 95.02 816.53 100707.84 0.13 46525.81 3557.90 0.00 3556.14 99.95
02:17:01 PM 10.56 901.38 122726.31 0.27 54936.13 8791.40 0.00 8790.17 99.99
02:18:01 PM 108.14 306.69 96519.75 1.15 67410.50 14315.98 31.48 14319.38 99.80
02:19:01 PM 5.97 489.67 88026.03 0.18 48526.07 1061.53 0.00 1061.42 99.99

借助上面这些指标,你可以更加明确地观察内存回收行为,下面是这些指标的具体含义:

  • pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。
  • pgscand/s: Application 在内存申请过程中每秒直接扫描的 page 个数。
  • pgsteal/s: 扫描的 page 中每秒被回收的个数。
  • %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。

**
进程运行所需要的内存类型有很多种,总的来说,这些内存类型可以从是不是文件映射,以及是不是私有内存这两个不同的维度来做区分,也就是可以划分为四类内存。**

  • 私有匿名内存。进程的堆、栈,以及 mmap(MAP_ANON | MAP_PRIVATE) 这种方式申请的内存都属于这种类型的内存。其中栈是由操作系统来进行管理的,应用程序无需关注它的申请和释放;堆和私有匿名映射则是由应用程序(程序员)来进行管理的,它们的申请和释放都是由应用程序来负责的,所以它们是容易产生内存泄漏的地方。

  • 共享匿名内存。进程通过 mmap(MAP_ANON | MAP_SHARED) 这种方式来申请的内存,比如说 tmpfs 和 shm。这个类型的内存也是由应用程序来进行管理的,所以也可能会发生内存泄漏。

  • 私有文件映射。进程通过 mmap(MAP_FILE | MAP_PRIVATE) 这种方式来申请的内存,比如进程将共享库(Shared libraries)和可执行文件的代码段(Text Segment)映射到自己的地址空间就是通过这种方式。对于共享库和可执行文件的代码段的映射,这是通过操作系统来进行管理的,应用程序无需关注它们的申请和释放。而应用程序直接通过 mmap(MAP_FILE | MAP_PRIVATE) 来申请的内存则是需要应用程序自己来进行管理,这也是可能会发生内存泄漏的地方。

  • 共享文件映射。进程通过 mmap(MAP_FILE | MAP_SHARED) 这种方式来申请的内存,我们在上一个模块课程中讲到的 File Page Cache 就属于这类内存。这部分内存也需要应用程序来申请和释放,所以也存在内存泄漏的可能性。

总结

  • 进程直接读写的都是虚拟地址,虚拟地址最终会通过 Paging(分页)来转换为物理内存的地址,Paging 这个过程是由内核来完成的。
  • 进程的内存类型可以从 anon(匿名)与 file(文件)、private(私有)与 shared(共享)这四项来区分为 4 种不同的类型,进程相关的所有内存都是这几种方式的不同组合。
  • 查看进程内存时,可以先使用 top 来看系统中各个进程的内存使用概况,再使用 pmap 去观察某个进程的内存细节。

直接内存回收是指在进程上下文同步进行内存回收,那么它具体是怎么引起 load 飙高的呢?

因为直接内存回收是在进程申请内存的过程中同步进行的回收,而这个回收过程可能会消耗很多时间,进而导致进程的后续行为都被迫等待,这样就会造成很长时间的延迟,以及系统的 CPU 利用率会升高,最终引起 load 飙高。

那么,针对直接内存回收引起 load 飙高或者业务 RT 抖动的问题,一个解决方案就是及早地触发后台回收来避免应用程序进行直接内存回收,那具体要怎么做呢?那么,我们可以增大 min_free_kbytes 这个配置选项来及早地触发后台回收,该选项最终控制的是内存回收水位

系统中脏页过多引起 load 飙高

那如何解决这类问题呢?一个比较省事的解决方案是控制好系统中积压的脏页数据。很多人知道需要控制脏页,但是往往并不清楚如何来控制好这个度,脏页控制的少了可能会影响系统整体的效率,脏页控制的多了还是会触发问题,所以我们接下来看下如何来衡量好这个“度”。

kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:

1
2
3
4
5
vm.dirty_background_bytes=0
vm.dirty_background_ratio=10
vm.dirty_bytes=0
vm.dirty_expire_centisecs=3000
vm.dirty_ratio=20

调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。

至于这些值调整大多少比较合适,也是因系统和业务的不同而异,我的建议也是一边调整一边观察,将这些值调整到业务可以容忍的程度就可以了,即在调整后需要观察业务的服务质量 (SLA),要确保 SLA 在可接受范围内。调整的效果你可以通过 /proc/vmstat 来查看:

1
2
3
grep "nr_dirty_" /proc/vmstat
nr_dirty_threshold 366998
nr_dirty_background_threshold 183275

你可以观察一下调整前后这两项的变化。这里我要给你一个避免踩坑的提示,解决该方案中的设置项如果设置不妥会触发一个内核 Bug,这是我在 2017 年进行性能调优时发现的一个内核 Bug,我给社区提交了一个 patch 将它 fix 掉了,具体的 commit 见 writeback: schedule periodic writeback with sysctl , commit log 清晰地描述了该问题,我建议你有时间看一看。

系统 NUMA 策略配置不当引起的 load 飙高

除了我前面提到的这两种引起系统 load 飙高或者业务延迟抖动的场景之外,还有另外一种场景也会引起 load 飙高,那就是系统 NUMA 策略配置不当引起的 load 飙高。

比如说,我们在生产环境上就曾经遇到这样的问题:系统中还有一半左右的 free 内存,但还是频频触发 direct reclaim,导致业务抖动得比较厉害。后来经过排查发现是由于设置了 zone_reclaim_mode,这是 NUMA 策略的一种。

设置 zone_reclaim_mode 的目的是为了增加业务的 NUMA 亲和性,但是在实际生产环境中很少会有对 NUMA 特别敏感的业务,这也是为什么内核将该配置从默认配置 1 修改为了默认配置 0: mm: disable zone_reclaim_mode by default ,配置为 0 之后,就避免了在其他 node 有空闲内存时,不去使用这些空闲内存而是去回收当前 node 的 Page Cache,也就是说,通过减少内存回收发生的可能性从而避免它引发的业务延迟。

那么如何来有效地衡量业务延迟问题是否由 zone reclaim 引起的呢?它引起的延迟究竟有多大呢?这个衡量和观察方法也是我贡献给 Linux Kernel 的:mm/vmscan: add tracepoints for node reclaim ,大致的思路就是利用 linux 的 tracepoint 来做这种量化分析,这是性能开销相对较小的一个方案。

推荐将 zone_reclaim_mode 配置为 0。vm.zone_reclaim_mode = 0因为相比内存回收的危害而言,NUMA 带来的性能提升几乎可以忽略,所以配置为 0,利远大于弊。

好了,对于 Page Cache 管理不当引起的系统 load 飙高和业务时延抖动问题,我们就分析到这里,希望通过这篇的学习,在下次你遇到直接内存回收引起的 load 飙高问题时不再束手无策。

总的来说,这些问题都是 Page Cache 难以释放而产生的问题,那你是否想过,是不是 Page Cache 很容易释放就不会产生问题了?这个答案可能会让你有些意料不到:Page Cache 容易释放也有容易释放的问题。这到底是怎么回事呢,我们下节课来分析下这方面的案例。

内核机制引起 Page Cache 被回收而产生的业务性能下降

我简单来解释一下这个图。Reclaimer 是指回收者,它可以是内核线程(包括 kswapd)也可以是用户线程。回收的时候,它会依次来扫描 pagecache page 和 slab page 中有哪些可以被回收的,如果有的话就会尝试去回收,如果没有的话就跳过。在扫描可回收 page 的过程中回收者一开始扫描的较少,然后逐渐增加扫描比例直至全部都被扫描完。这就是内存回收的大致过程。

image.png

接下来我所要讲述的案例就发生在“relcaim slab”中,我们从前一个案例已然知道,如果 inode 被回收的话,那么它对应的 Page Cache 也都会被回收掉,所以如果业务进程读取的文件对应的 inode 被回收了,那么该文件所有的 Page Cache 都会被释放掉,这也是容易引起性能问题的地方。

那这个行为是否有办法观察?这同样也是可以通过 /proc/vmstat 来观察的,/proc/vmstat 简直无所不能(这也是为什么我会在之前说内核开发者更习惯去观察 /proc/vmstat)。

1
2
3
$ grep inodesteal /proc/vmstat
pginodesteal 114341
kswapd_inodesteal 1291853

这个行为对应的事件是 inodesteal,就是上面这两个事件,其中 kswapd_inodesteal 是指在 kswapd 回收的过程中,因为回收 inode 而释放的 pagecache page 个数;pginodesteal 是指 kswapd 之外其他线程在回收过程中,因为回收 inode 而释放的 pagecache page 个数。所以在你发现业务的 Page Cache 被释放掉后,你可以通过观察来发现是否因为该事件导致的。

如何避免 Page Cache 被回收而引起的性能问题?

我们在分析一些问题时,往往都会想这个问题是我的模块有问题呢,还是别人的模块有问题。也就是说,是需要修改我的模块来解决问题还是需要修改其他模块来解决问题。与此类似,避免 Page Cache 里相对比较重要的数据被回收掉的思路也是有两种:

  • 从应用代码层面来优化;
  • 从系统层面来调整。

从应用程序代码层面来解决是相对比较彻底的方案,因为应用更清楚哪些 Page Cache 是重要的,哪些是不重要的,所以就可以明确地来对读写文件过程中产生的 Page Cache 区别对待。比如说,对于重要的数据,可以通过 mlock(2) 来保护它,防止被回收以及被 drop;对于不重要的数据(比如日志),那可以通过 madvise(2) 告诉内核来立即释放这些 Page Cache。

在有些情况下,对应用程序而言,修改源码是件比较麻烦的事,如果可以不修改源码来达到目的那就最好不过了。Linux 内核同样实现了这种不改应用程序的源码而从系统层面调整来保护重要数据的机制,这个机制就是 memory cgroup protection。

它大致的思路是,将需要保护的应用程序使用 memory cgroup 来保护起来,这样该应用程序读写文件过程中所产生的 Page Cache 就会被保护起来不被回收或者最后被回收。memory cgroup protection 大致的原理如下图所示:

image.png

如上图所示,memory cgroup 提供了几个内存水位控制线 memory.{min, low, high, max} 。

  • memory.max这是指 memory cgroup 内的进程最多能够分配的内存,如果不设置的话,就默认不做内存大小的限制。
  • memory.high如果设置了这一项,当 memory cgroup 内进程的内存使用量超过了该值后就会立即被回收掉,所以这一项的目的是为了尽快的回收掉不活跃的 Page Cache。
  • memory.low这一项是用来保护重要数据的,当 memory cgroup 内进程的内存使用量低于了该值后,在内存紧张触发回收后就会先去回收不属于该 memory cgroup 的 Page Cache,等到其他的 Page Cache 都被回收掉后再来回收这些 Page Cache。
  • memory.min这一项同样是用来保护重要数据的,只不过与 memoy.low 有所不同的是,当 memory cgroup 内进程的内存使用量低于该值后,即使其他不在该 memory cgroup 内的 Page Cache 都被回收完了也不会去回收这些 Page Cache,可以理解为这是用来保护最高优先级的数据的。

那么,如果你想要保护你的 Page Cache 不被回收,你就可以考虑将你的业务进程放在一个 memory cgroup 中,然后设置 memory.{min,low} 来进行保护;与之相反,如果你想要尽快释放你的 Page Cache,那你可以考虑设置 memory.high 来及时的释放掉不活跃的 Page Cache。

除了缓存和缓冲区,通过内存映射获取的文件映射页,也是一种常见的文件页。它也可以被释放掉,下次再访问的时候,从文件重新读取。

除了文件页外,还有没有其他的内存可以回收呢?比如,应用程序动态分配的堆内存,也就是我们在内存管理中说到的匿名页(Anonymous Page),是不是也可以回收呢?

我想,你肯定会说,它们很可能还要再次被访问啊,当然不能直接回收了。非常正确,这些内存自然不能直接释放。

但是,如果这些内存在分配后很少被访问,似乎也是一种资源浪费。是不是可以把它们暂时先存在磁盘里,释放内存给其他更需要的进程?

其实,这正是 Linux 的Swap机制。Swap把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

在前几节的案例中,我们已经分别学过缓存和 OOM 的原理和分析。那 Swap 又是怎么工作的呢?因为内容比较多,接下来,我将用两节课的内容,带你探索 Swap 的工作原理,以及 Swap 升高后的分析方法。

今天我们先来看看,Swap 究竟是怎么工作的。

Swap 原理

Swap 说白了就是把一块磁盘空间或者一个本地文件(以下讲解以磁盘为例),当成内存来使用。它包括换出和换入两个过程。

  • 所谓换出,就是把进程暂时不用的内存(swap)数据存储到磁盘中,并释放这些数据占用的内存。

  • 而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存(swap)中来。

所以你看,Swap 其实是把系统的可用内存变大了。这样,即使服务器的内存不足,也可以运行大内存的应用程序。

还记得我最早学习 Linux操作系统时,内存实在太贵了,一个普通学生根本就用不起大的内存,那会儿我就是开启了Swap来运行Linux桌面。当然,现在的内存便宜多了,服务器一般也会配置很大的内存,那是不是说Swap就没有用武之地了呢?

当然不是。事实上,内存再大,对应用程序来说,也有不够用的时候。

一个很典型的场景就是,即使内存不足时,有些应用程序也并不想被 OOM 杀死,而是希望能缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它。

除此之外,我们常见的笔记本电脑的休眠和快速开机的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。这样就省去了很多应用程序的初始化过程,加快了开机速度。

话说回来,既然 Swap 是为了回收内存,那么Linux到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢?

一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内存(比如前面提到的缓存),进而尽可能地满足新内存请求。这个过程通常被称为直接内存回收。

除了直接内存回收,还有一个专门的内核线程用来定期回收内存,也就是 kswapd0。为了衡量内存的使用情况,kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是

页最小阈值(pages_min)、页低阈值(pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示。

kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。

  • 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
  • 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内存回收,直到剩余内存大于高阈值为止。
  • 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
  • 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。

我们可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最小阈值计算生成的,计算方法如下 :

1
2
pages_low = pages_min*5/4
pages_high = pages_min*3/2

NUMA 与 Swap

很多情况下,你明明发现了Swap升高,可是在分析系统的内存使用时,却很可能发现,系统剩余内存还多着呢。为什么剩余内存很多的情况下,也会发生 Swap 呢?

看到上面的标题,你应该已经想到了,这正是处理器的 NUMA (Non-Uniform Memory Access)架构导致的。

关于 NUMA,我在 CPU 模块中曾简单提到过。在 NUMA 架构下,多个处理器被划分到不同 Node 上,且每个 Node 都拥有自己的本地内存空间。

而同一个 Node 内部的内存空间,实际上又可以进一步分为不同的内存域(Zone),比如直接内存访问区(DMA)、普通内存区(NORMAL)、伪内存区(MOVABLE)等,如下图所示:

先不用特别关注这些内存域的具体含义,我们只要会查看阈值的配置,以及缓存、匿名页的实际使用情况就够了。

既然 NUMA 架构下的每个 Node 都有自己的本地内存空间,那么,在分析内存的使用时,我们也应该针对每个 Node 单独分析。

你可以通过 numactl 命令,来查看处理器在 Node 的分布情况,以及每个 Node 的内存使用情况。比如,下面就是一个 numactl 输出的示例:

1
2
3
4
5
6
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1
node 0 size: 7977 MB
node 0 free: 4416 MB
...

这个界面显示,我的系统中只有一个 Node,也就是 Node 0 ,而且编号为 0 和 1 的两个 CPU, 都位于 Node 0 上。另外,Node 0 的内存大小为 7977 MB,剩余内存为 4416 MB。

了解了 NUNA 的架构和 NUMA 内存的查看方法后,你可能就要问了这跟 Swap 有什么关系呢?

实际上,前面提到的三个内存阈值(页最小阈值、页低阈值和页高阈值),都可以通过内存域在 proc 文件系统中的接口 /proc/zoneinfo 来查看。

比如,下面就是一个 /proc/zoneinfo 文件的内容示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

$ cat /proc/zoneinfo
...
Node 0, zone Normal
pages free 227894
min 14896
low 18620
high 22344
...
nr_free_pages 227894
nr_zone_inactive_anon 11082
nr_zone_active_anon 14024
nr_zone_inactive_file 539024
nr_zone_active_file 923986
...

这个输出中有大量指标,我来解释一下比较重要的几个。

  • pages 处的 min、low、high,就是上面提到的三个内存阈值,而 free 是剩余内存页数,它跟后面的 nr_free_pages 相同。

  • nr_zone_active_anon 和 nr_zone_inactive_anon,分别是活跃和非活跃的匿名页数。

  • nr_zone_active_file 和 nr_zone_inactive_file,分别是活跃和非活跃的文件页数。

从这个输出结果可以发现,剩余内存远大于页高阈值,所以此时的 kswapd0 不会回收内存。

当然,某个 Node 内存不足时,系统可以从其他 Node 寻找空闲内存,也可以从本地内存中回收内存。具体选哪种模式,你可以通过 /proc/sys/vm/zone_reclaim_mode 来调整。它支持以下几个选项:

  • 默认的 0 ,也就是刚刚提到的模式,表示既可以从其他 Node 寻找空闲内存,也可以从本地回收内存。
  • 1、2、4 都表示只回收本地内存,2 表示可以回写脏数据回收内存,4 表示可以用 Swap 方式回收内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vm.zone_reclaim_mode

设置方法:

echo 0 > /proc/sys/vm/zone_reclaim_mode,或

sysctl -w vm.zone_reclaim_mode=0,或

编辑/etc/sysctl.conf文件,加入vm.zone_reclaim_mode=0

# 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方式回收内存。

swappiness

到这里,我们就可以理解内存回收的机制了。这些回收的内存既包括了文件页,又包括了匿名页。

  • 对文件页的回收,当然就是直接回收缓存,或者把脏页写回磁盘后再回收。
  • 而对匿名页的回收,其实就是通过 Swap 机制,把它们写入磁盘后再释放内存。

不过,你可能还有一个问题。既然有两种不同的内存回收机制,那么在实际回收内存时,到底该先回收哪一种呢?

其实,Linux 提供了一个 /proc/sys/vm/swappiness 选项,用来调整使用 Swap 的积极程度。

swappiness 的范围是 0-100,数值越大,越积极使用 Swap,也就是更倾向于回收匿名页;数值越小,越消极使用 Swap,也就是更倾向于回收文件页。

虽然 swappiness 的范围是 0-100,不过要注意,这并不是内存的百分比,而是调整 Swap 积极程度的权重,即使你把它设置成 0,当剩余内存 + 文件页小于页高阈值(https://www.kernel.org/doc/Documentation/sysctl/vm.txt)时,还是会发生 Swap。

清楚了 Swap 原理后,当遇到 Swap 使用变高时,又该怎么定位、分析呢?别急,下一节,我们将用一个案例来探索实践。

小结

在内存资源紧张时,Linux 通过直接内存回收和定期扫描的方式,来释放文件页和匿名页,以便把内存分配给更需要的进程使用。

  • 文件页的回收比较容易理解,直接清空,或者把脏数据写回磁盘后再释放。
  • 而对匿名页的回收,需要通过 Swap 换出到磁盘中,下次访问时,再从磁盘换入到内存中。

你可以设置 /proc/sys/vm/min_free_kbytes,来调整系统定期回收内存的阈值(也就是页低阈值),还可以设置 /proc/sys/vm/swappiness,来调整文件页和匿名页的回收倾向。

在 NUMA 架构下,每个 Node都有自己的本地内存空间,而当本地内存不足时,默认既可以从其他 Node 寻找空闲内存,也可以从本地内存回收。

你可以设置 /proc/sys/vm/zone_reclaim_mode 来调整 NUMA 本地内存的回收策略。

程序被oom-kill

很快系统内存就会被耗尽,进而触发 OOM killer 去杀进程。这个信息可以通过 dmesg(该命令是用来查看内核日志的)这个命令来查看:

1
2
3
4
5
6
7
dmesg

oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止 OOM。

echo -17 > /proc/$(pidof ele-vue)/oom_adj

$ sudo sh -c "echo -17 > /proc/$(pidof ele-vue)/oom_adj"

开启 Swap 后,你可以设置 /proc/sys/vm/min_free_kbytes来调整系统定期回收内存的阈值,也可以设置 /proc/sys/vm/swappiness ,来调整文件页和匿名页的回收倾向。

1
echo 0 >/proc/sys/vm/swappiness

永久修改:

在 /etc/sysctl.conf 文件添加 ”vm.swappiness=0” 行

脏页(应用程序修改过但暂时未写入磁盘的数据)的数据处理(启用内核线程 pdflush 负责这些脏页的刷新)/proc/sys/vm/nr_pdflush_threads

1
vm.nr_pdflush_threads=2

kbdirty 就是系统中的脏页大小,它同样也是对 /proc/vmstat 中 nr_dirty 的解析。你可以通过调小如下设置来将系统脏页个数控制在一个合理范围:

1
2
3
4
5
vm.dirty_background_bytes=0
vm.dirty_background_ratio=10
vm.dirty_bytes=0
vm.dirty_expire_centisecs=3000
vm.dirty_ratio=20

调整这些配置项有利有弊,调大这些值会导致脏页的积压,但是同时也可能减少了 I/O 的次数,从而提升单次刷盘的效率;调小这些值可以减少脏页的积压,但是同时也增加了 I/O 的次数,降低了 I/O 的效率。

1
2
3
4
5
6
7
8
9
10
vm.min_free_kbytes=409600(来调整系统定期回收内存的阈值)

vm.vfs_cache_pressure=200
#加大这个参数设置了虚拟内存回收directory和i-node缓冲的倾向,这个值越大,回收的倾向越严重。调整这3个参数的目的就是让操作系统在平时就尽快回收缓冲,释放物理内存,这样就可以避免突发性的大规模换页。

vm.overcommit_memory=1(表示即使内存耗尽也不杀死任何进程)


#查看
sysctl -a
打赏

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2020-2023 交个朋友之猿天地
  • Powered By Hexo | Title - Nothing
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信