Ref: https://pdai.tech/md/db/nosql-redis/db-redis-x-cluster.html
Redis 集群的设计目标
Redis-cluster 是一种服务器 Sharding 技术,Redis3.0 以后版本正式提供支持。Redis Cluster 在设计时考虑了什么?我们不妨看下官网的介绍 Redis Cluster Specification(opens new window)
Redis Cluster goals
- 高性能:可线性扩展至最多 1000 节点。集群中没有代理,(集群节点间)使用异步复制,没有归并操作 (merge operations on values)
- 可接受的写入安全: 系统尝试 (采用 best-effort 方式) 保留所有连接到 master 节点的 client 发起的写操作。通常会有一个小的时间窗,时间窗内的已确认写操作可能丢失 (即,在发生 failover 之前的小段时间窗内的写操作可能在 failover 中丢失)。而在 (网络) 分区故障下,对少数派 master 的写入,发生写丢失的时间窗会很大。
- 可用性:Redis Cluster 在以下场景下集群总是可用:大部分 master 节点可用,并且对少部分不可用的 master,每一个 master 至少有一个当前可用的 slave。更进一步,通过使用 replicas migration 技术,当前没有 slave 的 master 会从当前拥有多个 slave 的 master 接受到一个新 slave 来确保可用性。
Clients and Servers roles in the Redis Cluster protocol
Redis Cluster 的节点负责维护数据,和获取集群状态,这包括将 keys 映射到正确的节点。集群节点同样可以自动发现其他节点、检测不工作节点、以及在发现故障发生时晋升 slave 节点到 master
所有集群节点通过由 TCP 和二进制协议组成的称为 Redis Cluster Bus 的方式来实现集群的节点自动发现、故障节点探测、slave 升级为 master 等任务。每个节点通过 cluster bus 连接所有其他节点。节点间使用 gossip 协议进行集群信息传播,以此来实现新节点发现,发送 ping 包以确认对端工作正常,以及发送 cluster 消息用来标记特定状态。cluster bus 还被用来在集群中传播 Pub/Sub 消息,以及在接收到用户请求后编排手动 failover。Write safety
Redis Cluster 在节点间采用了异步复制,以及 last failover wins 隐含合并功能 (implicit merge function)(【译注】不存在合并功能,而是总是认为最近一次 failover 的节点是最新的)。这意味着最后被选举出的 master 所包含的数据最终会替代(同一前 master 下)所有其他备份 (replicas/slaves) 节点(包含的数据)。当发生分区问题时,总是会有一个时间窗内会发生写入丢失。然而,对连接到多数派 master(majority of masters)的 client,以及连接到少数派 master(mimority of masters)的 client,这个时间窗是不同的。
相比较连接到少数 master (minority of masters) 的 client,对连接到多数 master (majority of masters) 的 client 发起的写入,Redis cluster 会更努力地尝试将其保存。 下面的场景将会导致在主分区的 master 上,已经确认的写入在故障期间发生丢失:
写入请求达到 master,但是当 master 执行完并回复 client 时,写操作可能还没有通过异步复制传播到它的 slave。如果 master 在写操作抵达 slave 之前挂了,并且 master 无法触达 (unreachable) 的时间足够长而导致了 slave 节点晋升,那么这个写操作就永远地丢失了。通常很难直接观察到,因为 master 尝试回复 client (写入确认) 和传播写操作到 slave 通常几乎是同时发生。然而,这却是真实世界中的故障方式。(【译注】不考虑返回后宕机的场景,因为宕机导致的写入丢失,在单机版 redis 上同样存在,这不是 redis cluster 引入的目的及要解决的问题)
另一种理论上可能发生写入丢失的模式是:
- master 因为分区原因不可用(unreachable)
- 该 master 被某个 slave 替换 (failover)
- 一段时间后,该 master 重新可用
- 在该 old master 变为 slave 之前,一个 client 通过过期的路由表对该节点进行写入。
上述第二种失败场景通常难以发生,因为:
- 少数派 master (minority master) 无法与多数派 master (majority master) 通信达到一定的时间后,它将拒绝写入,并且当分区恢复后,该 master 在重新与多数派 master 建立连接后,还将保持拒绝写入状态一小段时间来感知集群配置变化。留给 client 可写入的时间窗很小。
- 发生这种错误还有一个前提是,client 一直都在使用过期的路由表(而实际上集群因为发生了 failover,已有 slave 发生了晋升)。
写入少数派 master (minority side of a partition) 会有一个更长的时间窗会导致数据丢失。因为如果最终导致了 failover,则写入少数派 master 的数据将会被多数派一侧 (majority side) 覆盖(在少数派 master 作为 slave 重新接入集群后)。
特别地,如果要发生 failover,master 必须至少在 NODE_TIMEOUT 时间内无法被多数 masters (majority of maters) 连接,因此如果分区在这一时间内被修复,则不会发生写入丢失。当分区持续时间超过 NODE_TIMEOUT 时,所有在这段时间内对少数派 master (minority side) 的写入将会丢失。然而少数派一侧 (minority side) 将会在 NODE_TIMEOUT 时间之后如果还没有连上多数派一侧,则它会立即开始拒绝写入,因此对少数派 master 而言,存在一个进入不可用状态的最大时间窗。在这一时间窗之外,不会再有写入被接受或丢失。
可用性 (Availability)
Redis Cluster 在少数派分区侧不可用。在多数派分区侧,假设由多数派 masters 存在并且不可达的 master 有一个 slave,cluster 将会在 NODE_TIMEOUT 外加重新选举所需的一小段时间 (通常 1~2 秒) 后恢复可用。
这意味着,Redis Cluster 被设计为可以忍受一小部分节点的故障,但是如果需要在大网络分裂 (network splits) 事件中 (【译注】比如发生多分区故障导致网络被分割成多块,且不存在多数派 master 分区) 保持可用性,它不是一个合适的方案 (【译注】比如,不要尝试在多机房间部署 redis cluster,这不是 redis cluster 该做的事)。
假设一个 cluster 由 N 个 master 节点组成并且每个节点仅拥有一个 slave,在多数侧只有一个节点出现分区问题时,cluster 的多数侧 (majority side) 可以保持可用,而当有两个节点出现分区故障时,只有 1-(1/(N_2-1)) 的可能性保持集群可用。 也就是说,如果有一个由 5 个 master 和 5 个 slave 组成的 cluster,那么当两个节点出现分区故障时,它有 1/(5_2-1)=11.11% 的可能性发生集群不可用。
Redis cluster 提供了一种成为 Replicas Migration 的有用特性特性,它通过自动转移备份节点到孤 master 节点,在真实世界的常见场景中提升了 cluster 的可用性。在每次成功的 failover 之后,cluster 会自动重新配置 slave 分布以尽可能保证在下一次 failure 中拥有更好的抵御力。
性能 (Performance)
Redis Cluster 不会将命令路由到其中的 key 所在的节点,而是向 client 发一个重定向命令 (- MOVED) 引导 client 到正确的节点。 最终 client 会获得一个最新的 cluster (hash slots 分布) 展示,以及哪个节点服务于命令中的 keys,因此 clients 就可以获得正确的节点并用来继续执行命令。
因为 master 和 slave 之间使用异步复制,节点不需要等待其他节点对写入的确认(除非使用了 WAIT 命令)就可以回复 client。 同样,因为 multi-key 命令被限制在了临近的 key (near keys)(【译注】即同一 hash slot 内的 key,或者从实际使用场景来说,更多的是通过 hash tag 定义为具备相同 hash 字段的有相近业务含义的一组 keys),所以除非触发 resharding,数据永远不会在节点间移动。
普通的命令 (normal operations) 会像在单个 redis 实例那样被执行。这意味着一个拥有 N 个 master 节点的 Redis Cluster,你可以认为它拥有 N 倍的单个 Redis 性能。同时,query 通常都在一个 round trip 中执行,因为 client 通常会保留与所有节点的持久化连接(连接池),因此延迟也与客户端操作单台 redis 实例没有区别。
在对数据安全性、可用性方面提供了合理的弱保证的前提下,提供极高的性能和可扩展性,这是 Redis Cluster 的主要目标。
避免合并 (merge) 操作
Redis Cluster 设计上避免了在多个拥有相同 key-value 对的节点上的版本冲突(及合并 /merge),因为在 redis 数据模型下这是不需要的。Redis 的值同时都非常大;一个拥有数百万元素的 list 或 sorted set 是很常见的。同样,数据类型的语义也很复杂。传输和合并这类值将会产生明显的瓶颈,并可能需要对应用侧的逻辑做明显的修改,比如需要更多的内存来保存 meta-data 等。
这里 (【译注】刻意避免了 merge) 并没有严格的技术限制。CRDTs 或同步复制状态机可以塑造与 redis 类似的复杂的数据类型。然而,这类系统运行时的行为与 Redis Cluster 其实是不一样的。Redis Cluster 被设计用来支持非集群 redis 版本无法支持的一些额外的场景。
主要模块介绍
Redis Cluster Specification(opens new window) 同时还介绍了 Redis Cluster 中主要模块,这里面包含了很多基础和概念,我们需要先了解下。
哈希槽 (Hash Slot)
Redis-cluster 没有使用一致性 hash,而是引入了哈希槽的概念。Redis-cluster 中有 16384 (即 2 的 14 次方)个哈希槽,每个 key 通过 CRC16 校验后对 16383 取模来决定放置哪个槽。Cluster 中的每个节点负责一部分 hash 槽(hash slot)。
比如集群中存在三个节点,则可能存在的一种分配如下:
- 节点 A 包含 0 到 5500 号哈希槽;
- 节点 B 包含 5501 到 11000 号哈希槽;
-
Keys hash tags
Hash tags 提供了一种途径,用来将多个 (相关的) key 分配到相同的 hash slot 中。这是 Redis Cluster 中实现 multi-key 操作的基础。
hash tag 规则如下,如果满足如下规则,{和} 之间的字符将用来计算 HASH_SLOT,以保证这样的 key 保存在同一个 slot 中。 key 包含一个 {} 符
- 并且 如果在这个 {的右面有一个} 字符
- 并且 如果在 {和} 之间存在至少一个字符
例如:
- {user1000}.following 和 {user1000}.followers 这两个 key 会被 hash 到相同的 hash slot 中,因为只有 user1000 会被用来计算 hash slot 值。
- foo {}{bar} 这个 key 不会启用 hash tag 因为第一个 {和} 之间没有字符。
- foozap 这个 key 中的 {bar 部分会被用来计算 hash slot
- foo {bar}{zap} 这个 key 中的 bar 会被用来计算计算 hash slot,而 zap 不会
Cluster nodes 属性
每个节点在 cluster 中有一个唯一的名字。这个名字由 160bit 随机十六进制数字表示,并在节点启动时第一次获得 (通常通过 /dev/urandom)。节点在配置文件中保留它的 ID,并永远地使用这个 ID,直到被管理员使用 CLUSTER RESET HARD 命令 hard reset 这个节点。
节点 ID 被用来在整个 cluster 中标识每个节点。一个节点可以修改自己的 IP 地址而不需要修改自己的 ID。Cluster 可以检测到 IP /port 的改动并通过运行在 cluster bus 上的 gossip 协议重新配置该节点。
节点 ID 不是唯一与节点绑定的信息,但是他是唯一的一个总是保持全局一致的字段。每个节点都拥有一系列相关的信息。一些信息是关于本节点在集群中配置细节,并最终在 cluster 内部保持一致的。而其他信息,比如节点最后被 ping 的时间,是节点的本地信息。
每个节点维护着集群内其他节点的以下信息:node id, 节点的 IP 和 port,节点标签,master node id(如果这是一个 slave 节点),最后被挂起的 ping 的发送时间 (如果没有挂起的 ping 则为 0),最后一次收到 pong 的时间,当前的节点 configuration epoch ,链接状态,以及最后是该节点服务的 hash slots。
对节点字段更详细的描述,可以参考对命令 CLUSTER NODES 的描述。
CLUSTER NODES 命令可以被发送到集群内的任意节点,他会提供基于该节点视角 (view) 下的集群状态以及每个节点的信息。
下面是一个发送到一个拥有 3 个节点的小集群的 master 节点的 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.
Cluster 总线
每个 Redis Cluster 节点有一个额外的 TCP 端口用来接受其他节点的连接。这个端口与用来接收 client 命令的普通 TCP 端口有一个固定的 offset。该端口等于普通命令端口加上 10000. 例如,一个 Redis 节点在端口 6379 监听客户端连接,那么它的集群总线端口 16379 也会被打开。
节点到节点的通讯只使用集群总线,同时使用集群总线协议:有不同的类型和大小的帧组成的二进制协议。集群总线的二进制协议没有被公开文档化,因为他不希望被外部软件设备用来与集群节点进行对话。
集群拓扑
Redis Cluster 是一张全网拓扑,节点与其他每个节点之间都保持着 TCP 连接。 在一个拥有 N 个节点的集群中,每个节点由 N-1 个 TCP 传出连接,和 N-1 个 TCP 传入连接。 这些 TCP 连接总是保持活性 (be kept alive)。当一个节点在集群总线上发送了 ping 请求并期待对方回复 pong,(如果没有得到回复)在等待足够成时间以便将对方标记为不可达之前,它将先尝试重新连接对方以刷新与对方的连接。
而在全网拓扑中的 Redis Cluster 节点,节点使用 gossip 协议和配置更新机制来避免在正常情况下节点之间交换过多的消息,因此集群内交换的消息数目 (相对节点数目) 不是指数级的。
节点握手
节点总是接受集群总线端口的链接,并且总是会回复 ping 请求,即使 ping 来自一个不可信节点。然而,如果发送节点被认为不是当前集群的一部分,所有其他包将被抛弃。
节点认定其他节点是当前集群的一部分有两种方式:
- 如果一个节点出现在了一条 MEET 消息中。一条 meet 消息非常像一个 PING 消息,但是它会强制接收者接受一个节点作为集群的一部分。节点只有在接收到系统管理员的如下命令后,才会向其他节点发送 MEET 消息:
CLUSTER MEET ip port
- 如果一个被信任的节点 gossip 了某个节点,那么接收到 gossip 消息的节点也会把那个节点标记为集群的一部分。也就是说,如果在集群中,A 知道 B,而 B 知道 C,最终 B 会发送 gossip 消息到 A,告诉 A 节点 C 是集群的一部分。这时,A 会把 C 注册为网络的一部分,并尝试与 C 建立连接。
这意味着,一旦我们把某个节点加入了连接图 (connected graph),它们最终会自动形成一张全连接图 (fully connected graph)。这意味着只要系统管理员强制加入了一条信任关系(在某个节点上通过 meet 命令加入了一个新节点),集群可以自动发现其他节点。