前言

我目前在使用pve作为我的虚拟化系统,我在pve中开了一个centos7作为我的mc服务器系统

我写了个定时任务使用python每天凌晨3点将磁盘中的游戏数据备份到pve宿主机中的raid5阵列中,但是我发现服务器内存总是爆满,我初步判断是执行备份脚本导致的,后面实际测试后发现确实是

复现

我先手动触发一次备份 (后台备份)

1
nohup python server.py &

等待数据备份完成,在pve中查看服务器内存占用

发现服务器内存已经爆满了

登录服务器查看占用内存进程排行

1
top -o %MEM
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
top - 18:09:07 up  9:45,  1 user,  load average: 0.84, 0.36, 0.19
Tasks: 241 total, 2 running, 239 sleeping, 0 stopped, 0 zombie
%Cpu(s): 4.1 us, 4.8 sy, 0.0 ni, 90.6 id, 0.5 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 41034536 total, 17324592 free, 7754368 used, 15955576 buff/cache
KiB Swap: 3145724 total, 3135220 free, 10504 used. 32879168 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10536 root 20 0 43.0g 6.1g 25104 S 4.5 15.6 111:50.22 java
9140 polkitd 20 0 3191660 451248 21032 S 0.0 1.1 8:47.40 mysqld
8882 root 20 0 2379512 69380 25784 S 0.0 0.2 0:44.79 dockerd
872 root 20 0 920636 51408 18904 S 0.0 0.1 2:13.25 node
848 root 20 0 936892 47088 18456 S 0.0 0.1 1:10.98 node
8864 root 20 0 1440692 39356 14624 S 0.0 0.1 0:32.95 containerd
8802 root 20 0 715916 30604 6252 S 0.0 0.1 19:44.84 nattunnel
8734 root 20 0 723448 26208 4 S 9.1 0.1 51:22.65 frpc
8745 root 20 0 723704 24916 4 S 0.0 0.1 2:59.84 frpc
1122 root 20 0 574288 17340 6012 S 0.0 0.0 0:07.49 tuned
10472 root 20 0 2089844 16204 1760 S 0.0 0.0 9:55.88 docker-proxy
9120 root 20 0 720048 13496 4356 S 0.0 0.0 0:04.49 containerd-shim
10517 root 20 0 720304 13184 4388 S 0.0 0.0 0:04.79 containerd-shim
842 polkitd 20 0 612240 11224 4764 S 0.0 0.0 0:00.39 polkitd
869 root 20 0 626196 11216 6916 S 0.0 0.0 0:03.10 NetworkManager
10450 root 20 0 1489616 8076 1392 S 0.0 0.0 0:00.08 docker-proxy
8326 root 20 0 155288 7584 2368 R 72.7 0.0 1:15.53 python
1 root 20 0 194064 7224 4232 S 0.0 0.0 0:16.54 systemd
9098 root 20 0 1350604 6036 1392 S 0.0 0.0 0:00.21 docker-proxy
9104 root 20 0 1489872 6036 1392 S 0.0 0.0 0:00.08 docker-proxy
10457 root 20 0 1489872 6036 1396 S 0.0 0.0 0:00.08 docker-proxy
10479 root 20 0 1424080 6028 1384 S 0.0 0.0 0:00.07 docker-proxy
599 root 20 0 39056 5972 5648 S 0.0 0.0 0:01.44 systemd-journal
10494 root 20 0 1489872 5752 1152 S 0.0 0.0 0:00.07 docker-proxy
621 root 20 0 127396 5632 2604 S 0.0 0.0 0:00.05 lvmetad
7435 root 20 0 154796 5496 4172 S 0.0 0.0 0:01.03 sshd
635 root 20 0 48392 4864 2880 S 0.0 0.0 0:00.55 systemd-udevd
1123 root 20 0 113004 4376 3344 S 0.0 0.0 0:00.08 sshd
1615 postfix 20 0 90088 4308 3260 S 0.0 0.0 0:00.12 qmgr
3313 postfix 20 0 89912 4104 3092 S 0.0 0.0 0:00.06 pickup
10501 root 20 0 1424080 3984 1392 S 0.0 0.0 0:00.07 docker-proxy
1125 root 20 0 216400 3856 3164 S 0.0 0.0 0:04.15 rsyslogd
850 dbus 20 0 66452 2564 1880 S 0.0 0.0 0:01.13 dbus-daemon
7446 root 20 0 115680 2292 1708 S 0.0 0.0 0:00.39 bash
8436 root 20 0 162272 2272 1532 R 13.6 0.0 0:00.10 top

 首当其冲的就是java进程,我之前预想的是如果是使用python备份,应该是python进程占用太多内存,没想到是java,我想了想极有可能是系统的 buffer/cache 导致内存过高,因为内存爆满发生在进行读写操作之后

验证

1
2
3
4
[root@localhost home]# free -h
total used free shared buff/cache available
Mem: 39G 7.4G 295M 8.7M 31G 31G
Swap: 3.0G 41M 3.0G

buff/cache 竟然有31G 

先了解一下什么是buff/cache?

Buffer cache则主要是设计用来在系统对块设备进行读写的时候,对块进行数据缓存的系统来使用。比如我们在格式化文件系统的时候。

一般情况下两个缓存系统是一起配合使用的,比如当我们对一个文件进行写操作的时候,page cache的内容会被改变,而buffer cache则可以用来将page标记为不同的缓冲区,并记录是哪一个缓冲区被修改了。这样,内核在后续执行脏数据的回写(writeback)时,就不用将整个page写回,而只需要写回修改的部分即可。

Linux内核会在内存将要耗尽的时候,触发内存回收的工作,以便释放出内存给急需内存的进程使用。

既然它主要用来做缓存,只是在内存够用的时候加快进程对文件的读写速度,那么在内存压力较大的情况下,当然有必要清空释放cache,作为free空间分给相关进程使用。所以一般情况下,我们认为buffer/cache空间可以被释放,这个理解是正确的。

但是这种清缓存的工作也并不是没有成本。所以伴随着cache清除的行为的,一般都是系统IO飙高。因为内核要对比cache中的数据和对应硬盘文件上的数据是否一致,如果不一致需要写回,之后才能回收。

尝试释放buffer/cache

sync 命令将所有未写的系统缓冲区写到磁盘中,包含已修改的 i-node、已延迟的块 I/O 和读写映射文件。切记释放前最好sync一下,防止丢数据。

1
sync && echo 1 > /proc/sys/vm/drop_caches

执行之后发现内存已经正常,基本得出是 buffer/cache 中缓存的数据过多导致的内存爆满

执行之后需要将/proc/sys/vm/drop_caches 的值改成原来的0

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

但是我执行这个之后,产生了报错

1
2
[root@localhost home]# echo 0 > /proc/sys/vm/drop_caches
-bash: echo: write error: Invalid argument

在研究这个报错之前我们先了解几个概念

/proc/sys/vm/drop_caches

清除缓存策略:
1:手动清除page cache
2:手动清除slab分配器中的对象(包括目录项和inode)
3:手动清除page cache和slab分配器中的对象

0 是系统使用的,手动调整是不行的,而释放之后  /proc/sys/vm/drop_caches 显示为1

这个虽然是显示1,但是这个只是一种状态 echo 1 > /proc/sys/vm/drop_caches 是一次手动清除的行为,不会影响系统的自动清除内存

不过每次都要手动清除内存比较麻烦,我找到一个脚本

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
#! /bin/bash
# 需要释放内存的,内存使用百分比,可以传参,默认是85%
max_rate=$1
if [ ! "$max_rate" ] ; then
max_rate=85
fi
echo "max_rate: $max_rate"

total=`free -m | awk 'NR==2' | awk '{print $2}'`
used=`free -m | awk 'NR==2' | awk '{print $3}'`
free=`free -m | awk 'NR==2' | awk '{print $4}'`
rate=$(($used*100/$total));
log=/usr/local/logs/mem.log
echo "===========================" >> $log
date >> $log
echo "current_rate: $rate"
echo "Memory usage | [Total:${total}MB][Use:${used}MB][Free:${free}MB]" >> $log
if [ "$rate" -ge "$max_rate" ] ; then
sync && echo 1 > /proc/sys/vm/drop_caches
sync && echo 2 > /proc/sys/vm/drop_caches
sync && echo 3 > /proc/sys/vm/drop_caches
echo "OK" >> $log
else
echo "Not required" >> $log
fi

但是这个脚本是根据总内存来判断的,我使用之后并不能起到效果,我改成了根据 buff/cache 和总内存的百分比来判断是否要进行清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/bash
# 需要释放内存的,缓存占用百分比,可以传参,默认是70%
max_cache_rate=$1
if [ ! "$max_cache_rate" ] ; then
max_cache_rate=70
fi
echo "max_cache_rate: $max_cache_rate"

total_mem=$(free -m | awk 'NR==2{print $2}')
cache_mem=$(free -m | awk 'NR==2{print $6}')
cache_rate=$((cache_mem*100/total_mem))
log=/usr/local/logs/mem.log
echo "===========================" >> $log
date >> $log
echo "current_cache_rate: $cache_rate%"
echo "Memory usage | [Total:${total_mem}MB][Cache:${cache_mem}MB]" >> $log
if [ "$cache_rate" -ge "$max_cache_rate" ] ; then
sync && echo 1 > /proc/sys/vm/drop_caches
sync && echo 2 > /proc/sys/vm/drop_caches
sync && echo 3 > /proc/sys/vm/drop_caches
echo "OK, cache memory released" >> $log
else
echo "Not required to release cache memory" >> $log
fi

每隔30分钟运行一次

1
*/30 * * * * /usr/bin/sh /home/mem.sh

其他的解决方法
通过这个问题对这个buff/cache 有了了解,我就思考:

linux系统应该可以配置不使用buff/cache吧?

buff/cache 的系统自动回收,是不是也是可以配置自动回收的上限呢?

等等问题。我就找到了以下内容:

修改/etc/sysctl.conf 添加如下选项后就不会内存持续增加

1
2
3
4
5
6
7
8
9
10
vm.dirty_ratio = 1
vm.dirty_background_ratio=1
vm.dirty_writeback_centisecs=2
vm.dirty_expire_centisecs=3
vm.drop_caches=3
vm.swappiness =100
vm.vfs_cache_pressure=163
vm.overcommit_memory=2
vm.lowmem_reserve_ratio=32 32 8
kern.maxvnodes=3

上面的设置比较粗暴,使cache的作用基本无法发挥。

介绍一下具体的配置项:

dirty_ratio

这个参数控制文件系统的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,表示当写缓冲使用到系统内存多少的时候,开始向磁盘写出数 据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值,一般启动上缺省是 10。设1加速程序速度;

dirty_background_ratio

这个参数控制文件系统的pdflush进程,在何时刷新磁盘。单位是百分比,表示系统内存的百分比,意思是当写缓冲使用到系统内存多少的时 候,pdflush开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时, 应该降低其数值,一般启动上缺省是 5;

dirty_writeback_centisecs

这个参数控制内核的脏数据刷新进程pdflush的运行间隔。单位是 1/100 秒。缺省数值是500,也就是 5 秒。如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操作;

dirty_expire_centisecs

这个参数声明Linux内核写缓冲区里面的数据多“旧”了之后,pdflush进程就开始考虑写到磁盘中去。单位是 1/100秒。缺省是 30000,也就是 30 秒的数据就算旧了,将会刷新磁盘。对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致IO提高太快。建议设置为 1500,也就是15秒算旧。 

drop_caches

释放已经使用的cache;

page-cluster

该文件表示在写一次到swap区的时候写入的页面数量,0表示1页,1表示2页,2表示4页。

swapiness

该文件表示系统进行交换行为的程度,数值(0-100)越高,越可能发生磁盘交换。

vfs_cache_pressure

该文件表示内核回收用于directory和inode cache内存的倾向

1
2
3
4
5
6
7
vm.dirty_ratio = 5    #dft 20  %
vm.dirty_background_ratio =5 #dft 10 %
vm.dirty_writeback_centisecs=100 #dft 500 is 5s
vm.dirty_expire_centisecs=300 #dft 30000 is 30s
vm.drop_caches=3 #dft 0
vm.swappiness=100 #dft 60
vm.vfs_cache_pressure=133 #dft 100

目前我在使用的是脚本的方法,其他方法大家也可以试试