那一年Redis令人窒息而且蛋疼的异常

一、Redis错误的发现.

1.记得那一年…我是一个SpiderMan…
在使用scrapy-redis做分布式爬虫的时候,我遇到了这样一个错误….
在爬虫日志中错误内容为:

Traceback (most recent call last):
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/twisted/internet/task.py", line 517, in _oneWorkUnit
    result = next(self._iterator)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/utils/defer.py", line 63, in <genexpr>
    work = (callable(elem, *args, **named) for elem in iterable)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/core/scraper.py", line 183, in _process_spidermw_output
    self.crawler.engine.crawl(request=output, spider=spider)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/core/engine.py", line 210, in crawl
    self.schedule(request, spider)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/core/engine.py", line 216, in schedule
    if not self.slot.scheduler.enqueue_request(request):
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy_redis/scheduler.py", line 162, in enqueue_request
    if not request.dont_filter and self.df.request_seen(request):
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy_redis/dupefilter.py", line 100, in request_seen
    added = self.server.sadd(self.key, fp)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 1600, in sadd
    return self.execute_command('SADD', name, *values)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 668, in execute_command
    return self.parse_response(connection, command_name, **options)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 680, in parse_response
    response = connection.read_response()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/connection.py", line 629, in read_response
    raise response
redis.exceptions.ResponseError: MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
Unhandled Error
Traceback (most recent call last):
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/commands/crawl.py", line 58, in run
    self.crawler_process.start()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/crawler.py", line 291, in start
    reactor.run(installSignalHandlers=False)  # blocking call
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/twisted/internet/base.py", line 1243, in run
    self.mainLoop()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/twisted/internet/base.py", line 1252, in mainLoop
    self.runUntilCurrent()
--- <exception caught here> ---
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/twisted/internet/base.py", line 878, in runUntilCurrent
    call.func(*call.args, **call.kw)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/utils/reactor.py", line 41, in __call__
    return self._func(*self._a, **self._kw)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/core/engine.py", line 122, in _next_request
    if not self._next_request_from_scheduler(spider):
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy/core/engine.py", line 149, in _next_request_from_scheduler
    request = slot.scheduler.next_request()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy_redis/scheduler.py", line 172, in next_request
    request = self.queue.pop(block_pop_timeout)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/scrapy_redis/queue.py", line 115, in pop
    results, count = pipe.execute()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 2879, in execute
    return execute(conn, stack, raise_on_error)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 2775, in _execute_transaction
    self.immediate_execute_command('DISCARD')
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 2715, in immediate_execute_command
    return self.parse_response(conn, command_name, **options)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 2838, in parse_response
    self, connection, command_name, **options)
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/client.py", line 680, in parse_response
    response = connection.read_response()
  File "/root/.virtualenvs/ss_spider/lib/python3.5/site-packages/redis/connection.py", line 629, in read_response
    raise response
redis.exceptions.ResponseError: DISCARD without MULTI

发现错误后我就各种在某度某歌上搜redis.exceptions.ResponseError: DISCARD without MULTI
这顿搜啊…什么也没搜到…
然后就在这个时候,突然.我登录了下redis-cli发现了…

2.接下来我们看客户端中的错误
我登上Redis寻思keys *下,看看都有啥….
结果发现了…

(error)MISCONF Redis is configured to save RDB snapshots, but is currently not able to 
persist on disk. Commands that may modify the data set are disabled. Please check Redis
logs for details about the error.

3.好吧.既然人家说了,那我们就再去看看redis.log
果不其然,redis.log中错误为↓:

939:M 10 Aug 15:34:46.032 * 10000 changes in 60 seconds. Saving...
939:M 10 Aug 15:34:46.032 # Can't save in background: fork: Cannot allocate memory
939:M 10 Aug 15:34:52.044 * 10000 changes in 60 seconds. Saving...
939:M 10 Aug 15:34:52.044 # Can't save in background: fork: Cannot allocate memory
939:M 10 Aug 15:34:58.055 * 10000 changes in 60 seconds. Saving...
939:M 10 Aug 15:34:58.055 # Can't save in background: fork: Cannot allocate memory
939:M 10 Aug 15:35:04.067 * 10000 changes in 60 seconds. Saving...
939:M 10 Aug 15:35:04.067 # Can't save in background: fork: Cannot allocate memory

没谁了,一个Redis这家伙这错误报的啊…

二、解决问题,错误排查.

既然报Redis的错误了,那我们就一个个看吧

  • 1.我们先来看第一个错误

    • ① 错误内容

      (error)MISCONF Redis is configured to save RDB snapshots, but is currently not 
      able to persist on disk. Commands that may modify the data set are disabled. 
      Please check Redis logs for details about the error.
    • ② 翻译后的信息为

      Redis被配置为保存数据库快照,但它目前不能持久化到硬盘。用来修改集合数据的命令不能用。请查看Redis
      日志的详细错误信息。
    • ③ 原因
      强制关闭Redis快照导致不能持久化。

    • ④ 解决方案
      stop-writes-on-bgsave-error设置为no
      ☞第一种方式:登录redis-cli进入终端后直接设置.

      127.0.0.1:6379> config set stop-writes-on-bgsave-error no

      ☞第二种方式:更改redis.conf文件,更改后重启redis-server
      vim /etc/redis/redis.conf
      然后更改文件下面的这部分↓↓↓

# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes  # 看这里,看这里,看这里,将yes改成no

# Compress string objects using LZF when dump .rdb databases?
# For default that's set to 'yes' as it's almost always a win.

原因说明:

这个错误是因为,redis有个默认选项
stop-writes-on-bgsave-error yes
在默认情况下,如果rdb snapshots持久化出现问题,设置这个参数后,redis不允许用户进行任何更新

不彻底的解决方式,将这个选项改为no
stop-writes-on-bgsave-error no
这样只是当redis写硬盘快照出错时,可以让用户继续做更新操作,但是写硬盘仍然是失败的。

到这里我们的Redis暂时是回复使用了,但是并没有从根本上解决问题.

  • 2.接下来我们来看看第二个错误

    • ① 错误内容
    939:M 10 Aug 15:34:46.032 * 10000 changes in 60 seconds. Saving...
    939:M 10 Aug 15:34:46.032 # Can't save in background: fork: Cannot allocate memory
    • ② 原因

      在小内存的进程上做fork, 不需要太多资源. 但当这个进程的内存空间以G为单位时, fork就成为一件很恐怖的操作.
      在16G内存的主机上fork 14G的进程, 系统肯定Cannot allocate memory.
      主机的Redis改动的越频繁fork进程也越频繁, 所以一直在Cannot allocate memory
    • ③ 解决方案
      直接修改内核参数 vm.overcommit_memory = 1, Linux内核会根据参数vm.overcommit_memory参数的设置决定是否放行。
      ☞第一种方式:终端操作

      sudo echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
      sudo sysctl -p

      ☞第二种方式:文件操作
      vim /etc/sysctl.conf
      在文件中添加以下内容
      vm.overcommit_memory = 1
      执行
      sysctl -p

至此,我们就可以恢复使用了.参数vm.overcommit_memory又是闹撒子嘞,这么神奇?

三、关于参数vm.overcommit_memory

  • 1.什么是Overcommit

    Linux对大部分申请内存的请求都回复”yes”,以便能跑更多更大的程序。因为申请内存后,并不会马上使用内存,将这些不会使用的空闲内存分配给其它程序使用,以提高内存利用率,这种技术叫做Overcommit。一般情况下,当所有程序都不会用到自己申请的所有内存时,系统不会出问题,但是如果程序随着运行,需要的内存越来越大,在自己申请的大小范围内,不断占用更多内存,直到超出物理内存,当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory)。它会选择杀死一些进程(用户态进程,不是内核线程,哪些占用内存越多,运行时间越短的进程越有可能被杀掉),以便释放内存。

  • 2.那么哪些进程会被干掉呢?

    oom-killer发生时,linux会选择杀死哪些进程?选择进程的函数是oom_badness函数(在mm/oom_kill.c中),该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。每个进程的点数跟(/proc//oom_adj)oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

    当发生oom killer时,会将记录在系统日志中/var/log/messages

    如:

    Out of memory: Kill process 9682 (mysqld) score 9 or sacrifice child
    Killed process 9682, UID 27, (mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kB
    httpd invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
  • 3.那在什么条件下,linux会发现内存不足呢?
    在Linux下有个CommitLimit 用于限制系统应用使用的内存资源:

    root@Noah-s-Ark:~# grep -i commit /proc/meminfo
    CommitLimit:    12306972 kB  # CommitLimit是一个内存分配上限
    Committed_AS:   12224932 kB  # 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);

  • 4.关于overcommit_memory的几点说明.

    • vm.overcommit_memory = 0 启发策略

      比较此次请求分配的虚拟内存大小系统当前空闲的物理内存swap决定是否放行。系统在为应用进程分配虚拟地址空间时,会判断当前申请的虚拟地址空间大小是否超过剩余内存大小,如果超过,则虚拟地址空间分配失败。因此,也就是如果进程本身占用的虚拟地址空间比较大或者剩余内存比较小时,fork、malloc等调用可能会失败

    • vm.overcommit_memory = 1 允许overcommit

      直接放行,系统在为应用进程分配虚拟地址空间时,完全不进行限制,这种情况下,避免了fork可能产生的失败,但由于malloc是先分配虚拟地址空间,而后通过异常陷入内核分配真正的物理内存,在内存不足的情况下,这相当于完全屏蔽了应用进程对系统内存状态的感知,即malloc总是能成功,一旦内存不足,会引起系统OOM杀进程,应用程序对于这种后果是无法预测的。

    • vm.overcommit_memory = 2 禁止overcommit

      根据系统内存状态确定了虚拟地址空间的上限,由于很多情况下,进程的虚拟地址空间占用远大于其实际占用的物理内存,这样一旦内存使用量上去以后,对于一些动态产生的进程(需要复制父进程地址空间)则很容易创建失败,如果业务过程没有过多的这种动态申请内存或者创建子进程,则影响不大,否则会产生比较大的影响 。这种情况下系统所能分配的内存不会超过上面提到的CommitLimit大小,如果这么多资源已经用光,那么后面任何尝试申请内存的行为都会返回错误,这通常意味着此时没法运行任何新程序

    小小归纳

    vm.overcommit_memory = 1,直接放行
    vm.overcommit_memory = 0:则比较 此次请求分配的虚拟内存大小和系统当前空闲的物理内存加上swap,决定是否放行。
    vm.overcommit_memory = 2:则会比较进程所有已分配的虚拟内存加上此次请求分配的虚拟内存和系统当前的空闲物理内存加上swap,决定是否放行。

四、这一大堆怎么影响到Redis的呢?

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

  • 同步回写:即SAVE命令,主进程直接向磁盘回写数据。在数据大的情况下会导致系统假死很长时间,所以一般不是推荐的
  • 异步回写:即BGSAVE命令,主进程fork后复制自身并通过这个新的进程回写磁盘,回写结束后新进程自行关闭。由于这样做不需要主进程阻塞,系统不会假死,一般默认会采用这个方法。

故! 默认采用方式2,所以如果我们要将数据刷到硬盘上,这时redis分配内存不能太大,否则很容易发生内存不够用无法fork的问题;
设置一个合理的写磁盘策略,否则写频繁的应用,也会导致频繁的fork操作,对于占用了大内存的redis来说,fork消耗资源的代价是很大的

五、总结

说白了,以上错误,归根结底,都是因为'它'引起的,那就是—>内存
在这里,做过上面几项后,我们可以做以下优化,个人见解,不喜勿喷.(如有错误,请指正,万分感谢!)
(1)增加物理内存
(2)从磁盘中划分出一块作为虚拟内存[swap]↓↓↓
      参见我的令一篇文章《Linux下装机后对swap分区的操作》(✈机票点我)
(3)将RedisRDB关闭 [1.你的机器非常稳定,不会宕机 2.你的数据,不重要 或 可复原.]
(4)如果你是爬虫君,更换去重方式,或更换队列,不使用Redis


在此.真诚的感谢以下N篇文章的作者:

https://www.jianshu.com/p/3aaf21dd34d6
https://blog.csdn.net/zqz_zqz/article/details/53384854
http://www.cnblogs.com/qq78292959/p/3994341.html
https://blog.csdn.net/lizhe_dashuju/article/details/55803644
https://www.jianshu.com/p/d03216c0150b


!!!版权声明!!!

本系列为博主学心得与体会,所有内容均为原创(✿◡‿◡)

欢迎传播、复制、修改。引用、转载等请注明转载来源。感谢您的配合

用于商业目的,请与博主采取联系,并请与原书版权所有者联系,谢谢!\(≧▽≦)/

我的联系方式:email–> [email protected]

!!!版权声明!!!



生活嘛~ 最重要的就是开心喽~ O(∩_∩)O~~

这里写图片描述


猜你喜欢

转载自blog.csdn.net/aa1209551258/article/details/81634646