主从库模式
Redis的高可靠性主要包括两方面:
数据尽量少丢失:RDB & AOF机制
服务尽量少中断:增加副本冗余
主从模式
Redis提供了主从库模式,增加冗余的副本来提高Redis集群的高可靠性。主从库之间采用读写分离的方式,写请求只能在主库,读请求在主从库都可以完成。
读操作:主库、从库
写操作:主库 --> 主库写完后同步从库
写请求为什么只能在主库上,若从库和主库上都可以进行读写会发生什么?
1. 若有3个写请求先后都是对key1进行操作,并且分别请求到了不同实例上,对应修改的值分别为v1,v2,v3,那么key1对应的值在每个实例上都是不一致的。读数据时,可能会读到旧的值。
2. 若要保持这个数据在3个实例上都是一致的,旧要涉及到加锁、实例间协商是否完成修改等一系列操作,但这会带来巨额的开销,得不偿失。
主从库之间是如何同步的
全量同步:当从库是第一次建立连接并开始同步,该种情况下的同步如下图:
从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。首先,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。
(1)runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。
(2)offset,此时设为 -1,表示第一次复制。FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。在第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。
主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。
增量同步:
Redis增量复制是指replica初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis的主节点创建和维护一个环形缓冲复制队列(即repl_backlog_buffer),从节点部分复制(增量复制)的数据均来自于repl_backlog_buffer。主节点只有一个repl_backlog_buffer,所有从节点共享。
主-从-从模式减少主库全量同步时的压力
主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。如下图:
主从库网络中断怎么办
Redis的主节点创建和维护一个环形缓冲复制队列(即repl_backlog_buffer),从节点部分复制(增量复制)的数据均来自于repl_backlog_buffer。主节点只有一个repl_backlog_buffer,所有从节点共享。
当主从库断连后,主库每次都会把收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。主库会记录自己写到的位置,从库则会记录自己已经读到的位置。主库来的偏移量是master_repl_offset,对从库来说从库已复制的偏移量是slave_repl_offset,正常情况下,这两个偏移量基本相等。
主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。
哨兵
哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知,哨兵节点其实就是一个特殊的Redis实例节点。
监控:判断主从库下线
选主:选出新主库
通知:让从库与主库同步,通知客户端与主库连接
哨兵与没一个主库和从库相连接,并且哨兵节点之间行成集群。
监控
哨兵与没一个Redis节点相连接并通过PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。没一个Redis节点的状态可以分为”主观下线“和”客观下线“。
如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。
如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,开启主从切换。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。
在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。同时,这会进一步触发哨兵开始主从切换流程。
选主
当主库挂了以后,哨兵会在从库中进行选主,选主主要包括以下三个原则。
首先,优先级最高的从库得分高。
其次,和旧主库同步程度最接近的从库得分高。
最后,ID 号小的从库得分高。
用户可以通过 slave-priority 配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始进行第二个原则判断。
这个规则的依据是,如果选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。主从库同步时有个命令传播的过程。在这个过程中,主库会用 master_repl_offset 记录当前的最新写操作在 repl_backlog_buffer 中的位置,而从库会用 slave_repl_offset 这个值记录当前的复制进度。
每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
分片集群
当数据容量越来越大时,如Redis集群的数据量有5G增长到了25G,那么需要考虑集群的扩容问题,在扩容时一般有两种方案:
纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。将磁盘容量扩展到50G
简单直接,但是增加了硬件和成本的限制、当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞横向扩展:横向增加当前 Redis 实例的个数,就像下图中,原来使用 1 个 *GB 内存、10GB 磁盘的实例,现在使用三个相同配置的实例。
分片和实例的对应分布关系
在分片集群中,数据需要分布在不同实例上,那么,数据和实例之间如何对应呢?这就和接下来我要讲的 Redis Cluster 方案有关了。不过,我们要先弄明白切片集群和 Redis Cluster 的联系与区别。
Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;
然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
以只有五个槽为例,展示,数据、哈希槽、实例这三者的映射分布情况,如下图:
客户端如何定位数据
客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。那么,客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?这是因为,Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
在集群中,实例有新增或删除,Redis 需要重新分配哈希槽
为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍
Redis Cluster 方案提供了一种重定向机制,所谓的“重定向”,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。
需要注意的是,在上图中,当客户端给实例 2 发送命令时,Slot 2 中的数据已经全部迁移到了实例 3。在实际应用时,如果 Slot 2 中的数据比较多,就可能会出现一种情况:客户端向实例 2 发送请求,但此时,Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息。
这个结果中的 ASK 命令就表示,客户端请求的键值对所在的哈希槽 13320,在实例3上,但是这个哈希槽正在迁移。此时,客户端需要先给 实例3这个实例发送一个 ASKING 命令。这个命令的意思是,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据。
在下图中,Slot 2 正在从实例 2 往实例 3 迁移,key1 和 key2 已经迁移过去,key3 和 key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令。ASK 命令表示两层含义:第一,表明 Slot 数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。ASK 命令并不会更新客户端缓存的哈希槽分配信息。
作者:买个橘子