对于一项技术的学习,我们要对这项技术有一个全局观,下面是一张 redis 全景图,我觉得画得非常全面。
图源:极客时间《Redis核心技术与实战》-蒋德均
今天我们主要关注 Redis 的高可用主线。Redis 的高可用性,具体来说,有两方面含义:一是服务少中断,二是数据少丢失。
为了保证服务少中断,通常的做法就是冗余,增加服务的副本,但是当副本多了以后,如何保证副本的数据一致就成了问题。
在这方面,Redis 提供了主从库模式,主从库之间采用的是读写分离的方式:对于读操作请求,主从库都可以接收;对于写操作,首先到主库执行,然后,主库将写操作同步给从库。
Redis读写分离
那么,主从库同步是如何完成的呢?主库数据是一次性传给从库,还是分批同步?要是主从库间的网络断连了,数据还能保持一致吗?
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系。
例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:
replicaof 172.16.19.3 6379
之后会按照三个阶段完成数据的第一次同步。
第一阶段:主从库间建立连接、协商同步的过程,主要是为全量复制做准备。具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset(这个offset是当前最新的值),返回给从库。从库收到响应后,会记录下这两个参数。
第二阶段:主库将所有数据同步给从库。具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
为什么要先清空当前数据库呢?这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。
在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
第三阶段:当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。
主从复制整个过程示意图如下所示。
主从库第一次同步的三个阶段
这样一来,主从库就实现同步了。不可忽视的是,这个过程中存在着风险点,最常见的就是网络断连或阻塞。如果网络断连,主从库之间就无法进行命令传播了,从库的数据自然也就没办法和主库保持一致了,客户端就可能从从库读到旧数据。
在 Redis 2.8 之前,如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。只把主从库网络断连期间主库收到的命令,同步给从库。
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令写入 repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,主库写到的位置会大于从库读到的位置。当网络恢复后,主库只用把主库写到的位置和从库读到的位置之间的命令操作同步给从库就行。
repl_backlog_buffer示意图
Redis 通过主从库模式,既提高了系统处理请求的吞吐量,也保证了系统的可用性。
如果从库发生故障了,客户端可以继续向主库或其他从库发送请求,进行相关的操作。但是如果主库发生故障了怎么办?此时从库没有相应的主库可以进行数据复制操作了,且一旦有写操作请求,系统也将无法处理。
所以,如果主库挂了,我们就需要运行一个新主库,比如说把一个从库切换为主库,把它当成主库。在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制。
Redis哨兵机制
哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。
哨兵的职责
监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
选主是指主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。
然后,哨兵会执行最后一个任务:通知。在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
但是你有没有想过,如果有哨兵实例在运行时发生了故障,主从库还能正常切换吗?
哨兵单点故障问题,Redis 也是通过建立哨兵集群来解决的。那我们再回头看哨兵的职责,在监控主从库是否下线时,如果出现了哨兵内部的意见不统一怎么办?比如说有 3 个哨兵,其中一个哨兵认为主库下线了,而另外 2 个却认为主库是正常的,这时该听谁的呢?
这就好比我们团队内部出现了意见分歧,那最好的解决办法就是民主投票了,采用“少数服从多数的原则”。哨兵集群内部也一样,在网络拥塞的情况下,有个别哨兵与主库的 PING 命令失败,这时哨兵就认为该主库故障了,然而实际并没有。这时就要采用“民主”的办法,大多数哨兵认为主库故障,才会进行下一步的选主。
哨兵的实例数应该是 2N+1 的单数,这样才不致于出现观点对立的情况,通常我们至少会配置 3 个哨兵实例。
那选主同样需要考虑一个问题:哨兵这么多,该由哪个执行主从切换?
此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为“Leader 选举”。因为最终执行主从切换的哨兵称为 Leader,投票过程就是确定 Leader。
到这里,我们就大致理清了 Redis 保证服务少中断所采取的一系列方案了。那 Redis 是如何保证数据少丢失的呢?
了解 MySQL 的同学可能听说过,MySQL 是具有 Crasf-Safe 的能力的,这归功于数据库的写前日志(Write Ahead Log, WAL) Redo Log。同样,Redis 也提供了 AOF 日志。
AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。我们以 Redis 收到“set testkey testvalue”命令后记录的日志为例,看看 AOF 日志的内容。其中,“ *3 ”表示当前命令有三个部分,每部分都是由“$+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有 3 个字节,也就是“set”命令。
AOF日志内容
但是,因为记录的是操作命令,而不是实际的数据,所以,用 AOF 方法进行故障恢复的时候,需要逐一把操作日志都执行一遍。如果操作日志非常多,Redis 就会恢复得很缓慢,影响到正常使用。
因此,Redis 提供了另一种数据持久化方法:内存快照 RDB。和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。
对于快照来说,系统多久执行一次快照直接影响数据丢失的多少。如下图所示,我们先在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。
RDB数据丢失
所以,要想尽可能恢复数据,t 值就要尽可能小。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?
Redis 4.0 中提出了一个混合使用 AOF 和 RDB 的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,快照不用很频繁地执行。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
RDB与AOF混合使用
这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势。
我来总结一下本文的内容。Redis 系统的高可用,具体可以通过两个方面来理解:一是服务少中断,二是数据少丢失。我整理的知识消化链路如下。
服务少中断 -> 多副本 -> 主从库模式保证数据一致及从库的高可用 -> 哨兵保证主库的高可用 -> 哨兵集群保证哨兵高可用。
数据少丢失 -> AOF日志 -> AOF恢复数据较慢 -> RDB内存快照 -> 执行快照间隔不宜过短 -> AOF+RDB
关于哨兵机制的更多实现细节,我会在后面的内容里继续更新,敬请关注。公众号「杨同学technotes」,欢迎技术交流。