Redis时延问题分析及应对


Redis时延问题分析及应对

文章插图
 
redis时延问题分析及应对Redis的事件循环在一个线程中处理,作为一个单线程程序,重要的是要保证事件处理的时延短,这样,事件循环中的后续任务才不会阻塞;
当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的影响尤为严重;
下面我们总结下在redis中有哪些耗时的场景及应对方法;
耗时长的命令造成阻塞
keys、sort等命令
keys命令用于查找所有符合给定模式 pattern 的 key,时间复杂度为O(N),N 为数据库中 key 的数量 。当数据库中的个数达到千万时,这个命令会造成读写线程阻塞数秒;
类似的命令有sunion sort等操作;
如果业务需求中一定要使用keys、sort等操作怎么办?
解决方案:
Redis时延问题分析及应对

文章插图
 
在架构设计中,有“分流”一招,说的是将处理快的请求和处理慢的请求分离来开,否则,慢的影响到了快的,让快的也快不起来;这在redis的设计中体现的非常明显,redis的纯内存操作,epoll非阻塞IO事件处理,这些快的放在一个线程中搞定,而持久化,AOF重写、Master-slave同步数据这些耗时的操作就单开一个进程来处理,不要慢的影响到快的;
同样,既然需要使用keys这些耗时的操作,那么我们就将它们剥离出去,比如单开一个redis slave结点,专门用于keys、sort等耗时的操作,这些查询一般不会是线上的实时业务,查询慢点就慢点,主要是能完成任务,而对于线上的耗时快的任务没有影响;
smembers命令
smembers命令用于获取集合全集,时间复杂度为O(N),N为集合中的数量;
如果一个集合中保存了千万量级的数据,一次取回也会造成事件处理线程的长时间阻塞;
解决方案:
和sort,keys等命令不一样,smembers可能是线上实时应用场景中使用频率非常高的一个命令,这里分流一招并不适合,我们更多的需要从设计层面来考虑;
在设计时,我们可以控制集合的数量,将集合数一般保持在500个以内;
比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围;
如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count];随机返回集合中的指定数量,当然,如果要遍历集合中的所有元素,这个命令就不适合了;
save命令
save命令使用事件处理线程进行数据的持久化;当数据量大的时候,会造成线程长时间阻塞(我们的生产上,reids内存中1个G保存需要12s左右),整个redis被block;
save阻塞了事件处理的线程,我们甚至无法使用redis-cli查看当前的系统状态,造成“何时保存结束,目前保存了多少”这样的信息都无从得知;
解决方案:
我没有想到需要用到save命令的场景,任何时候需要持久化的时候使用bgsave都是合理的选择(当然,这个命令也会带来问题,后面聊到);
fork产生的阻塞
在redis需要执行耗时的操作时,会新建一个进程来做,比如数据持久化bgsave:
开启RDB持久化后,当达到持久化的阈值,redis会fork一个新的进程来做持久化,采用了操作系统的copy-on-wirte写时复制策略,子进程与父进程共享Page 。如果父进程的Page(每页4K)有修改,父进程自己创建那个Page的副本,不会影响到子进程;
fork新进程时,虽然可共享的数据内容不需要复制,但会复制之前进程空间的内存页表,如果内存空间有40G(考虑每个页表条目消耗 8 个字节),那么页表大小就有80M,这个复制是需要时间的,如果使用虚拟机,特别是Xen虚拟服务器,耗时会更长;
在我们有的服务器结点上测试,35G的数据bgsave瞬间会阻塞200ms以上;
类似的,以下这些操作都有进程fork;
  • Master向slave首次同步数据:当master结点收到slave结点来的syn同步请求,会生成一个新的进程,将内存数据dump到文件上,然后再同步到slave结点中;
  • AOF日志重写:使用AOF持久化方式,做AOF文件重写操作会创建新的进程做重写;(重写并不会去读已有的文件,而是直接使用内存中的数据写成归档日志);
解决方案:
为了应对大内存页表复制时带来的影响,有些可用的措施:
  1. 控制每个redis实例的最大内存量;


    推荐阅读