redis阻塞了怎么办

核心提示单线程你别阻塞,Redis时延问题分析及应对Redis的事件循环在一个线程中处理,作为一个单线程程序,重要的是要保证事件处理的时延短,这样,事件循环中的后续任务才不会阻塞;?当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的

单线程你别阻塞,Redis时延问题分析及应对

Redis的事件循环在一个线程中处理,作为一个单线程程序,重要的是要保证事件处理的时延短,这样,事件循环中的后续任务才不会阻塞;?

当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的影响尤为严重;?

下面我们总结下在redis中有哪些耗时的场景及应对方法;

耗时长的命令造成阻塞

keys、sort等命令

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

类似的命令有sunion sort等操作;?

如果业务需求中一定要使用keys、sort等操作怎么办?

解决方案:?

在架构设计中,有“分流”一招,说的是将处理快的请求和处理慢的请求分离来开,否则,慢的影响到了快的,让快的也快不起来;这在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文件重写操作会创建新的进程做重写;(重写并不会去读已有的文件,而是直接使用内存中的数据写成归档日志);

解决方案:?

为了应对大内存页表复制时带来的影响,有些可用的措施:

控制每个redis实例的最大内存量;?

不让fork带来的限制太多,可以从内存量上控制fork的时延;?

一般建议不超过20G,可根据自己服务器的性能来确定(内存越大,持久化的时间越长,复制页表的时间越长,对事件循环的阻塞就延长)?

新浪微博给的建议是不超过20G,而我们虚机上的测试,要想保证应用毛刺不明显,可能得在10G以下;

使用大内存页,默认内存页使用4KB,这样,当使用40G的内存时,页表就有80M;而将每个内存页扩大到4M,页表就只有80K;这样复制页表几乎没有阻塞,同时也会提高快速页表缓冲TLB(translation lookaside buffer)的命中率;但大内存页也有问题,在写时复制时,只要一个页快中任何一个元素被修改,这个页块都需要复制一份(COW机制的粒度是页面),这样在写时复制期间,会耗用更多的内存空间;

使用物理机;?

如果有的选,物理机当然是最佳方案,比上面都要省事;?

当然,虚拟化实现也有多种,除了Xen系统外,现代的硬件大部分都可以快速的复制页表;?

但公司的虚拟化一般是成套上线的,不会因为我们个别服务器的原因而变更,如果面对的只有Xen,只能想想如何用好它;

杜绝新进程的产生,不使用持久化,不在主结点上提供查询;实现起来有以下方案:?

1)?只用单机,不开持久化,不挂slave结点。这样最简单,不会有新进程的产生;但这样的方案只适合缓存;?

如何来做这个方案的高可用??

要做高可用,可以在写redis的前端挂上一个消息队列,在消息队列中使用pub-sub来做分发,保证每个写操作至少落到2个结点上;因为所有结点的数据相同,只需要用一个结点做持久化,这个结点对外不提供查询;?

2)?master-slave:在主结点上开持久化,主结点不对外提供查询,查询由slave结点提供,从结点不提供持久化;这样,所有的fork耗时的操作都在主结点上,而查询请求由slave结点提供;?

这个方案的问题是主结点坏了之后如何处理??

简单的实现方案是主不具有可替代性,坏了之后,redis集群对外就只能提供读,而无法更新;待主结点启动后,再继续更新操作;对于之前的更新操作,可以用MQ缓存起来,等主结点起来之后消化掉故障期间的写请求;?

如果使用官方的Sentinel将从升级为主,整体实现就相对复杂了;需要更改可用从的ip配置,将其从可查询结点中剔除,让前端的查询负载不再落在新主上;然后,才能放开sentinel的切换操作,这个前后关系需要保证;

持久化造成的阻塞

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

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

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

原因分析:?

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

redis.conf中是这么说的:?

When the AOF fsync policy is set to always or everysec, and a background?

saving process (a background save or AOF log background rewriting) is?

performing a lot of I/O against the disk, in some Linux configurations?

Redis may block too long on the fsync() call. Note that there is no fix for?

this currently, as even performing fsync in a different thread will block?

our synchronous write(2) call.?

当执行AOF重写时会有大量IO,这在某些Linux配置下会造成主进程fsync阻塞;

解决方案:?

设置 no-appendfsync-on-rewrite yes, 在子进程执行AOF重写时, 主进程不调用fsync()操作;注意, 即使进程不调用 fsync(), 系统内核也会根据自己的算法在适当的时机将数据写到硬盘(Linux 默认最长不超过 30 秒).?

这个设置带来的问题是当出现故障时,最长可能丢失超过30秒的数据,而不再是1秒;

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

我们来梳理下:?

1) 起因:有大量IO操作write(2) 但未主动调用同步操作?

2) 造成kernel buffer中有大量脏数据?

3) 系统同步时,sync的同步时间过长?

4) 造成redis的写aof日志write(2)操作阻塞;?

5) 造成单线程的redis的下一个事件无法处理,整个redis阻塞(redis的事件处理是在一个线程中进行,其中写aof日志的write(2)是同步阻塞模式调用,与网络的非阻塞write(2)要区分开来)

产生1)的原因:这是redis2.6.12之前的问题,AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。?

另外的原因:系统IO繁忙,比如有别的应用在写盘;

解决方案:?

控制系统sync调用的时间;需要同步的数据多时,耗时就长;缩小这个耗时,控制每次同步的数据量;通过配置按比例(vm.dirty_background_ratio)或按值(vm.dirty_bytes)设置sync的调用阈值;(一般设置为32M同步一次)?

2.6.12以后,AOF rewrite 32M时会主动调用fdatasync;

另外,Redis当发现当前正在写的文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被block。但如果已经超过两秒都还是这个样子,则会强行执行write(2),即使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重写命令;

Redis哨兵机制原理浅析

redis持久化的意义主要是为了做 灾难恢复、数据恢复 其实可以把它归类到高可用的一个环节。

RDB持久化机制,对redis中的 数据 执行周期性的持久化。

AOF机制对 每条写入命令 作为日志,以append-only的模式写入一个日志文件,在redis重启对时候,可以通过回放AOF日志中写入的指令来重新构建整个的数据集。

如果同时使用AOF和RDB两种持久化机制 ,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加的完整。

优点:

(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备。可以将文件存储到云端,本地磁盘等等。

(2)RDB机制对redis对外提供读写服务时候的影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB的持久化即可。

(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加的快速。

缺点:

(1)如果想让redis出现故障,尽可能的少丢失数据,那么RDB没有AOF好。因为一般来说,RDB数据快照文件,基本上都是每隔5分钟或者更长的时间,生成一次,这个时候,如果一旦发生宕机,那么就会把这段时间内的数据都丢失掉。

(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。

优点:

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行fsync操作,最多丢失1秒钟的数据。

(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易受损,即使文件尾部受损,也能很容易恢复,打开文件,把后面损坏的数据删除即可。

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据对最小日志出来,再创建新日志文件的时候,老日志文件还是会照常写入指令,当新的日志文件生成好之后,会将旧日志文件中后面写入的指令合并到新的日志文件中,这个新的merge后的日志文件,会在ready的时候,与旧的日志文件进行交换。之后就会把旧的日志文件删除掉。

(4)AOF文件中保存的是执行的指令,所以这个特性非常适合做灾难性的误操作紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么久可以立即拷贝这个AOF文件出来,将最后一条flushall命令删除,然后再将AOF文件放回去,就可以通过恢复机制,自动的恢复所有数据了。

缺点:

(1)对于同一份数据来说,AOF的日志文件通常要比RDB的数据快照文件要大。

(2)AOF开启之后,Redis服务支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然每秒一次fsync的性能也还是很高的。

(3)以前的AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来,所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge,而是基于当时内存中的数据进行指令的重新构建,这样健壮性能更好一些。

综合使用两者,用AOF来保证数据尽可能的少丢失,作为第一选择,其次在AOP丢失或者损坏的情况下,用RDB来更加快速的恢复数据。

上一篇文章Redis主从复制原理中简要地说明了主从复制的一个基本原理,包含全量复制、复制积压缓冲区与增量复制等内容,有兴趣的同学可以先看下。

利用主从复制,可以实现读写分离、数据备份等功能。但如果主库宕机后,需要运维人员手动地将一个从库提升为新主库,并将其他从库slaveof新主库,以此来实现故障恢复。

因此, 主从模式的一个缺点,就在于无法实现自动化地故障恢复 。Redis后来引入了哨兵机制,哨兵机制大大提升了系统的高可用性。

哨兵,就是站岗放哨的,时刻监控周围的一举一动,在第一时间发现敌情并发出及时的警报。

Redis中的哨兵(Sentinel), 则是一个特殊的Redis实例 ,不过它并不存储数据。也就是说,哨兵在启动时,不会去加载RDB文件。

关于Redis的持久化,可以参考我的另外一篇文章 谈谈Redis的持久化——AOF日志与RDB快照

上图就是一个典型的哨兵架构,由数据节点与哨兵节点构成,通常会部署多个哨兵节点。

哨兵主要具有三个作用, 监控、选主与通知

监控:哨兵会利用心跳机制,周期性不断地检测主库与从库的存活性

选主:哨兵检测到主库宕机后,选择一个从库将之切换为新主库

通知:哨兵会将新主库的地址通知到所有从库,使得所有从库与旧主库slaveof新主库,也会将新主库的地址通知到客户端上

我会在下文详细讲一下监控与选主的过程

哨兵系统是通过3个定时任务,来完成对主库、从库与哨兵之间的探活。

首先我们会在配置文件中配置主库地址,这样哨兵在启动后,会以 每隔10秒 的频率向主库发送info命令,从而获得当前的主从拓扑关系,这样就拿到了所有从库的地址。

在此基础上,哨兵会 每隔1秒 向主库、从库与其他哨兵节点发送PING命令,因此来进行互相探活。

当某个哨兵在 down-after-milliseconds(默认是30秒) 配置的连续时间内,仍然没有收到主库的正确响应,则当前哨兵会认为主库 主观下线 ,并将其标记为sdown(subjective down)

为了避免当前哨兵对主库的误判,因此这个时候还需要参考其他哨兵的意见。

接着当前哨兵会向其他哨兵发送 sentinel is-master-down-by-addr 命令, 如果有半数以上(由quorum参数决定)的哨兵认为主库确实处于主观下线状态,则当前哨兵认为主库客观下线 ,标记为odown(objective down)

一旦某个主库被认定为客观下线时,这个时候需要进行哨兵选举,选举出一个领导者哨兵,来完成主从切换的过程。

哨兵A在向其他哨兵发送 sentinel is-master-down-by-addr 命令时,同时要求其他哨兵同意将其设置为Leader,也就是想获得其他哨兵的投票。

在每一轮选举中,每个哨兵仅有一票。投票遵循先来先到的原则,如果某个哨兵没有投给别人,就会投给哨兵A。

首先获得半数以上投票的哨兵,将被选举称为Leader。

这里的哨兵选举,采用的是Raft算法。这里不对Raft做详细的探讨,有兴趣的同学,可以参考我的另外一篇文章 22张图,带你入门分布式一致性算法Raft

该文章采用大量的图例,相信你可以从中学习到全新的知识,从而打开分布式一致性算法的大门,大伙们记得等我搞完Paxos与Zab。

过半投票机制也常用于很多算法中,例如RedLock,在半数以上的节点上加锁成功,才代表申请到了分布式锁,具体可参考这篇文章的最后 我用了上万字,走了一遍Redis实现分布式锁的坎坷之路,从单机到主从再到多实例,原来会发生这么多的问题

在Zookeeper选举中,同样也用到了过半投票机制,在这篇文章中 面试官:能给我画个Zookeeper选举的图吗? 我从源码角度分析了Zookeeper选举的过程。

在选举到领导者哨兵后,将由该哨兵完成故障恢复工作。

故障恢复分为以下两步:

详细说一下第一步,挑选是有条件的。首先要过滤出不 健康 的节点,再按某种规则排序,最后取第一个从库,我们直接从源码入手:

因此,以下从库会被过滤出:

剩下的节点,就是 健康 的节点,此时再执行一次快速排序,排序的规则如下:

本文算是Redis哨兵的一个入门文章,主要讲了哨兵的作用,例如监控、选主和通知。

在Redis读写分离的情况下,使用哨兵可以很轻松地做到故障恢复,提升了整体的可用性。

但哨兵无法解决Redis单机写的瓶颈,这就需要引入集群模式,相应的文章也被列为明年的写作计划中。

 
友情链接
鄂ICP备19019357号-22