深入理解Redis Master-Slaver/Sentinel/Cluster原理

前言

这篇总结参考自 Redis 的官方文档与《Redis设计与实现》。

Redis中的多实例架构主要分为三种:主从(Master-Slaver)、哨兵(Sentinel) 和集群(Cluster),其中 Sentinel 和 Cluster 都是以 Master-Slaver 为基础的。

主从(Master-Slaver)

注:由于某些政治正确原因,Redis 官方现在已经将 Master-Slaver 改为 Master-Replica,一些相关的命令诸如 slaverof <Matser-IP> 也改成了 replicaof <Matser-IP>,但原有的命令暂时还是可用并存的。

个人理解,主从模式的作用有以下两点:

  • 容灾备份:当一个节点损坏时由于有备份,可以十分容易地恢复数据。
  • 负载均衡:通过读写分离,所有写请求将由 master完成,读请求则可以通过其他 slave 来完成且 slave 是可以无限扩展的,从而可以支持更高的并发量。

主从模式下,一个 master 可以有多个 slave,但一个 slave 只能有一个master,slave 也可以有自己的 slave,其中的数据流向是单向的,由 master 到 slave。

注意,出于保证主和各个从之间数据一致性的考虑,slave 默认是无法提供写服务的,除非将配置文件中的 replica-read-only 改为 no,但这样的话就要自己实现主从一致性。

1
2
# Since Redis 2.6 by default replicas are read-only.
replica-read-only yes

旧版复制(SYNC)

旧版复制功能可以分为同步命令传播两个操作:

  • 同步:用于将 slave 更新至与 master 数据一致的状态。
  • 命令传播:用于 master 数据被更改,导致主从不一致的情况,让 master 与 slave 重新回到数据一致的在状态。

同步

同步操作需要 SYNC 命令来完成,具体步骤如下:

  1. 从节点向 master 发送 PSYNC 命令。
  2. Master 执行 BGSAVE,开启一个后台进程生成 RDB 文件。
  3. Master 开始缓冲所有新的写命令。
  4. 当后台保存任务完成时, master 将 RDB 文件读取至内存中并传输给 slave, slave 将其保存在磁盘上,然后加载 RDB 文件到内存进行同步。
  5. Master 将缓冲区内缓存的所有写命令发送给 slave,slave 执行这些写命令完成全量复制。

命令传播

经过同步操作后,主从之间的数据将达到一致状态,但当 master 数据又被改变时,为了让主从再次回到数据一致状态,master 还需要对 slave 进行命令传播。

即 master 会将新的造成数据不一致的命令都发送给 slave,这样当 slave执行完这些命令后,主从将恢复数据一致状态。

新版复制(PSYNC)

新版的复制命令PSYNC 提供了全量复制与部分复制两种模式。

全量复制:用于初次复制或其它无法进行部分复制的情况,将 master 中的所有数据都发送给 slave。

全量复制的步骤与旧版复制中的同步操作一样,其过程中的创建 RDB 文件、加载至内存、经网络传输、slave 接收并载入 RDB 文件中都是对 master 与 slave 开销十分大的操作。所以全量复制是一个非常耗费资源的操作。

为了部分解决这个问题,从 2.8.18 开始,Redis 支持无磁盘复制,子进程可以直接发送 RDB 文件给 slave,无需使用磁盘作为中间储存介质。更进一步,Redis 还提供了部分复制的方案。

部分复制:用于处理网络短暂性中断等原因造成的数据丢失场景,当 slave 重新连上 master后,master 会补发断线期间丢失数据给 slave。因为补发的数据远远小于全量数据,所以可以有效避免全量复制的过高开销。

部分复制依靠以下三个部分完成:

  • Redis Master 的 replication ID ,这是一个伪随机字符串,标记了一个给定的 dataset。
  • Redis Master 与 Slave 的 offset 偏移量,新增了多少字节的数据,其自身的 offset 就会增加多少。
  • Redis Master 的 Replication Bakclog 复制积压缓冲区。

Replication Bakclog 是由 master 维护的固定长度 FIFO 队列,默认大小为 1MB

当复制时master 就会将 replication stream 发送给 slave 。offset 即使在没有任何 slave 连接到 master 的情况下也会自增,所以对于每一对给定的(Replication ID, offset) ,都标识一个 master dataset的确切版本。

当 slave 连接到 master 进行同步时,会使用 PSYNC 命令来发送旧的 master replication ID 和已经处理过的 offset。通过这种方式, master 能够仅发送 slave 所需的增量部分实现部分复制。但是如果 master 的缓冲区中没有足够的 backlog(“However if there is not enough backlog in the master buffers”),或者如果 slave 引用了不可知的历史 replication ID,则会转而进行全量复制。

需要注意的是,如果网络中断时间过长,造成 master 没有缓存完整中断期间执行的写命令,则无法进行部分复制,此时会转而使用全量复制。

规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程

解决方案:

  • 单主节点复制风暴:master 只有一个,此时应减少 master 的 slave 数量,或者采用树状复制结构,加入中间层 slave 用来保护最上面的 mster。

  • 单机器复制风暴:master 有多个,但这些 master 都在同台机器上,此时很容易阻塞 IO,此时应该把这些 master 分散部署在多台机器上

主从模式下的过期 key 处理策略

Slave 不会过期 key,只会等待 master 过期 key。在 master 主动过期一个key,或者有客户端读过期 key 时 master 通过惰性删除策略淘汰了这个 key 的情况下,master 就会发送一条 del 命令给 slave 删除该 key。

其中, master 删除该 key 到 slave 也删除该 key 的过程是存在一个延迟的。因此,主从模式下 Slave 会存在读到过期 key 的情况

Brain split(脑裂)问题

如果 master 与其他 slave 断开了连接,但还是对外提供正常服务,此时哨兵会认为 master 失效并选举新的 master,此时集群中就存在了两个 master。有的客户端会认为旧的 master 仍正常工作,继续向其写入新的数据,而一旦旧的 master 恢复连接,就会成为新的 matser 的 slave,之前新写入的数据将会丢失。

解决办法:

  1. 通过修改 redis.conf 中的 min-slaves-to-write 可以指定如果没有给指定数量的 slave 发送数据,则会直接拒绝客户端的写请求。
  2. min-slaves-max-lag 可以指定如果 slave 超过指定时间没有与自己进行 ack,也会直接拒绝客户端的写请求。

注:最新的配置文件中已经改成了 min-replicas-to-writemin-replicas-max-lag

主从模式部署建议

官方文档中给出以下建议:

“When Redis Sentinel is used for high availability, also turning off persistence on the master, together with auto restart of the process, is dangerous.”
主从模式下永远不要关闭持久化的同时配置了自动重启 ,严重情况下可能会导致 master 和所有 slave 中的数据都被删除

设想以下场景:master 与 slave1,slave2,当 master 失效后,由于 master 关闭了持久化并配置了自动重启,重启后的 master 将是空的,此时 slave1 与 slave2 进行复制,复制后的结果将是所有节点的数据都被清空。

主从模式的缺点

普通的主从(Master/Slaver)模式有以下缺点:

  • master 节点挂了以后,Redis 集群将不可提供写服务,因为剩下的 slave 不能成为 master。

为了解决主从自动切换的问题,就有了 Sentinel 模式。

哨兵(Sentinel)

“Redis Sentinel provides high availability for Redis.”
Sentinel 是 Redis 官方提供的 Redis 集群 高可用 解决方案。

Sentinel 主要提供以下四个功能:

  • 监控(Monitoring):Sentinel 会不断地检查 master 与 slave 是否在按预期地工作。
  • 通知(Notification):当被监控的 Redis 实例出现问题时,Sentinel 可以通过一个 api 来通知系统管理员或者另外的应用程序。
  • 自动故障转移(Automatic failover):如果一个 master 节点没有按照预期工作,sentinel 就会开始故障转移,把一个 slave 节点提升 master主节点,并重新配置其他的 slave 节点使用新的 master 节点,使用Redis 服务的应用程序在连接哨兵时也会被通知新的 master 地址。
  • 配置提供者(Configuration provider):Sentinel 可以作为客户端服务发现的提供者,比如客户端会连接到 Sentinels 来请求当前 master 节点的地址。当故障转移发生的时候,Sentinels 将返回新的地址。

Sentinel 模式建立于主从模式基础上,当 master 节点挂了以后,sentinel 会在 slave 中选择一个做为master。而当 master 节点重新启动后,也不会恢复原有的 master role,而是作为新的 slave 。

Redis 官方文档中建议至少部署 3 个哨兵实例,原因是在以下情况下,sentinel 将无法进行选举。

1
2
3
4
5
6
7
8
9
# 如果M1失效,R1将成为新的master
+----+ +----+
| M1 |---------| R1 |
| S1 | | S2 |
+----+ +----+
Configuration: quorum = 1

# 虽然此时可以完成重新选举,但如果M1所在的机器宕机,S1也将停止工作
# 这种情况下S2将无法进行failover

注意,无论配置了需要多少个 sentinel 同意才能判断一个节点失效, 一个 sentinel 都需要过半数的 sentinel 同意, 才能发起一次 failover。在上面的情况中,由于 S1 挂掉导致集群中只有 S2一个 sentinel ,故无法进行切换 matser。换句话说, 在只有少数 sentinel 正常运作的情况下,sentinel 是不能执行 failover 的。

选举哨兵 Leader

当一个 master 被判断为客观下线,所有的 sentinel 就需要选举出一个 leader 来执行 failover 故障转移操作(所有的 sentinel 都有被选为 leader 的资格),具体步骤如下:

  1. 当某个 sentinel 发现 master 客观下线时,就会向其他 sentinel 发送 sentinel is-master-down-by-addr 命令,其中命令参数 runid 为发起命令的 sentinel 运行 ID ,这表示该 sentinel 请求目标 sentinel 将自己设置为 leader。
  2. 收到命令的 sentinel 如果之前没有收到其他 sentinel 发送的 sentinel is-master-down-by-addr 命令,则返回一条回复(即每个 sentinel 只能投一票),回复中的 leader_runid 参数与 leader_epoch 分别记录了该 sentinel 的局部 leader 信息。
  3. 发起命令的 sentinel 收到回复后会检查自己的 configuration epoch是否与回复中的 leader_epoch 一致,如果一致则接着检查 leader_runid 是否与自己的运行 ID 一致,如果一致则表示收到命令的 sentinel 已经将发起命令的 sentinel 设为了局部 leader。
  4. 如果某个 sentinel 被半数以上的 sentinel 节点设为了局部 leader,则其就成为了 sentinel leader,下一步的 failover 操作将由该 sentinel 执行。
  5. 如果在给定时间内没有选出 leader,所有的 sentinel 在一段时间后会进行下一次选举,直至选出 sentinel leader 为止。

注意,无论每次选举结果的成功与否,一次选举后所有的 sentinel 的 configuration epoch 都会自增一次。

Failover 步骤

选出 sentinel leader 后,该节点就会对已客观下线的 matser 执行 failover 故障转移操作,该操作包括:

  1. sentinel leader 会将该 master 下的所有 slave 保存至一个列表中,按照以下规则进行过滤选出新的 master。
  2. 删除列表中所有处于下线或断线状态的 slave。
  3. 删除列表中所有最近 5 秒内没有回复过 sentinel leader info 命令的 slave。
  4. 删除列表中所有与已客观下线 master 连接断开超过 down-after-milliseconds * 10 ms 的 slave,这样可以过滤掉数据较旧的 slave。
  5. 在过滤后的列表中选出 slave-priority 值最高的 slave,如果存在该 slave 则直接返回。 slave-priority 代表 slave 的优先级,在 sentinel.conf 中可以配置,数值越小优先级越高,但如果为 0 则表示永远不会被选为新的 master。
  6. 如果不存在 slave-priority 值最高的 slave(即存在多个具有相同优先度的 slabe),此时将按照复制 offset 偏移量最大的原则进行选择(offset 大表示数据较新)。
  7. 如果 offset 仍相同,则按照运行 ID 进行排序,选出 ID 最小的 slave。
  8. 选出 slave 后,sentinel leader 会发送 slaveof no one 命令使其成为新的 master。
  9. 接着 sentinel leader 会让原 master 的所有 slave 复制新的 matser 数据,这一步通过发送 slaveof <new-master-ip> <new-master-port> 命令实现。
  10. 最后 sentinel leader 会将原 master 设为新的 master 的 slave,这步同样通过 slaveof 命令实现。

Sentinel 部署建议

官方文档中给出以下建议:

“Please deploy at least three Sentinels in three different boxes always.”
Sentinel 由于存在单个进程挂掉的可能,所以一般使用的是 sentinel 集群。在生产中 sentinel 最好不要和 Redis master 或 slave 部署在同一台机器上,不然机器挂了以后 sentinel 也会不可用,且各 sentinel 最好部署在独立的机器或虚拟机中。

集群(Cluster)

即使使用了哨兵模式,Redis 集群中的每个节点也是全量存储的,不仅浪费内存且有木桶效应。为了最大化利用内存,可以采用 Cluster 对数据进行分布式存储,即每个 Redis 节点(node)都存储不同的数据

官方文档是这样说明的:

“Redis Cluster provides a way to run a Redis installation where data is automatically sharded across multiple Redis nodes.”
Redis Cluster 提供了一种在多个 Redis 节点间自动共享数据的方式(即数据分片)。

注意,Cluster 并不能保证强一致性,仅能提供最终一致性。

“Redis Cluster is not able to guarantee strong consistency. / In general Redis + Sentinel as a whole are a an eventually consistent system.”

Cluster 的每个 node 都会使用 clusterNode 的结构来保存自己的当前状态,clusterNode 存储了 node 的创建时间、名字、configuration epoch、IP地址与端口等等信息。同时每个 node 也会为集群中的其他所有 node 都创建一个相应的 clusterNode 结构来记录其他 node 的状态。

Cluster 新节点的加入

Cluster 的主从模型

为了使在部分节点失效的情况下下 Redis Cluster 仍然可用,所以 Cluster 也使用了主从复制模型,每个 hash slot 都会有 1~N 个拷贝(为 1 就是只有一个 matser)。

Cluster 的 master 节点一般用于处理读写请求,而 slave 节点则一般只用于备份。当有请求向 slave 发起时,会直接重定向到对应 key 所在的 master 来处理。

由于半数同意原则,Redis 官方文档建议 Cluster 至少为 3 主 3 从模式。

“Note that the minimal cluster that works as expected requires to contain at least three master nodes.”

槽位指派

Redis Cluster 没有采用一致性哈希算法,而是引入了 hash slot 哈希槽的概念。Redis 集群共有 16384(2 的 14 次方) 个 hash slot,每个 master 都会分得一部分 slot,对于每个 key ,分配其槽位的算法为 hash_slot = CRC16(key) mod 16384 ,即先对 key 进行 CRC16 循环冗余校验再对 16384 取模得到放置的槽位。

这种结构很容易添加或者删除节点。 比如当新添加节点 D 时,就从节点 A, B, C 中分得部分槽到 D 上。 如果想移除节点 A,则需要将 A 中的槽移到 B 和 C 上,然后将没有任何槽的 A 从集群中移除即可。由于从一个节点将 hash slot 移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的 hash slot 数量都不会造成集群不可用的状态。

当 Cluster 中的 16384 个 slot 都有节点在处理时,Cluster 处于上线状态(ok),如果有任何一个 slot 没有得到处理,Cluster 就会处于下线状态(fail)。

重新分片

Cluster 的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的 k-v 对也会从源节点移动到目标节点(这里的重新分片不是 rehash,注意与客户端一致性 hash 分片区分开来)。

重新分片操作可以在线进行,在重新分片过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

Cluster 的重新分片操作是通过 redis-trib 来负责完成的。Redis 提供了进行重新分片的命令,而 redis-trib 则通过向源节点与目标节点发送命令来完成重新分片操作。

redis-trib 对 Cluster 的单个槽进行重新分片的步骤如下:

  1. redis-trib对目标节点发送 CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,让目标节点准备好从源节点导入(import)在这个槽中的 k-v对。

  2. redis-trib 对源节点发送 CLUSTER SETSLOT <slot> MIGRATING <target_id> 命令,让源节点准备好将这个槽中的 k-v对迁移(migrate)至目标节点。

  3. redis-trib 向源节点发送 CLUSTER GETKEYSINSLOT <slot> <count> 命令,获得 count 个属于这个槽的 k-v 对的 key。

  4. 对于步骤 3 获得的每个 key,redis-trib 都向源节点发送一个 MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,将被选中的 key 原子地从源节点迁移至目标节点。

  5. 重复执行步骤 3 和步骤 4,直到源节点保存的所有在槽中的 k-v 对都被迁移至目标节点为止。

  6. redis-trib 向 Cluster 中的任意一个节点发送 CLUSTER SETSLOT <slot> NODE <target_id> 命令,将槽指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道这个槽已经被指派给了目标节点

设计多个槽的重新分片时,则对每个槽都执行上述步骤。

如果节点 A 正在迁移槽 i 到 节点 B,当有客户端请求查找 A 中已迁移的 key 时,A 会向客户端返回一个 ACK 错误,指引客户端到 B 继续查找该 key。ACK 错误只是两个节点在槽迁移过程中采用的一种临时措施,而 MOVED 错误才真正表示槽的负责权已经从一个节点转移到另一个节点。

当节点接受请求中的 key 不在自己负责的槽位中,就会返回 MOVED 错误,MOVED 错误携带的信息可以指引客户端转向真正负责该槽位的节点。

故障检测

Cluster 这种的每个节点都会定期向其他节点发送 PING 消息,如果接收 PING 的节点没有在规定时间内返回 PONG 消息,那么发送 PING 消息的节点就会将该节点标记为疑似下线(probable fail, PFAIL)。

如果某个节点发现集群内半数以上的 master 节点都将某个 master 节点 x 标记为 PFAIL ,则这个节点 x 将会被标记为 FAIL 已下线。发现节点 x 进入 FAIL 状态的节点会向集群广播消息,所有收到这条 FAIL 消息的节点会立即将节点 x 标记为 FAIL。

Failover 步骤

当一个 slave 发现一个 master 进入 FAIL 状态时,就会触发故障转移 failover,步骤包括:

  1. 从 FAIL master 的所有 slave 中选出一个。
  2. 被选中的 slave 会执行 slaveof no one 成为新的 master。
  3. 新的 master 会撤销对 FAIL master 的所有槽指派,并将这些槽指派给自己。
  4. 新的 master 向集群广播一条 PONG 消息,让其他节点知道自己已经从 slave 变成了 master,且接管了原来 FAIL master的槽。
  5. 新的 master 开始接收所有与自己负责槽有关的命令,故障转移完成。

上述第一步中的选举新的 master 规则如下:

  • Cluster 的 configuration epoch 是一个自增计数器,初始值为 0。当集群中的某个节点开始一次 failover 时,configuration epoch的值会加 1。
  • 在每个 configuration epoch 中,每个 master 都会有一次投票机会,第一个向其他 master 请求投票 的 原有FAIL master 的 slave 将会获得该票,即票是先到先得的。

具体的选举步骤如下:

  1. 当 slave 发现自己的 master 进入 FAIL 状态时,就会向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,请求具有投票权的 master 给自己投票。
  2. 如果某个具有投票权的 master 之前尚未给其他 slave 投过票,则会向该 slave 返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示同意投票。
  3. 每个参与选举的 slave 会统计自己收到的CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息条数来确定自己获得了多少个 master 的同意。
  4. 当某个 slave 获得的票数大于所有具有投票权的 master数量一半时,它就会成为 新的 master。
  5. 如果一个 configuration epoch 中没有任何一个 slave 获得足够票数,则集群将会进入下一个 configuration epoch 并再次进行选举,直到选出新的 master 为止。

可以看出,Cluster 的选举规则和 Sentinel 是非常类似的,二者都是基于 Raft 算法的 Leader election 方法实现的。

Cluster 中节点的通信

Cluster 中的各个节点通过发送和接受 message 来进行通信,发送消息的节点为 sender,接受消息的节点为 receiver。

节点发送的 message 主要有以下五种:

  1. MEET:当 sender 接收到客户端发送的 CLUSTER MEET 命令时,sender 会向 receiver 发送 MEET 消息请求 receiver 加入到 sender 当前所处的 Cluster 中。
  2. PING:Cluster 中的每个节点会从已知节点列表中随机选出 5 个节点,每隔 1s 向这 5 个节点中最长时间没有发过 PING 消息的节点发送 PING 消息,以此检测该节点是否在线。如果这个发送 PING 消息的节点距离上次接收某个节点 PONG 消息的时间超过了 cluster-node-timeout 的一半,节点也会发送一条 PING 消息来避免对其他节点信息的更新滞。
  3. PONG:当 receiver 接收到 sender 发送的 MEETPING 消息时,就会返回一条 PONG 消息。此外,一个节点也可以通过向 Cluster 广播 PONG 消息来让其他节点立即刷新自己的相关信息,例如在failover 让其他节点知道自己成为了新的 master。
  4. FAIL:当其他节点收到某个节点发送的 FAIL 消息时,会立即将 FAIL 消息内的节点标记为 FAIL 状态。
  5. PUBLISH:当节点接收到一个 PUBLISH 命令时,就会向集群广播一条 PUBLISH 消息,所有接收到这条 PUBLISH 的节点都会执行相同的 PUBLISH 命令。
  • 本文作者: Marticles
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!