Redis内存溢出后报错的解决办法

故障简介

早上查看Redis日志的时候发现一直在报错

1
2
[1524] 24 Mar 10:00:56.037 * 1 changes in 900 seconds. Saving...
[1524] 24 Mar 10:00:56.037 # Can't save in background: fork: Cannot allocate memory

php程序的错误日志

1
Exception: MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. 

故障分析

简单来说就是redis的数据快照不能持久化到磁盘,然后程序就不能继续把新数据保存到redis。网上说是因为磁盘空间满了或者是redis没有写入权限导致的,但是分析发现是因为我的服务器内存满了(内存被php-fpm进程耗尽了)。有时候看起来内存还是有不少空闲的,但是还是会出现这种情况。下面做一个详细分析。

Redis数据回写机制分同步和异步两种方式:

  • 同步回写(SAVE), 主进程直接向磁盘回写数据. 在数据量大的情况下会导致系统假死很长时间
  • 异步回写(BGSAVE), 主进程fork后, 复制自身并通过这个新的进程回写磁盘, 回写结束后新进程自行关闭.
    由于 BGSAVE 不需要主进程阻塞, 系统也不会假死, 一般会采用 BGSAVE 来实现数据回写.

简单地说:Redis在保存数据到硬盘时为了避免主进程假死,需要Fork一份主进程,然后在Fork进程内完成数据保存到硬盘的操作,如果主进程使用了4GB的内存,Fork子进程的时候需要额外的4GB,此时内存就不够了(即便此时还有2G的空闲内存),Fork失败,进而数据保存硬盘也失败了。

解决方案

很多人都是建议进入reids控制台执行config set stop-writes-on-bgsave-error no或者直接在配置文件找到这一项,修改后重启redis。这样做其实是不好的,这仅仅是让程序忽略了这个异常,使得程序能够继续往下运行,但实际上数据还是会存储到硬盘失败!所以应该修改内核参数 vm.overcommit_memory = 1, Linux内核会根据参数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大小,如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序。

源码如下:

1
2
3
/* 在/etc/sysctl.conf文件里面加入或者直接删除也可以,因为它缺省值就是 */
sudo echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
sudo sysctl -p

注:如果是内存被php-fpm进程耗尽导致的问题,应该调整php-fpm的生成规则。