Redis时延分析

Redis采用Reactor模式,Reactor模式将事件驱动逻辑和具体的业务逻辑分离,同时将不同类型请求通过面向对象的方式分离,在Reactor上提供了注册/移除事件等方法,供应用代码使用,而事件分发方法,通常是一个循环调用,Reactor模式一般都是单线程的,好处是每个事件处理时不需要考虑共享资源的互斥访问,缺点是不能高效利用CPU,对于时间驱动,程序向中间人注册一个Event Handler,当 I/O就绪后,中间人就产生一个事件通知对应的Handler进行处理。所以总结起来,Reactor模式就是一个不断循环等待的单独线程,接受所有Handler的注册,并负责向操作系统查询 I/O是否就绪,如果就绪的话调用指定的Handler进行处理。

所以,Redis的事件循环是在单线程中进行的,因此要确保快速的进行事件处理,这样事件循环中的后续任务才不会被阻塞。

在Redis中影响时延的场景主要有三种:

  • 耗时长的命令造成的阻塞;
  • fork产生的阻塞;
  • 持久化造成的阻塞;


耗时长的命令造成的阻塞

耗时长的命令主要包括keys、sort、smembers等。

对于keys命令,它是用于查找所有符合给定模式pattern的key,时间复杂度为O(N), N为数据库中key的数量。当数据库中的个数达到千万时,这个命令会造成读写线程阻塞数秒。类似的命令有sunion sort等操作。

解决办法:


将处理快的请求和处理慢的请求分离,否则慢的影响到了快的;因为redis中的操作都是基于内存的,并且epoll是非阻塞的,这样可以把这些操作放置在一个单线程中完成,而对于持久化、AOF重写以及Master-slave同步数据这些耗时的操作就创建一个新进程来处理。 
同样对于keys这样的耗时操作,可将它分离出去,比如单独使用一个redis从节点专门用于keys、sort等耗时操作,通常这些查询一般不会是线上的实时业务,对时延要求不高。

对于smembers命令,它是用于获取集合全集,时间复杂度为O(N),N为集合中的数量。如果一个集合中保存了千万级的数据,一次性取回也会造成事件处理线程的长时间阻塞。

解决方案: 
和sort、keys等命令不一样,smembers是线上实时应用场景中使用频率非常高的一个命令,可以从设计层面来考虑: 可以控制集合的数量。比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围。

如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count]随机返回集合中的指定数量,当然如果要遍历集合中的所有元素,这个命令就不适合了。

对于save命令,使用事件处理线程进行数据的持久化,当数据量大的时候,会造成线程长时间阻塞,整个redis被block。save阻塞了事件处理的线程,我们甚至无法使用redis-cli查看当前的系统状态,造成“何时保存结束,目前保存了多少”这样的信息都无从得知。

解决方案: 
通过使用bgsave代替save来进行持久化。


fork产生的阻塞

在Redis中可以通过新建一个子进程来执行耗时的操作,比如用于数据持久化保存bgsave命令: 开启RDB持久化后,当达到持久化的阈值,redis会fork一个新的子进程来做持久化,采用了操作系统的copy-on-wirte写时复制策略,子进程与父进程共享Page。如果父进程的Page(每页4K)有修改,父进程自己创建那个Page的副本,不会影响到子进程。
另外在fork子进程时,虽然可共享的数据内容不需要复制,但会复制之前进程空间的内存页表,如果内存空间有40G(考虑每个页表条目8个字节),那么页表大小就有80M,这个复制会消耗大量时间。

类似的,以下这些操作都会fork子进程:

  • Master向Slave首次同步数据:当Master节点接收到Slave节点的syn同步请求,会fork一个新的子进程,将内存数据dump到文件上,然后再同步到Slave节点中;
  • AOF日志重写:使用AOF持久化方式,做AOF文件重写操作会创建新的进程去完成;

解决方案: 
解决大内存页表复制时带来的影响:

  • 控制每个redis实例的最大内存量, 从内存量上控制fork的时延; 一般建议不超过20G,根据自己服务器性能来确定(内存越大,持久化的时间越长,复制页表的时间越长,对事件循环的阻塞就延长) 。
  • 使用大内存页,默认内存页使用4KB,这样当使用40G的内存时,页表就有80M,而将每个内存页扩大到4M,页表就只有80K,这样复制页表几乎没有阻塞,同时也会提高快速页表缓冲TLB的命中率,但大内存页也有问题,在写时复制时,只要一个页快中任何一个元素被修改,这个页块都需要复制一份(COW机制的粒度是页面),这样在写时复制期间,会耗用更多的内存空间。
  • 避免fork新进程,不使用持久化,不在主节点上进行查询: 
    • 只用单机,不开持久化,不挂slave节点(但这样的方案只适合缓存);另外为实现高可用,可以在写redis的前端挂上一个消息队列,在消息队列中使用pub-sub来做分发,保证每个写操作至少落到2个节点上;因为所有节点中数据相同,只需要用一个节点做持久化,并且该节点不对外提供查询。
    • master-slave:在主节点上开持久化,主节点不对外提供查询,查询由slave节点提供,从节点不提供持久化;这样所有fork耗时的操作都

      在主节点上,而查询请求由slave节点提供; 为解决主节点down机,因为主节点不具有可替代性,redis集群对外就只提供读,待主节点启

      动后,再继续更新操作;对于之前的更新操作,可以用MQ缓存起来,等主节点恢复后完成故障期间的写请求。


持久化造成的阻塞

执行持久化(AOF/RDB)对系统性能有较大影响,特别是服务器节点上还有其它读写磁盘的操作时(比如应用服务和redis服务部署在相同节点上,应用服务实时记录进出报日志);应尽可能避免在IO密集节点上开Redis持久化。

子进程持久化时的write和父进程的fsync冲突造成的阻塞。

在开启了AOF持久化的节点上,当子进程执行AOF重写或者RDB持久化时,出现了Redis查询卡顿甚至长时间阻塞的问题,,此时Redis无法提供任何读写操作:

原因分析: Redis 服务设置了appendfsync everysec,主进程每秒钟便会调用fsync(),,要求内核将数据”确实”写到存储硬件里,但由于服务器正在进行大量IO操作, 导致主进程 fsync操作被阻塞,,最终导致Redis主进程阻塞。

解决方案: 
设置 no-appendfsync-on-rewrite yes, 在子进程执行AOF重写时,,主进程不调用fsync()操作;注意,即使进程不调用 fsync(),,系统内核也会根据自己的算法在适当的时机将数据写到硬盘(Linux默认最长不超过30秒)。这个设置带来的问题是当出现故障时,最长可能丢失超过30秒的数据,而不再是1秒。

子进程进行AOF重写时,系统的sync造成主进程write阻塞:

  • 有大量IO操作write, 但未主动调用同步操作,造成kernel buffer中有大量脏数据;
  • 系统同步时,sync的同步时间过长,造成redis的写aof日志write操作阻塞; 
  • 造成单线程的redis的下一个事件无法处理,整个redis阻塞。

解决方案: 
控制系统sync调用的时间,需要同步的数据多时,耗时就长;缩小这个耗时,控制每次同步的数据量;通过配置按比例(vm.dirty_background_ratio)或按值(vm.dirty_bytes)设置sync的调用阈值(一般设置为32M同步一次);2.6.12以后,AOF rewrite 32M时会主动调用fdatasync。

另外,Redis当发现当前正在写的文件有在执行fdatasync时,就先不调用write,只存在cache里,免得被block。但如果已经超过两秒都还是这个样子,则会强行执行write,即使redis会被block住。

AOF重写完成后合并数据时造成的阻塞在bgrewriteaof过程中,所有新来的写入请求依然会被写入旧的AOF文件,同时放到AOF buffer中,当rewrite完成后,会在主线程把这部分内容合并到临时文件中之后才rename成新的AOF文件,所以rewrite过程中会不断打印"Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB",要监控这部分的日志。这个合并的过程是阻塞的,如果产生了280MB的buffer,在100MB/s的传统硬盘上,Redis就要阻塞2.8秒。

解决方案: 
将硬盘设置的足够大,将AOF重写的阈值调高,保证高峰期间不会触发重写操作,在闲时使用crontab 调用AOF重写命令。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值