redis问题之Can‘t save in background: fork: Cannot allocate memory

问题现象:某项目压测时,半小时后发现交易批量失败。

  • 查看业务日志发现 redis失去连接

2021/04/01 14:00:11 [error] [exception.Predis\Connection\ConnectionException] exception 'Predis\Connection\ConnectionException' with message 'Connection timed out [tcp://127.0.0.1:6379]' in
 /opt/www/source/integration/Predis/Connection/AbstractConnection.php:146
Stack trace:
#0 /opt/www/source/integration/Predis/Connection/StreamConnection.php(85): Predis\Connection\AbstractConnection->onConnectionError('Connection time...', 110)
................................
 

  • 去redis容器中查看redis日志,发现大量打印" Can't save in background: fork: Cannot allocate memory"错误信息:如下

 [12] 01 Apr 12:02:38.008 # Can't save in background: fork: Cannot allocate memory
[12] 01 Apr 12:02:44.016 * 1 changes in 900 seconds. Saving...
[12] 01 Apr 12:02:44.016 # Can't save in background: fork: Cannot allocate memory
[12] 01 Apr 12:02:50.025 * 1 changes in 900 seconds. Saving...
[12] 01 Apr 12:02:50.025 # Can't save in background: fork: Cannot allocate memory
[12] 01 Apr 12:02:56.039 * 1 changes in 900 seconds. Saving...
[12] 01 Apr 12:02:56.039 # Can't save in background: fork: Cannot allocate memory
[12] 01 Apr 12:03:02.061 * 1 changes in 900 seconds. Saving...
[12] 01 Apr 12:03:02.061 # Can't save in background: fork: Cannot allocate memory

问题分析:

redis.conf配置文件中有个默认的选项:

stop-writes-on-bgsave-error yes

 这个选项默认情况下,如果在RDB snapshots持久化过程中出现问题,设置该参数后,Redis是不允许用户进行任何更新操作。

解决方法:

解决方式一:

将这个选项改为 stop-writes-on-bgsave-error false
缺点:但是这样只是当redis写硬盘快照出错时,可以让用户继续做更新操作,但是写硬盘仍然是失败的;

解决方式二:(最佳方式)
编辑文件 /etc/sysctl.conf 添加 vm.overcommit_memory=1

redis问题之Can‘t save in background: fork: Cannot allocate memory
执行sysctl -p使其生效。

 

vm.overcommit_memory 参数详解

Linux对大部分申请内存的请求都回复"yes",以便能跑更多更大的程序。因为申请内存后,并不会马上使用内存,将这些不会使用的空闲内存分配给其它程序使用,以提高内存利用率,这种技术叫做Overcommit。

一般情况下,当所有程序都不会用到自己申请的所有内存时,系统不会出问题,但是如果程序随着运行,需要的内存越来越大,在自己申请的大小范围内,不断占用更多内存,直到超出物理内存,

当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory)。它会选择杀死一些进程(用户态进程,不是内核线程,哪些占用内存越多,运行时间越短的进程越有可能被杀掉),以便释放内存。

当oom-killer发生时,linux会选择杀死哪些进程?选择进程的函数是oom_badness函数(在mm/oom_kill.c中),该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。

每个进程的点数跟(/proc/<pid>/oom_adj)oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

 

在什么条件下,linux会发现内存不足呢?

在Linux下有个CommitLimit 用于限制系统应用使用的内存资源:

[root@localhost home]#  grep -i commit /proc/meminfo

CommitLimit:    33503788 kB

Committed_AS:   32569740 kB

其中:

CommitLimit是一个内存分配上限,

Committed_AS是已经分配的内存大小。

虚拟内存算法:

CommitLimit = 物理内存 * overcommit_ratio(/proc/sys/vm/overcmmit_ratio,默认50,即50%) + swap大小

它是由内核参数overcommit_ratio的控制的,当我们的应用申请内存的时候,当出现以下情况:

应用程序要申请的内存 + 系统已经分配的内存(也就是Committed_AS)> CommitLimit

这时候就是内存不足,到了这里,操作系统要怎么办,就要祭出我们的主角“overcommit_memory”参数了(/proc/sys/vm/overcommit_memory);

vm.overcommit_memory = 0   启发策略

比较 此次请求分配的虚拟内存大小和系统当前空闲的物理内存加上swap,决定是否放行。系统在为应用进程分配虚拟地址空间时,会判断当前申请的虚拟地址空间大小是否超过剩余内存大小,

如果超过,则虚拟地址空间分配失败。因此,也就是如果进程本身占用的虚拟地址空间比较大或者剩余内存比较小时,fork、malloc等调用可能会失败。

 

 vm.overcommit_memory = 1 允许overcommit

直接放行,系统在为应用进程分配虚拟地址空间时,完全不进行限制,这种情况下,避免了fork可能产生的失败,但由于malloc是先分配虚拟地址空间,而后通过异常陷入内核分配真正的物理内存,

在内存不足的情况下,这相当于完全屏蔽了应用进程对系统内存状态的感知,即malloc总是能成功,一旦内存不足,会引起系统OOM杀进程,应用程序对于这种后果是无法预测的。

 

vm.overcommit_memory = 2 禁止overcommit

根据系统内存状态确定了虚拟地址空间的上限,由于很多情况下,进程的虚拟地址空间占用远大于其实际占用的物理内存,这样一旦内存使用量上去以后,对于一些动态产生的进程(需要复制父进程地址空间)则很容易创建失败

,如果业务过程没有过多的这种动态申请内存或者创建子进程,则影响不大,否则会产生比较大的影响 。这种情况下系统所能分配的内存不会超过上面提到的CommitLimit大小,

如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序。

 

前面讲了一大堆参数,那么这些参数又是怎么影响redis的呢?

Redis的数据回写机制分同步和异步两种,

    同步回写即SAVE命令,主进程直接向磁盘回写数据。在数据大的情况下会导致系统假死很长时间,所以一般不是推荐的。

    异步回写即BGSAVE命令,主进程fork后,复制自身并通过这个新的进程回写磁盘,回写结束后新进程自行关闭。由于这样做不需要主进程阻塞,系统不会假死,

一般默认会采用这个方法。

默认采用方式2,所以如果我们要将数据刷到硬盘上,这时redis分配内存不能太大,否则很容易发生内存不够用无法fork的问题;

设置一个合理的写磁盘策略,否则写频繁的应用,也会导致频繁的fork操作,对于占用了大内存的redis来说,fork消耗资源的代价是很大的。

参考链接:https://blog.csdn.net/zqz_zqz/article/details/53384854

 

上一篇:Android 排查问题断点遇到 Cannot find local variable


下一篇:vue中Cannot read property '_wrapper' of undefined 报错