Redis Cluster 实现原理

本文基本上是 redis cluster 规范 的翻译(意译),希望能对您理解 redis cluster 的原理有所帮助。若发现哪里说的不明白,或者有错误,希望能够在评论里指出,不胜感激。

    • 主要特性和设计原则
      • 设计目标
      • Redis Cluster 支持的操作
      • Redis Cluster 协议中 Client 和 Server 的作用
      • 写安全
      • 可用性
      • 性能
      • 为什么没有 merge 操作
    • Redis Cluster 的主要组成部分
      • keys distribution model
      • 哈希标签 hash tags
      • 集群节点属性
      • 集群总线
      • 集群拓扑结构
      • 节点间的握手
    • 重定向和重新分片
      • MOVED 重定向
      • 运行时集群配置变更
      • ASK 重定向
      • 集群 client
      • 多键操作
      • 用 slave 节点扩展读请求
    • 容错
      • 心跳和 gossip 消息
      • 心跳包内容
      • 错误检测
        • PFAIL flag
        • FAIL flag
    • 配置处理,传播和失败转移
      • 集群 current epoch
      • configuration epoch
      • Slave 选举和提升
      • Slave Rank
      • master 回应 slave 的选举请求
      • 在分隔(partition)情况下 configEpoch 的实际作用
      • 哈希槽配置的传播
      • 节点重新加入集群
      • 备份迁移
      • 备份迁移算法
      • configEpoch 冲突解决算法
      • 节点重置
      • 从集群删除节点
    • 发布-订阅
    • 附录
      • 附录A: CRC16 算法的 C 语言实现

主要特性和设计原则

设计目标

Redis Cluster 是 Redis 的一个分布式实现。它具有以下设计重点:

  • 高性能,可水平扩展到1000个节点。没有代理;异步备份;冲突时没有 merge 操作。
  • 可接受的写安全。系统尽最大可能保证连接到大部分 master 节点的 client 的写操作的完整性。不过数据仍然有一个丢失窗口,在这个窗口内写操作有可能成功返回但不保证完整性。如果 client 只连接到小部分的 master 节点,这个丢失窗口更大。
  • 可用性。只要满足这两个条件可用性就能保证:a. 大部分 master 节点可达;b. 每个 master 不可达的节点都有至少一个可达的 slave。

Redis Cluster 支持的操作

  • 实现单机版 redis 的所有单 key 命令
  • 多 key 操作,只要 key 在同一节点上就支持
  • 支持 hash tags. 保证特定的 key 映射到同一机器.
  • 多 key 操作在人工 resharding 的时候可能某些时候会不可用

Redis Cluster 协议中 Client 和 Server 的作用

集群的节点作用:

  • 存储数据
  • 维护集群状态. 包含从 key 到具体节点的映射
  • 自动发现其他节点
  • 检测不工作的节点
  • 错误发生时,必要情况下将 slave 节点提升到 master

集群内通过 Redis Cluster Bus 协议来交互。每一个节点都维护同其他所有节点的 TCP 连接,而节点之间通过 gossip 协议来交互集群的信息。

集群节点不能代理请求,而是通过向 client 返回重定向信息来告知 client 重定向到其他节点,client 再向其他节点发送请求. client 可以维护自身的 key -> node 映射来提升性能.

写安全

异步备份;last failover wins - 最后一个成为 master 的机器上的数据生效。

Redis Cluster 不保证 100% 的写安全。在 partition (可以认为集群被切割成互不相连的几部分) 发生时,会有一个短暂的时间窗口会丢失 write 请求写入的数据。而按照 client 连接的 partition 的情况,又分为两种情况。

第一种情况,client 连接到具有多数 master 节点的部分:

  • write 写入到 master
  • master 返回确认 给 client
  • master 挂了(master 尚未同步数据到 slave)
  • 一段时间后,slave 被提升为新的master

在这种情况下尚未同步到 slave 的数据丢失了:

第二种情况,client 连接到具有少数 master 节点的部分。

  • write 写入到 master
  • master 被替换
  • master 连不上大多数 master,开始拒绝请求

在这种情况下,master 拒绝之前的写操作的内容都丢失了。因此丢失窗口更大。

除此之外,还有一种理论上可能有的情况:

  • master 丢失
  • master 被 failover
  • master 重新可达
  • client 写入到该 master

这种情况一般不会发生,因为 master 被 failover,意味着已经过了一定时间,它也已经开始拒绝写请求了。同时这种情况也要求 client 维护的信息是旧的才可能发生。

可用性

分隔的少部分是不会达到可用状态的。

如果集群大部分 master 都可达,并且每个不可达的 master 都至少有一个可达的 slave。则在 NODE_TIMEOUT 时间段后,会开始 slave failover master 的操作。操作完成之后集群达到可用状态。

比如,若一个集群有 N 个 master 节点,每个 master 节点都有唯一一个 slave 节点。则任意一个节点失败都不会使集群达到不可用状态。若有两个节点失败,则集群有 1/(2*N - 1) 的可能性集群达到不可用状态。

Redis Cluster 一个名为 replica migration 的新 feature 会迁移没有备份的 master 的备份,因此在一定程度上可以提高可用性。

性能

Redis Cluster 为提升性能做了一些事情:

  • 不用 proxy,而用 redirect,让 client 自己去联系相应的节点
  • client 保留集群状态的副本,一般可以直接触达要联系的节点
  • 异步备份机制,不需要等待备份完成即返回
  • 多 key 操作只操作本机的 key,数据无需在节点之间移动( resharding 时候除外)

Redis Cluster 设计的时候,将高性能、扩展性放在首位;提供弱但合理的数据安全和可用性。

为什么没有 merge 操作

merge 操作用于解决冲突,如 git 中的 merge。

Redis Cluster 不提供 merge 操作,主要是因为 redis 的应用场景通常没有这个需求。而实现 merge 操作又会比较繁琐~。

Redis Cluster 的主要组成部分

keys distribution model

将键空间划分为 16384 (2^14)个 slot,这也就意味着 redis cluster 最多支持 16384 个 master。

每个主节点处理若干个 slot;在稳定状态,每个 slot 由唯一 master 节点提供服务。

稳定状态 - cluster 不处于重新配置过程中。

哈希 slot 计算方法如下(hash tag 是个例外):

HASH_SLOT = CRC16(key) mod 16384

CRC16 实际上是循环冗余校验算法,是数据通讯中常用的一种校验算法。这里用它来计算哈希槽。

哈希标签 hash tags

多键操作时,需要多个键位于同一 slot 中。这是通过 hash tags 来实现。

如果key 包含 “{…}” 这样的模式,则只有 “{” 和 “}” 之间的内容会用来计算 hash slot。由于key 里面可能包含多个 “{” 或 “}”,因此计算规则如下:

  • key 包含 “{“
  • key 中在 “{” 之后有 “}”
  • key 中第一个 “{” 和 第一个 “}” 之间有至少一个字符

看几个例子

  • 两个 key:{user1000}.following 和 {user1000}.followers 都使用 user1000 来计算hash slot,因此位于同一个 slot 中
  • 键 foo{}{bar} 作为正常的键来计算 hash slot,即整个字符串. 因为第一个大括号之内没有内容
  • 键 foo{{bar}}zap中,”{bar” 用于计算 hash slot
  • 键 foo{bar}{zap} 中,用 “bar” 来计算
  • 若键以 “{}” 开头,则一定会被作为整体计算 hash slot。二进制数据作为键名的时候比较有用

集群节点属性

node ID : 一个 160 位随机数的16进制表示,显示出来是40个字符. node ID 保存在节点的配置文件中,并会永久使用.

node ID 是集群用来区分不同节点的依据。IP 和端口可能变化,但只要 node ID不变,集群就会识别为同一个节点。

node ID 是节点的唯一全局一致的信息。

每个节点还会维护其知道的集群中其他节点的如下信息:

  • node ID
  • 节点的 IP 和 port
  • 一些 flags
  • 对应的 master 节点(若是 slave)
  • 上次 ping 该节点的时间
  • 上次收到该节点 pong 的时间
  • 节点的当前的 configEpoch (后文解释)
  • 连接状态
  • 该节点服务的 hash slots

cluster nodes 命令可以获取呢集群节点的状态:

$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095

上面显示顺序为: node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots

集群总线

集群的节点使用一个额外的 TCP 端口进行集群节点之间的内部通讯。端口号同接受 client 连接所使用的端口号关联。比如,若 redis 使用 6379 端口接受 client 连接,则 6379 + 10000,即 16379 端口就是节点用于集群内通讯的端口。

集群之间的通讯使用集群总线和集群总线协议。这是一个内部协议,目前没有开放文档。不过可以通过源码来看。

集群拓扑结构

redis cluster 是一个全连接的网状结构,每个节点同其他所有节点之间都有 TCP 连接。

比如,若集群有 N 个节点,则每个节点都有 N-1 个外向的TCP 连接,同时还有 N-1个内向的TCP连接。

集群的节点间通过 gossip 协议和一个配置更新策略来保证正常状态下节点交换的数据不随着节点数指数增加。

节点间的握手

集群中节点对消息的处理:

  • 节点接受所有集群总线上的连接请求
  • 节点接收到 ping 请求后会回复,无论请求来源是否可信
  • 对其他类型的请求,仅当请求来源同属同一集群的时候才处理

节点仅在两种情况下接受另一节点为本集群节点:

  • 收到 MEET 消息. 这种消息只能由系统管理员通过命令触发
  • 信任的节点通过 gossip 消息告知本节点它信任的某个节点的信息. A 信任 B,B 信任 C,则 A 也 信任 C.

因此,只要向集群中添加节点,他们之间最终都会自动形成一个全连接图。

重定向和重新分片

MOVED 重定向

client 可以向集群中的所有节点发送 query 请求. 节点收到请求后,首先分析请求并解析哪个机器为相应的 slots 提供服务。若是本节点提供服务,则直接返回结果给 client;否则,则会返回一个 MOVED 错误,并将相应的 slot 和对应机器返回给 client。如下:

GET x
-MOVED 3999 127.0.0.1:6381

集群内部节点之间是通过 node ID 标识的,但为了方便,对 client 的接口仍然使用 IP+port.

client 收到 MOVED 请求后,应该讲 SLOT 跟 IP+port 的对应关系存储起来,以便下次使用。

client 收到 MOVED 请求后,也可以通过 CLUSTER NODES 命令或 CLUSTER SLOTS 命令更新整个集群的分布信息。这主要是由于重新配置的时候,经常会有多个 slot 的配置同时发生变更。

完整的集群 client 实现还应该实现 ASK 重定向。我们后文会具体讲到。

运行时集群配置变更

新增、删除节点,以及集群的再平衡都使用了一个操作:将 slot 从一个节点移动到另一个节点。

  • 新增节点. 将一些 slots 从已存在节点移动到新节点
  • 删除节点. 将当前节点的 slots 移动到其他节点
  • 再平衡. 将一些 slots 从一个节点移动到另一个节点

集群中的 slots 迁移可能用到以下命令:

  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node

ADDSLOTS 命令经常在集群创建时使用,指定某个节点服务哪些 slots。
DELSLOTS 常用语手动修改集群配置,或用语调试任务。实际中很少使用。
SETSLOT 将某个 slot 分配给某个 master 节点。

我们下面详细说一下 slot 迁移,这个过程使用了 MIGRATING 和 IMPORTING 命令。如将 slot 从节点 A 迁移到 B:

  • 向A 发送 CLUSTER SETSLOT 8 MIGRATING B
  • 向B 发送 CLUSTER SETSLOT 8 IMPORTING A
  • 节点 A 上的 slot 8 处于 MIGRATING 状态。因此对 slot 8 的请求,A 只接受 key 存在于 A 的 slot 8 上的请求;若 key 在 A 上不存在,则返回 ASK 重定向到 B
  • 节点 B 上的 slot 8 处于 IMPORTING 状态。因此对 slot 8 的请求,B 只接受 ASKING 命令之后的请求。若没有 ASKING 命令,则会通过 MOVED 重定向到 A

slot 的迁移一般是通过 redis-trib 程序来执行的。该程序首先执行 CLUSTER GETKEYSINSLOT slot count 来获取若干个 key,然后遍历向 A 发送 MIGRATE 命令:

MIGRATE target_host target_port key target_database id timeout

MIGRATE 时 A 会连接到目标主机,发送相应 key 的信息,一旦收到 OK code 返回值,则从本地库中将该 key 删除。从外部 client 的视角来看,一个key 只能存在 A 或者 B 其中之一上。

迁移过程完成之后,SETSLOT NODE 命令会被发送到这两个节点上。一般来说也会发送到集群上所有节点上,从而使集群的新配置尽快生效,而不必等待自然传播来将配置同步到整个集群。

ASK 重定向

已经有了 MOVED 重定向,为什么还需要 ASK 重定向呢?MOVED 意味着我们认为请求的 hash slot 会永久的由另一个节点提供服务;而 ASK 则仅仅意味着下一个请求需要发送给指定的节点。

由于下一个到该 slot 的请求数据可能仍在 A 上,因此每次对该 slot 的请求都要先访问 A,若A 上没有则通过 ASK 重定向到 B。由于只有一个 slot 会发生这种情况,因此性能不成问题。

B 节点需要保证只接受尝试访问过 A 之后的针对该 slot 的请求。因此访问 B 节点该 slot 数据前,需要先发送 ASKING 命令。若一个 client 有bug,没有先发送 ASKING,则 B 会拒绝服务并 MOVED 重定向到 A,而集群本身不会出问题。

迁移完成之后,再次请求 A 数据的该 slot 将会使用 MOVED 重定向到 B。

集群 client

集群 client 的实现是可以不储存 slot 配置信息的,但是这样做会非常低效。

一般来讲 client 都需要保存 slot 的配置信息,但不要求实时性。如果配置过期,client 请求的时候会受到 MOVED 重定向信息,触发更新本地的 slot 配置信息。

CLUSTER SLOTS 命令可以获取 slot 配置信息。

127.0.0.1:7000> cluster slots
1) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 7001
   4) 1) "127.0.0.1"
      2) (integer) 7004

第一行 5461 表示起始的 slot,第二行代表结束 slot(包含)。第三部分代表 master 节点信息。第四部分代表 slave,可以用来做 readonly。

多键操作

若使用 hash tag,则可以自由使用多键操作。

需要注意多键操作在 resharding 的时候可能不可用。更进一步讲,若 resharding 的时候造成部分迁移,则多键操作当前不可用,访问时会返回 TRYAGAIN 错误,客户端可以过一会儿再访问。若resharding 的时候所有key 都在同一个节点,则多键操作也还是可用的。

用 slave 节点扩展读请求

正常情况下, slave 若收到请求则会重定向到相应的 master。但是可以通过 READONLY 命令指定 slave 提供读请求服务。

READONLY 意味着可能读到过期数据,并且不会操作写。若业务不敏感,可以开启 READONLY 模式。

容错

心跳和 gossip 消息

集群内的节点会连续交换 ping pong 包。这两种包具有相同的结构,都包含重要的配置信息。本质上只是消息类型不同。ping pong 包合在一起也叫做心跳包。

正常情况下,节点向另外一个节点发送 ping 包,触发相应节点返回 pong 包。不过这也不是绝对的,节点也可以只发送 pong 包。这在广播新配置变更的时候比较有用。

节点会在几种情况下发送ping 包:

  • 节点每秒随机向几个节点发送 ping
  • 若超过 NODE_TIMEOUT/2 没有收到过来自某节点的 ping 或 pong,则在 NODE_TIMEOUT 之前一定会向该节点发送 ping。NODE_TIMEOUT 超时之前,节点会尝试重连 TCP 连接,防止将 TCP 连接的问题误认为是机器不可达。

若 NODE_TIMEOUT 设置的太小而集群中节点数量太多,则交互的消息数量是可调的,从而保证在半个 NODE_TIMEOUT 时间之内所有节点都有一次交互以防止过期。

举个例子,若 NODE_TIMEOUT 设置为 60秒,集群大小为 100。这就要求在 30秒内每个节点都要同其他99个节点进行一次交互,即平均 3.3 ping/s。整个集群就有 330 ping/s。

消息数量还有其他方式进一步降低。不过一般不会遇到集群带宽问题,因此使用了这个直接的方案。

心跳包内容

ping 包和 pong 包包含两个部分: 所有消息类型共有的 header 部分;心跳包独有的 gossip 部分。

header 部分包含以下信息:

  • NODE ID.
  • currentEpoch 和 configEpoch
  • 节点 flag. 表示节点是 master 还是 slave,还有一些其他 flag 信息
  • 节点提供服务的 hash slot 的 bitmap,若节点是 slave,则是其 master 的bitmap
  • 发送者的基础 TCP 端口. 加10000 才是集群总线端口
  • 发送者认为的集群状态
  • 如果是 slave,则包含 master 的 NODE ID

ping pong包还包含 gossip 部分。这部分内容包含了该节点认为的集群中其他节点的状态,不过不是全部,而是随机选择的一部分。对于包含的每个节点,都有以下信息:

  • NODE ID
  • 节点的 IP 和 端口
  • NODE flags

gossip 部分的内容对于错误检测节点发现很有用。

错误检测

当集群中的某个 master 或者 slave 不能被集群中的大部分 master 触达时,需要通过错误检测来识别。识别出之后将一个 slave 提升为 master。若不能提升,则集群进入错误状态,不再对外提供服务。

如上文所述,每个节点都维护其他节点的 flag 状态,其中 PFAIL 和 FAIL 状态对于错误检测很有用。PFAIL 表示“可能失败”,FAIL 表示节点的 FAIL 状态经过集群中大部分 master 确认过。

PFAIL flag

若某个节点 NODE_TIMEOUT 超时仍不可达,则将其标记为 PFAIL 状态。

节点不可达,意味着我们发了一个 ping,但是到 NODE_TIMEOUT 超时也没有收到回复。这也意味着 NODE_TIMEOUT 要比 round trip time 要大得多。

FAIL flag

PFAIL 信息仅仅是一个局部的信息,代表本机认为目标机器不可达。但是仅有 PFAIL 状态不足以触发 slave 提升为 master 的操作。需要将 PFAIL 提升为 FAIL。

当满足下面条件时 PFAIL 提升到 FAIL:

  • 节点 A 将 B 标记为 PFAIL
  • A 通过 gossip message 了解到大部分 master 认为的 B 的状态
  • 大部分 master 在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT 时间内通知 B 为 PFAIL 或FAIL 状态

若这些条件满足,则 A:

  • 标记 B 为FAIL
  • 向所有节点发送 FAIL 消息

收到 FAIL 消息的节点也会强制将 B 标记为 FAIL 状态。

FAIL 状态基本是单向的,只能由 PFAIL 提升到 FAIL 。但在下列情况下 FAIL 标记可以被清除:

  • 节点重新可达且是 slave. 清除 FAIL 状态
  • 节点重新可达且是不提供 slot 服务的 master. 可以被清除
  • 节点重新可达且是 master,并且在较长时间内没有 slave 被提升为 master

PFAIL 到 FAIL 的状态提升使用了一个弱协议:

  • 节点状态数据是在不同时间点的. 即使大部分 master 达成一致,也是在所收集数据上,无法保证某个时间点大部分master 达成一致。不过我们会丢弃过早时间的检测数据,因此是在一个时间段内达成的一致。
  • 检测到 FAIL 后,会强制其他节点提升到 FAIL。但若一个节点检测到 FAIL,但由于网络 partition 造成无法将信息同步出去。

但是集群的错误检测有一个要求: 最终所有节点都要在某个给定节点的状态上达成一致。若发生 split brain 问题,会发生两种情况:

  • Case 1: 大部分 master 将某节点标记为 FAIL,这最终种情况下所有节点都会将该 master 标记为 FAIL
  • Case 2: 少部分 master 将节点标记为 FAIL,由于长时间没有节点被提升为 master,则节点的 FAIL 标记被清除

FAIL 标记仅仅作为运行算法安全部分的触发器。理论上讲,每个 slave 在发现 master 不可达之后都可以自行将自身提升为 master,若master 仍被多数可达则等待其他 master 的拒绝消息。但是新增的 PFAIL->FAIL,错误传播机制也有其作用:

  • 保证了一旦集群处于错误状态,则所有节点都拒绝写操作。
  • 避免了 slave 由于局部网络异常不能脸上master 而发起错误的选举尝试

配置处理,传播和失败转移

集群 current epoch

redis 集群使用了类似 Raft 算法中 “term” 的概念,只不过在 redis 集群中叫 epoch。

currentEpoch 是一个 64 位无符号数字。

集群节点创建时, currentEpoch 设置为 0.

每次节点收到一个包,若发送者的 epoch 比当前节点的大,则 currentEpoch 更新为发送者的 epoch。

最终,集群在节点的最大 configEpoch 上达成一致。

configuration epoch

master 会在 ping 或 pong 包里包含其 configEpoch 信息。

节点新创建时, configEpoch 为0.

在 slave selection 过程中,获得授权的 slave 会创建新的 configEpoch 并用其转变为 master。

configEpoch 可用于解决分支问题。

configEpoch 发生变化时,会永久保存在所有节点的 nodes.conf 文件上。

Slave 选举和提升

Slave 选举由 slave 节点在投票给 slave 节点的 master 节点的帮助下完成。

Slave 若想提升为 master,需要发起选举并赢得选举。 slave 发起选举的条件如下:

  • 该 Slave 的 master 处于 FAIL 状态
  • slave 的 master 服务的 slot 数量大于0
  • slave 同master 之间备份数据的链接断开时间在一定时间之内,以保证 slave 上数据相对比较新

选举操作过程如下:

  • 增加 currentEpoch
  • 向所有 master 发送 FAILOVER_AUTH_REQUEST 授权请求,等待最多 2 * NODE_TIMEOUT 时间
  • master 若同意 slave 的授权请求,将返回 FAILOVER_AUTH_ACK。并在 2 * NODE_TIMEOUT 时间内不再同意其他授权请求。这个策略对于安全性保证来讲不是必须的,因为 configEpoch 最大的才能作为 master。不过该策略可以防止多个 slave 在同一时间被选举为 master
  • slave 丢弃所有 epoch 比 currentEpoch 小的 ACK,以防止误将上次选举的 ACK 当做本次的
  • 若 slave 从大部分 master 获取到 ACK,则执行提升。否则本次选举失败,需要间隔 NODE_TIMEOUT * 4 时间段后重试

Slave Rank

slave 发现 master 进入 FAIL状态后,要等一段时间才发起选举。等待时间 delay 为

DELAY = 500 ms + random delay between 0-500 ms + SLAVE_RANK * 1000 ms

delay 分为几部分:

  • 固定的 500 毫秒,让 FAIL 状态尽可能在网络内传播,防止 master 收到选举请求的时候还未发现master 已经FAIL,不返回确认
  • 随机部分. 防止多个 slave 同时开启选举
  • SLAVE_RANK 部分,master fail 的时候,slave 之间会交互信息,从而将slave 排序。具有最新信息的 slave 的 SLAVE_RANK 为0,次之为1,等等. 这部分保证数据最全的 slave 最先发起选举.

slave 一旦被选中,它就获得一个新的当前最大的 configEpoch。它立马会使用该 configEpoch 发送 pong 包广播给集群中所有节点。后续ping 和 pong 包中也会携带该 configEpoch 信息。

其他节点收到通知后,会用较大 configEpoch 的节点作为处理 slot 的master。而原来的 master 若收到,还会建立到新 master 的备份,将自己转变为新 master 的一个 slave。

master 回应 slave 的选举请求

master 若收到 slave 发起的 FAILOVER_AUTH_REQUEST 请求,在下列条件满足的情况下将会返回 ACK确认:

  • master 对每个 epoch 只投一次票,并且不对较老的 epoch 投票. master 维护 lastVoteEpoch,若收到的 currentEpoch 比 lastVoteEpoch 小则不投票.
  • 仅当发起投票的 slave 的 master 进入 FAIL状态,本 master 才投票
  • 所有 currentEpoch 比 master 的 currentEpoch 小的请求都会被忽略.

master 执行选举由以下策略:

  • 若 master 已经投票给某个 slave,则在 NODE_TIMEOUT * 2 时间内不会再投票给同一 master 的 slave。这条策略从安全性来讲不是必须的,但是会防止额外的 failover 操作发生。
  • master 不对 slave 做区分. 只要 slave 的 master 处于 FAIL 状态且当前周期还未投票,就投票给发起请求的 slave
  • 若 master 不投票给 slave,则不会返回数据。仅仅是忽略请求。
  • 若 slave 的 slot 的 configEpoch 大于当前 slave 发送请求的 configEpoch,则 master 不投票给该 slave

在分隔(partition)情况下 configEpoch 的实际作用

举个列子:

  • 一个 master 挂了,该 master 有 A、B、C 三个 slave
  • A 赢得选举并将自身提升为 master
  • 出现网络 partition,A 对大部分 master 不可达
  • B 赢得选举并将自身提升为 master
  • 出现 partition,B 对大部分 master 不可达
  • 第一个partition 修复,A 对大部分 master 可达

此时 A 作为master,同时 C 试图选举并提升为 master。那么 C 可以赢得选举提升为 master 吗?

  • C 发起选举,并会获得成功。因为 master 是 FAIL 状态
  • A 尝试发起通知自身提升为 master,但是大部分 master 不会接受。因为 B 已经赢得过选举,因此大部分 master 会认同 B 的 configEpoch,而A 的 configEpoch 比较旧了。
  • 最终,C 发起提升通知并成功。因为 C 的 configEpoch 更新。

假设另一种情况,没有 B 的存在。在这种情况下 A 断开一小段时间重新恢复,A 有可能可以提升成功。A 提升成功的情况下 C 就不能选举成功;反之亦然。

哈希槽配置的传播

主要有两种方式:

  • 心跳消息
  • UPDATE 消息。心跳消息携带了 configEpoch,若接收方发现发送方的信息比较陈旧(configEpoch 比较旧),旧发送一个消息强制更新发送节点的信息

hash slot的更新有两个规则:

  • 规则1: 若 hash slot 未分配(当前为NULL),则更改关联的 hash slot table
  • 规则2: 若广播的 configEpoch 币本地 hash slot table 关联的 configEpoch 大,则重新绑定到新节点

节点重新加入集群

在重新配置过程中,某个 master 的所有节点都会由原来的 slave 提供服务。如之前的例子。master 的多个 slot 可能由 A 或 B 提供服务。

因此当 master 重新加入集群后,它需要将自身变为某个节点的一个 slave。那它要变为 A 的slave 还是 B 的slave 呢?切换规则为: master 节点重新加入后会变为从它拿走最后一个 slot 的那个节点的 slave.

备份迁移

备份迁移用于提升系统可用性。

我们先看没有备份迁移的情况:

  • master A 有唯一 slave A1
  • A 挂了,A1 提升为 master
  • 过一段时间,A1 也挂了。这时候系统可用性收到伤害,系统不再可用。

备份迁移方案下的例子。A、B、C 为master,A1 和 B1 分别是 A、B 的 slave;C1 和 C2 是 C 的slave:

  • A 挂了,A1 提升为 master
  • C2 迁移为 A1 的 slave(A1此时没有其他 slave)
  • 过一段时间,A1 挂了
  • C2 替换 A1 成为 master
  • 集群仍然可用

备份迁移算法

备份迁移算法主要用于决定哪个 slave 来做迁移操作。

acting slave: 具有最多关联 slave 的 master 的 slave,并且取 id 最小的那一个。

需要注意,当配置不稳定的时候,可能会有多个 slave 认为自己是 acting slave,都去执行迁移操作。这不是大问题,因为当配置稳定之后,若原来的 master 下没有 slave,则slave 还是会迁移回来的。最终每个 master 都会有 slave。而且绝大部分情况下不会发生这种事情。

configEpoch 冲突解决算法

slave 自己生成的 configEpoch 是可以保证唯一的。但是管理员可以触发一些操作,从而使集群内产生同样的 configEpoch。但集群希望任何情况下每个 master 都有唯一的 configEpoch,因此需要 configEpoch 冲突解决算法来强制保证这一点。具体算法为:

  • 一个 master 发现其他 master 节点广播的 configEpoch 跟自身的相同
  • 本 master 的 Node ID 相对比较小(字典序)
  • 本 master 将 currentEpoch 赠1,并用它作为新的 configEpoch

节点重置

可以在不重启节点的情况下对其 reset,从而另做他用。重置命令为:

  • CLUSTER RESET SOFT
  • CLUSTER RESET HARD

命令直接发给要重置的节点。重置将执行以下操作:

  • 若节点是 slave,将其设置为 master,并丢弃所有数据。若节点是 master,则放弃重置操作。
  • 释放所有 slot,重置手动 failover 状态
  • 将节点表中所有数据删除。因此不再知道其他节点信息
  • 设置 currentEpoch,configEpoch 和 lastVoteEpoch 为0 (只对 HARD reset 生效)
  • 重新生成 Node ID(只对 HARD reset 生效)

从集群删除节点

删除节点只需要将其所有 slot 迁移到其他 master ,并关机即可。不过其他节点仍然记得它的 Node ID 并会尝试重连。

因此希望将该节点的记录从其他节点的节点表删除。这通过 CLUSTER FORGET 命令完成。这条命令主要做下面工作:

  • 从 node table 移除相应节点信息
  • 60 秒内阻止同样名字的节点重新添加到节点表。这是为了防止通过 gossip 协议重新加回来。

发布-订阅

客户端可以从任意节点订阅内容,也可以发布信息到任意节点。redis 集群会保证相应信息被转发。

当前的实现仅仅是将每个发布的消息广播到其他所有节点。以后会通过 Bloom Filter 或其他算法优化。

附录

附录A: CRC16 算法的 C 语言实现

/*
 * Copyright 2001-2010 Georges Menie (www.menie.org)
 * Copyright 2010 Salvatore Sanfilippo (adapted to Redis coding style)
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the University of California, Berkeley nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* CRC16 implementation according to CCITT standards.
 *
 * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the
 * following parameters:
 *
 * Name                       : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN"
 * Width                      : 16 bit
 * Poly                       : 1021 (That is actually x^16 + x^12 + x^5 + 1)
 * Initialization             : 0000
 * Reflect Input byte         : False
 * Reflect Output CRC         : False
 * Xor constant to output CRC : 0000
 * Output for "123456789"     : 31C3
 */

static const uint16_t crc16tab[256]= {
    0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
    0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
    0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
    0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
    0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
    0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
    0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
    0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
    0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
    0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
    0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
    0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
    0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
    0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
    0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
    0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
    0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
    0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
    0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
    0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
    0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
    0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
    0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
    0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
    0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
    0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
    0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
    0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
    0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
    0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
    0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
    0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0
};

uint16_t crc16(const char *buf, int len) {
    int counter;
    uint16_t crc = 0;
    for (counter = 0; counter < len; counter++)
            crc = (crc<<8) ^ crc16tab[((crc>>8) ^ *buf++)&0x00FF];
    return crc;
}

你可能感兴趣的:(分布式系统)