CH07-高可用性

主从复制

Redis 主从复制模式可以将主节点的数据同步给从节点,从而保障当主节点不可达的情况下,从节点可以作为后备顶上来,并且可以保障数据尽量不丢失。主从复制可以保障最终一致性

从节点可以扩展主节点的读能力,一旦主节点不能支持大规模并发量的读操作,从节点可以在一定程度上分担主节点的压力。

20220219220108

主从复制面临的问题:

  1. 当主节点发生故障的时候,需要手动的将一个从节点晋升为主节点,同时通知应用方修改主节点地址并重启应用,同时需要命令其它从节点复制新的主节点,整个过程需要人工干预。
  2. 主节点的写能力受到单机的限制。
  3. 主节点的存储能力受到单机的限制。

复制过程

一般当slave第一次启动连接master,或者“被认为是第一次连接”,是主从采用全量复制。全量复制的执行流程如下:

  1. slave redis启动. 会从redis.conf中读取master iphost
  2. 定时任务每秒检查是否有新的mater需要连接,如果发现就与master建立socket连接。
  3. slave发送ping指令到mater
  4. 如果mater配置require passslave需要发送认证给master
  5. Salve会发送sync命令到Master
  6. Master启动一个后台进程,将Redis中的数据快照rdb保存到文件中。
  7. 启动后台进程的同时,Master会将保存数据快照期间接收到的写命令缓存起来。
  8. Master完成写文件操作后,将rdb发送给Salve
  9. Salverdb保存到磁盘上,然后加载rdbredis内存中。
  10. Salve完成数据快照的恢复后,aster将这期间收集的写命令发送给Salve端。
  11. 后续Master收集到的写命令都会通过之前建立的连接. 增量发送给salve端。
20220219220824

增量复制

slave节点与master全量同步后,master节点上数据再次发生更新,就会触发增量复制。

当我们在 master 服务器增减数据的时候,就会触发 replicationFeedSalves()函数,接下来在 Master 服务器上调用的每一个命令都会使用replicationFeedSlaves() 函数来同步到Slave服务器。当然,在执行此函数之前master服务器会判断用户执行的命令是否有数据更新,如果有数据更新并且slave服务器不为空,才会执行此函数,函数主要的工作就是把用户执行的命令发送到所有的 slave服务器,让slave服务器执行。 流程如下图:

20220219220856

断点续传

断点续传或者说是断点恢复复制,也就是说 slave 因为某种原因与master断开连接了一段时间,然后又与master发生重连。redis2.8以后对于这种场景进行了优化,开始加入了PSYNC同步策略。这种策略性能一定是大于全量复制的。

  1. 从服务器向主服务器发送PSYNC命令,携带主服务器的runid和复制偏移量;
  2. 主服务器验证runid和自身runid是否一致,如不一致,则进行全量复制;
  3. 主服务器验证复制偏移量是否在积压缓冲区内,如不在,则进行全量复制;
  4. 如都验证通过,则主服务器将保持在积压区内的偏移量后的所有数据发送给从服务器,主从服务器再次回到一致状态。
20220219220940

PSYNC 核心参数

断点续传的几个核心参数,offsetbacklogrunid。这三个参数在 PSYNC 中起到了至关重要的作用,下面我们来一一介绍一下。

  • offet复制偏移量 , offset是用来记录masterlslave某个时段的数据版本状态的,slave每秒会向master上报offsetmaster保存下来,当触发 PSYNC 时再拿来和masteroffset数据作对比。说白了,它就是记录数据在某一时刻的快照,用来对比 master 和 slave 数据差异用的。
  • backlog积压缓冲区
    1. 这个也是一个非常核心的参数,它默认大小为1mb,复制积压缓冲区是由Master维护的一个固定长度的FIFO队列,它的作用是缓存已经传播出去的命令。当Master进行命令传播时,不仅将命令发送给所有Slave,还会将命令写入到复制积压缓冲区里面。
    2. 全量复制的时候,master的数据更新(读写操作,主动过期删除等)会临时存放在backlog中待全量复制完成后增量发到slave,必须为此保留足够的空间。
    3. 断点续传时,backlog会存下slave断开连接后,master变更的数据。当然由于它大小有限制,而且先进先出特性,所以达到缓冲大小后会弹出老数据。这样,就可以把它作为一个衡量执行sync还是psync的一个标准(backlog = offset : 部分同步,backlog < offset 执行全量同步)。一般为了避免,大规模全量复制,我们都会给一个恰当的值,根据公式second*write_size_per_second来估算:其中second为从服务器断线后重新连接上主服务器所需的平均时间(以秒计算);而write_size_per_second则是主服务器平均每秒产生的写命令数据量(协议格式的写命令的长度总和);
  • master run id, master唯一标示,slave连接master时会传runidmaster每次重启runid都发生变化,当slave发现masterrunid变化时都会触发全量复制流程。

优缺点

优点:

  1. 高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题;
  2. 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。

缺点:

  1. 故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐;
  2. 主库的写能力受到单机的限制,可以考虑分片;
  3. 主库的存储能力受到单机的限制,可以考虑Pika
  4. 原生复制的弊端在早期的版本中也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IOCPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。

Redis Sentinel

20220219220207

当主节点出现故障时,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。RedisSentinel 是一个分布式架构,其中包含若干个 Sentinel 节点和 Redis 数据节点,每个 Sentinel 节点会对数据节点和其余 Sentinel 节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他的 Sentinel 节点进行协商,当大多数 Sentinel 节点都认为主节点不可达时,它们会选举一个 Sentinel 节点来完成自动故障转移的工作,同时会将这个变化实时通知给 Redis 应用方。整个过程是自动的,不需要人工干预,解决了Redis 的高可用问题。

Redis Sentinel 包含了若干个 Sentinel 节点,这样做也带来了两个好处:

  1. 对节点的故障判断是由多个 Sentinel 节点共同完成,这样可以有效的防止误判。
  2. Sentinel 节点集合是由若干个 Sentinel 节点组成的,这样即使个别 Sentinel 节点不可用,整个Sentinel节点集合依然是健壮的。

Redis Sentinel 具有以下几个功能:

  1. 监控:Sentinel 会定期检测 Redis 数据节点、其余 Sentinel 节点是否可到达
  2. 通知:Sentinel 会将故障转移的结果通知给应用方。
  3. 主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系。
  4. 配置提供者:在 RedisSentinel 结构中,客户端在初始化的时候连接的是 Sentinel 节点集合,从中获取主节点信息。

RedisSentinel 处理流程:

  1. Sentinel 集群通过给定的配置文件发现 master,启动时会监控 master。通过向 master 发送 info 信息获得该服务器下面的所有从服务器。
  2. Sentinel 集群通过命令连接向被监视的主从服务器发送 hello 信息(每秒一次),该信息包括 Sentinel 本身的 IP、端口、id 等内容,以此来向其他 Sentinel 宣告自己的存在。
  3. Sentinel 集群通过订阅连接接收其他 Sentinel 发送的 hello 信息,以此来发现监视同一个主服务器的其他 Sentinel;集群之间会互相创建命令连接用于通信,因为已经有主从服务器作为发送和接收 hello 信息的中介,Sentinel 之间不会创建订阅连接。
  4. Sentinel 集群使用 ping 命令来检测实例的状态,如果在指定的时间内(down-after-milliseconds)没有回复或则返回错误的回复,那么该实例被判为下线。
  5. 当 failover 主备切换被触发后,failover 并不会马上进行,Sentinel 中的大多数 Sentinel 授权后才可以进行 failover,即进行 failover 的 Sentinel 会去获得指定 quorum 个的 Sentinel 的授权,成功后进入 ODOWN 状态。如在 5 个 Sentinel 中配置了 2 个 quorum,等到 2 个 Sentinel 认为 master 死了就执行 failover。
  6. Sentinel 向选为 master 的 slave 发送 SLAVEOF NO ONE 命令,选择 slave 的条件是 Sentinel 首先会根据 slaves 的优先级来进行排序,优先级越小排名越靠前。如果优先级相同,则查看复制的下标,哪个从 master 接收的复制数据多,哪个就靠前。如果优先级和下标都相同,就选择进程 ID 较小的。
  7. Sentinel 被授权后,它将会获得宕掉的 master 的一份最新配置版本号 (config-epoch),当 failover 执行结束以后,这个版本号将会被用于最新的配置,通过广播形式通知其它 Sentinel,其它的 Sentinel 则更新对应 master 的配置。

1 到 3 是自动发现机制:

  • 以 10 秒一次的频率,向被监视的 master 发送 info 命令,根据回复获取 master 当前信息。
  • 以 1 秒一次的频率,向所有 redis 服务器、包含 Sentinel 在内发送 PING 命令,通过回复判断服务器是否在线。与主节点,从节点,其余 Sentinel 都建立起连接,实现了对每个节点的监控。
  • 以 2 秒一次的频率,通过向所有被监视的 master,slave 服务器发送当前 Sentinel master 信息的消息。这个定时任务可以完成以下两个工作:
    • 发现新的 Sentinel 节点:通过订阅主节点的 Sentinel:hello 了解其他 Sentinel 节点信息。如果是新加入的 Sentinel 节点,将该 Sentinel 节点信息保存起来,并与该 Sentinel 节点创建连接
    • Sentinel 节点之间交换主节点状态,作为后面客观下线以及领导者选举的依据

4 是检测机制,5 和 6 是 failover 机制,7 是更新配置机制

Leader 选举

其实在 sentinels 故障转移中,仍然需要一个 Leader 来调度整个过程:master 的选举以及 slave 的重配置和同步。当集群中有多个 sentinel 实例时,如何选举其中一个 sentinel 为 leader 呢?

在配置文件中 can-failover、quorum 参数,以及 is-master-down-by-addr 指令配合来完成整个过程。

  1. can-failover 用来表明当前 sentinel 是否可以参与 failover 过程,如果为 YES 则表明它将有能力参与 Leader 的选举,否则它将作为 Observer ,observer 参与 leader 选举投票但不能被选举;
  2. quorum 不仅用来控制 master ODOWN 状态确认,同时还用来选举 leader 时最小「赞同票」数;
  3. is-master-down-by-addr,在上文中以及提到,它可以用来检测 ip + port 的 master 是否已经处于 SDOWN 状态,不过此指令不仅能够获得 master 是否处于 SDOWN,同时它还额外的返回当前 sentinel 本地「投票选举」的 Leader 信息 (runid);

每个 sentinel 实例都持有其他的 sentinels 信息,在 Leader 选举过程中(当为 leader 的 sentinel 实例失效时,有可能 master server 并没失效,注意分开理解),sentinel 实例将从所有的 sentinels 集合中去除 can-failover = no 和状态为 SDOWN 的 sentinels,在剩余的 sentinels 列表中按照 runid 按照「字典」顺序排序后,取出 runid 最小的 sentinel 实例,并将它「投票选举」为 Leader,并在其他 sentinel 发送的 is-master-down-by-addr 指令时将推选的 runid 追加到响应中。每个 sentinel 实例都会检测 is-master-down-by-addr 的响应结果,如果「投票选举」的 leader 为自己,且状态正常的 sentinels 实例中,赞同者的自己的 sentinel 个数不小于(>=) 50% + 1,且不小与 ,那么此 sentinel 就会认为选举成功且 leader 为自己。

在 sentinel.conf 文件中,我们期望有足够多的 sentinel 实例配置 can-failovers,这样能够确保当 leader 失效时,能够选举某个 sentinel 为 leader,以便进行 failover。如果 leader 无法产生,比如较少的 sentinels 实例有效,那么 failover 过程将续。

failover 过程

在 Leader 触发 failover 之前,首先 wait 数秒(随机 0~5),以便让其他 sentinel 实例准备和调整,如果一切正常,那么 leader 就需要开始将一个 salve 提升为 master,此 slave 必须为状态良好(不能处于 SDOWN/ODOWN 状态)且权重值最低(redis.conf中)的,当 master 身份被确认后,开始 failover:

  1. +failover-triggered: Leader 开始进行 failover,此后紧跟着 +failover-state-wait-start ,wait 数秒。
  2. +failover-state-select-slave: Leader 开始查找合适的 slave
  3. +selected-slave: 已经找到合适的 slave
  4. +failover-state-sen-slaveof-noone: Leader 向 slave 发送 slaveof no one 指令,此时 slave 已经完成角色转换,此 slave 即为 master
  5. +failover-state-wait-promotition: 等待其他 sentinel 确认 slave
  6. +promoted-slave:确认成功
  7. +failover-state-reconf-slaves: 开始对 slaves 进行 reconfig 操作。
  8. +slave-reconf-sent: 向指定的 slave 发送 slaveof 指令,告知此 slave 跟随新的 master
  9. +slave-reconf-inprog: 此 slave 正在执行 slaveof + SYNC 过程,如过 slave 收到 +slave-reconf-sent 之后将会执行 slaveof 操作。
  10. +slave-reconf-done: 此 slave 同步完成,此后 leader 可以继续下一个 slave 的 reconfig 操作。循环步骤 10
  11. +failover-end: 故障转移结束
  12. +switch-master:故障转移成功后,各个 sentinel 实例开始监控新的 master。

总结

Redis-Sentinel 是 Redis 官方推荐的高可用性解决方案,Redis-sentinel 本身也是一个独立运行的进程,它能监控多个 master-slave 集群,发现 master 宕机后能进行自动切换。Sentinel 可以监视任意多个主服务器(复用),以及主服务器属下的从服务器,并在被监视的主服务器下线时,自动执行故障转移操作。

20220219220545

为了防止 sentinel 的单点故障,可以对 sentinel 进行集群化,创建多个 sentinel。

20220219220607

Redis Cluster

Redis 集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis 实现, 集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset)。

Redis 集群中不存在中心(central)节点或者代理(proxy)节点, 集群的其中一个主要设计目标是达到线性可扩展性(linear scalability)。

Redis 集群提供了一种运行 Redis 的方式,其中数据在多个 Redis 节点间自动分区。Redis 集群还在分区期间提供一定程度的可用性,即在实际情况下能够在某些节点发生故障或无法通信时继续运行。但是,如果发生较大故障(例如,大多数主站不可用时),集群会停止运行。

集群模型

20220219221214

所有的节点通过服务通道直接相连,各个节点之间通过二进制协议优化传输的速度和带宽。

客户端与节点之间通过 ascii 协议进行通信。

客户端与节点直连,不需要中间 Proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

尽管这些节点彼此相连,功能相同,但是仍然分为两种节点:master 和 slave。

节点间传递信息

20220219221317

各个节点之间通过 PING-PONG 机制通信,下面是一段关于 PING-PONG 机制的会话”内容”。

节点M:PING,嘿,朋友你好吗?我是 XYZ 哈希槽的 master ,配置信息是 FF89X1JK。

节点N:PONG,我很好朋友,我也是 XYZ 哈希槽的 master ,配置信息是 FF89X1JK。

节点M:我这里有一些关于我最近收到的其他节点的信息 ,A 节点回复了我的 PING 消息,我认为 A 节点是正常的。B 没有回应我的消息,我猜它现在可能出问题了,但是我需要一些 ACK(Acknowledgement) 消息来确认。

节点N:我也想给你分享一些关于其它节点的信息,C 和 D 节点在指定的时间内回应了我, 我认为它们都是正常的,但是 B 也没有回应我,我觉得它现在可能已经挂掉了。

每个节点会向集群中的其他节点发送节点状态信息,如果某个节点挂掉停止了服务,那么会执行投票容错机制,关于这个机制,会在下面讲到。

Hash 槽(slot)

Redis 集群不使用一致的散列,而是一种不同的分片形式,其中每个键在概念上都是我们称之为散列槽的一部分,目的是使数据均匀的存储在诸多节点中。这点类似于 HashMap 中的桶(bucket)。

20220219221405

Redis 集群中有 16384 个散列槽,为了计算给定密钥的散列槽,Redis 对 key 采用 CRC16 算法,以下是负责将键映射到槽的算法:

slot = crc16(key) mod NUMER_SLOTS

例如,你可能有 3 个节点,其中一个集群:

  • 节点 A 包含从 0 到 5500 的散列槽。
  • 节点 B 包含从 5501 到 11000 的散列槽。
  • 节点 C 包含 从 11001 到 16383 的散列槽。

Hash 槽可以轻松地添加和删除集群中的节点。例如,如果我想添加一个新节点 D,我需要将节点 A,B,C 中的一些散列槽移动到 D。同样,如果我想从节点 A 中删除节点 A,可以只移动由 A 服务的散列槽到 B 和 C。当节点 A 为空时,可以将它从群集中彻底删除。

20220219221447
  1. 对象保存到 Redis 之前先经过 CRC16 哈希到一个指定的 Node 上,例如 Object4 最终 Hash 到了 Node1 上。
  2. 每个 Node 被平均分配了一个 Slot 段,对应着 0-16384,Slot 不能重复也不能缺失,否则会导致对象重复存储或无法存储。
  3. Node 之间也互相监听,一旦有 Node 退出或者加入,会按照 Slot 为单位做数据的迁移。例如 Node1 如果掉线了,0-5640 这些 Slot 将会平均分摊到 Node2 和 Node3 上,由于 Node2 和 Node3 本身维护的 Slot 还会在自己身上不会被重新分配,所以迁移过程中不会影响到 5641-16384Slot 段的使用。
20220219221600

想扩展并发读就添加 Slaver,想扩展并发写就添加 Master,想扩容也就是添加 Master,任何一个 Slaver 或者几个 Master 挂了都不会是灾难性的故障。

简单总结下哈希 Slot 的优缺点:

缺点:每个 Node 承担着互相监听、高并发数据写入、高并发数据读出,工作任务繁重

优点:将 Redis 的写操作分摊到了多个节点上,提高写的并发能力,扩容简单。

容错

20220219221622
  • 集群中的节点不断的 PING 其他的节点,当一个节点向另一个节点发送 PING 命令, 但是目标节点未能在给定的时限内回复, 那么发送命令的节点会将目标节点标记为 PFAIL(possible failure,可能已失效)。
  • 当节点接收到其他节点发来的信息时, 它会记下那些被其他节点标记为失效的节点。 这被称为失效报告(failure report)。
  • 如果节点已经将某个节点标记为 PFAIL , 并且根据节点所收到的失效报告显式, 集群中的大部分其他主节点也认为那个节点进入了失效状态, 那么节点会将那个失效节点的状态标记为 FAIL
  • 一旦某个节点被标记为 FAIL , 关于这个节点已失效的信息就会被广播到整个集群, 所有接收到这条信息的节点都会将失效节点标记为 FAIL

简单来说, 一个节点要将另一个节点标记为失效, 必须先询问其他节点的意见, 并且得到大部分主节点的同意才行。

如果被标记为 FAIL 的是从节点, 那么当这个节点重新上线时, FAIL 标记就会被移除。 一个从节点是否处于 FAIL 状态, 决定了这个从节点在有需要时能否被提升为主节点。

如果一个主节点被打上 FAIL 标记之后, 经过了节点超时时限的四倍时间, 再加上十秒钟之后, 针对这个主节点的槽的故障转移操作仍未完成, 并且这个主节点已经重新上线的话, 那么移除对这个节点的 FAIL 标记。在不符合上面的条件后,一旦某个主节点进入 FAIL 状态, 如果这个主节点有一个或多个从节点存在, 那么其中一个从节点会被升级为新的主节点, 而其他从节点则会开始对这个新的主节点进行复制。

优缺点

优点:

  1. 无中心架构;
  2. 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布;
  3. 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
  4. 高可用性:部分节点不可用时,集群仍可用。通过增加Slavestandby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成SlaveMaster的角色提升;
  5. 降低运维成本,提高系统的扩展性和可用性。

缺点:

  1. Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”
  2. 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
  3. 数据通过异步复制,不保证数据的强一致性。
  4. 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
  5. Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。
  6. Key批量操作限制,如使用msetmget目前只支持具有相同slot值的Key执行批量操作。对于映射为不同slot值的Key由于Keys不支持跨slot查询,所以执行msetmgetsunion等操作支持不友好。
  7. Key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个Key分布于不同的节点上时无法使用事务功能。
  8. Key作为数据分区的最小粒度,不能将一个很大的键值对象如hashlist等映射到不同的节点。
  9. 不支持多数据库空间,单机下的redis可以支持到 16 个数据库,集群模式下只能使用 1 个数据库空间,即 db 0。
  10. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
  11. 避免产生hot-key,导致主库节点成为系统的短板。
  12. 避免产生big-key,导致网卡撑爆、慢查询等。
  13. 重试时间应该大于cluster-node-time时间。
  14. Redis Cluster不建议使用pipelinemulti-keys操作,减少max redirect产生的场景。

Codis

Codis 是一个代理中间件,如下图,Codis 在系统的位置是这样的。

20220219221752

Codis分为四个部分,分别是Codis Proxy (codis-proxy)、Codis Dashboard (codis-config)、Codis Redis(codis-server)和ZooKeeper/Etcd. Codis就是起着一个中间代理的作用,能够把所有的Redis实例当成一个来使用,在客户端操作着SDK的时候和操作Redis的时候是一样的,没有差别。 因为Codis是一个无状态的,所以可以增加多个Codis来提升QPS,同时也可以起着容灾的作用。

分片原理

Codis中,Codis会把所有的key分成 1024 个槽,这 1024 个槽对应着的就是Redis的集群,这个在Codis中是会在内存中维护着这 1024 个槽与Redis实例的映射关系。这个槽是可以配置,可以设置成 2048 或者是 4096 个。看你的Redis的节点数量有多少,偏多的话,可以设置槽多一些。 Codiskey的分配算法,先是把key进行CRC32 后,得到一个 32 位的数字,然后再hash%1024后得到一个余数,这个值就是这个key对应着的槽,这槽后面对应着的就是redis的实例。(可以思考一下,为什么 Codis 很多命令行不支持,例如 KEYS 操作)

CRC32:CRC本身是“冗余校验码”的意思,CRC32则表示会产生一个32bit(8 位十六进制数)的校验值。由于CRC32产生校验值时源数据块的每一个bit(位)都参与了计算,所以数据块中即使只有一位发生了变化,也会得到不同的CRC32值。

Codis中Key的算法代码如下:

hash = crc32(command.key)
slot_index = hash % 1024
redis = slots[slot_index].redis
redis.do(command)

槽位同步

思考一个问题:如果这个 Codis 节点只在自己的内存里面维护着槽位与实例的关系,那么它的槽位信息怎么在多个实例间同步呢?

Codis 把这个工作交给了 ZooKeeper 来管理,当 Codis 的 Codis Dashbord 改变槽位的信息的时候,其他的 Codis 节点会监听到 ZooKeeper 的槽位变化,会及时同步过来。如图:

20220219221951

节点扩容

思考一个问题:在 Codis 中增加了 Redis 节点后,槽位的信息怎么变化,原来的 key 怎么迁移和分配?如果在扩容的时候,这个时候有新的 key 进来,Codis 的处理策略是怎么样的?

因为Codis是一个代理中间件,所以这个当需要扩容Redis实例的时候,可以直接增加redis节点。在槽位分配的时候,可以手动指定Codis Dashbord来为新增的节点来分配特定的槽位。

Codis中实现了自定义的扫描指令SLOTSSCAN,可以扫描指定的slot下的所有的key,将这些key迁移到新的Redis的节点中(话外语:这个是Codis定制化的其中一个好处)。

首先,在迁移的时候,会在原来的Redis节点和新的Redis里都保存着迁移的槽位信息,在迁移的过程中,如果有key打进将要迁移或者正在迁移的旧槽位的时候,这个时候Codis的处理机制是,先是将这个key强制迁移到新的Redis节点中,然后再告诉Codis,下次如果有新的key的打在这个槽位中的话,那么转发到新的节点。代码策略如下:

slot_index = crc32(command.key) % 1024
if slot_index in migrating_slots:
    do_migrate_key(command.key)  # 强制执行迁移
    redis = slots[slot_index].new_redis
else:
    redis = slots[slot_index].redis
redis.do(command)

自动均衡策略

面对着上面讲的迁移策略,如果有成千上万个节点新增进来,都需要我们手动去迁移吗?那岂不是得累死啊。当然,Codis也是考虑到了这一点,所以提供了自动均衡策略。自动均衡策略是这样的,Codis 会在机器空闲的时候,观察Redis中的实例对应着的slot数,如果不平衡的话就会自动进行迁移。

Codis 的牺牲

因为CodisRedis的基础上的改造,所以在Codis上是不支持事务的,同时也会有一些命令行不支持,在官方的文档上有(Codis不支持的命令) 官方的建议是单个集合的总容量不要超过 1M,否则在迁移的时候会有卡顿感。在Codis中,增加了proxy来当中转层,所以在网络开销上,是会比单个的Redis节点的性能有所下降的,所以这部分会有些的性能消耗。可以增加proxy的数量来避免掉这块的性能损耗。

MGET 的过程

思考一个问题:如果熟悉 Redis 中的 MGET、MSET 和 MSETNX 命令的话,就会知道这三个命令都是原子性的命令。但是,为什么 Codis 支持 MGET 和 MSET,却不支持 MSETNX 命令呢?

20220219222034

原因如下:

Codis中的MGET命令的原理是这样的,先是在Redis中的各个实例里获取到符合的key,然后再汇总到Codis中,如果是MSETNX的话,因为key可能存在在多个Redis的实例中,如果某个实例的设值成功,而另一个实例的设值不成功,从本质上讲这是不成功的,但是分布在多个实例中的Redis是没有回滚机制的,所以会产生脏数据,所以 MSETNX 就是不能支持了。