Redis Cluster
支持在线迁移,方便自动缩容扩容,支持自动故障切换。相对于codis
,oneStroe
,twemproxy
这种中心化Proxy架构,大约能节省一半的机器。同时,由于它是单层架构,读写性能相对于Proxy这种中心化架构得到明显提升。Redis Cluster
替代twemproxy
后,响应时间从100-200us减少到50-100us(以上数据可参见唯品会大规模 Redis Cluster 的生产实践)。
Redis Cluster
发展到到现在,已经在业界有了广泛的使用。比如头条的抖音业务线使用多个Redis
集群,每个集群节点数500+
,峰值qps 1.5
亿左右。它是一个无中心化的分布式系统,架构图如下:
Redis Cluster
设计的核心目标是高性能、方便水平伸缩,而并不是去保证系统的一致性或者可用性。所以即使是属于分布式系统,但与其他分布式存储的设计和实现细节还是有不少差异。这些差异会给使用以及运维带来一些挑战。下面将会针对
Redis Cluster
的设计,分析一下这些差异以及会带来的潜在问题。
一、一致性与可用性优化(CAP)
在现代分布式系统中,节点数据是巨大的。在CAP理论范围内,P是必然发生的。在设计上,并且系统内发生节点失败的机会随着节点数的增加而呈指数级增加:
所以对于分布式系统来说,讨论最多的就是
AP
和CP
系统。但Redis Cluster
由于性能的要求,并不会保证严格的一致性或者可用性,所以它既不是一个严格的AP
或CP
系统。我更愿意这样定义Redis Cluster
:
- 无中心化分布式系统
- 核心目标是高性能、方便水平伸缩
- 在高性能基础上,尽力做好AP优化。
所以接下来,重点看一下。在保证高性能的基础上,如何做AP上的优化设计。
一致性(C):
Redis Cluster
采用异步复制,只保持弱一致性(主从复制异步),甚至允许丢失数据。
比如在以下两种情况都会丢失数据,导致数据不一致:
- 主节点写入某条数据,但是这个写入没有通过主从节点传播到从节点那里。 此时主节点宕机了,那么写入会永远地丢失掉。
- 某个旧主节点不可达,故障转移使旧从节点被提升为新主节点。过段时间,旧主节点又可达。此时某个客户端由于键分布空间未更新,会将数据写入这个旧主节点。
1)是由于故障切换导致的丢失数据,这其实是可接受的,毕竟这是故障。2)在实际运用中出现也是极小概率事件(The second failure mode is unlikely to happen
)。也就是说,即使是在系统故障时,与大多数节点相连的客户端的写入都会被保存下来。
可用性(A):
Redis
集群的设计是能容忍集群中少数节点的出错,在绝大多数的主节点是可达的,并且对于每一个不可达的主节点都至少有一个它的从节点可达的情况下,Redis
集群仍能提供正常访问。但对于出现大量节点不可达的情况下,集群才不可用。
举个例子,当cluster-require-full-coverage设置为yes。一个由 N
个主节点组成的集群,每个主节点都只有一个从节点。当有一个节点故障后,集群的多数节点仍然是可访问的。当有两个节点故障后,仍可用的概率是 1-(1/(2N-1))
。比如一个拥有5
个节点的集群,每个节点都只有一个从节点,那么在两个节点从多数节点故障后,集群不再可用的概率是 1/(5*2-1) = 0.1111
,即有大约 11%
的概率。
-
cluster-require-full-coverage yes
同一分片的主从节点都不可用会导致集群不可用。
一个由N
个主节点和N
个从节点组成的集群。挂掉两个节点后,集群不可用的概率:1/((N * 2)(N * 2-1))
。 -
cluster-require-full-coverage no
主节点数小于M=(N/2+1)
会导致集群不可用。
一个由N
个主节点和N
个从节点组成的集群。挂掉M-1
个节点后,集群不可用概率:1/((N * 2)(N * 2-1)...(M-1))
实际上,单个节点挂掉的几率并不高,再乘以特定条件下集群不可用概率,这个概率就更小。所以在总体上来说,可用性是很高的。在此基础上,Redis Cluster
实现了一个叫做备份迁移的概念,以提高系统的可用性。
集群备份迁移提升可用性
例如有一个每个主节点都只有一个从节点的集群,当主节点或者从节点故障失效的时候集群能让操作继续执行下去,但如果主从节点都失效的话就没法让操作继续执行下去。然而这样长期会积累很多由硬件或软件问题引起的单一节点独立故障。例如:
假设集群有三个主节点 A,B,C
。节点 A
和 B
都各有一个从节点,A1
和 B1
。节点 C
有两个从节点:C1
和 C2
。
备份迁移是从节点自动重构的过程,为了迁移到一个没有可工作从节点的主节点上。在上面提到的例子中,备份迁移过程如下:
* 主节点 A 失效。A1 被提升为主节点。
* 节点 C2 迁移成为节点 A1 的从节点,要不然 A1 就没有任何从节点。
* 三个小时后节点 A1 也失效了。
* 节点 C2 被提升为取代 A1 的新主节点。
* 集群仍然能继续正常工作。
备份迁移只能有限的解决问题,无法根除问题。除此之外,一个集群在多次故障之后,势必会出现多个主节点在一个机房甚至一个机器,Redis Cluster
的部署的整个设计需要整体约束,不能松散管理。
二、分布式集群水平扩展
这也是Redis Cluster
核心设计目标。Redis Cluster
只用通过多机部署就能获得水平扩展带来的性能提升。在1000
个节点的时候仍能表现出很好的性能,易于支撑大型集群规模。整个迁移过程不存在锁客户端的窗口时间,一般不会对性能造成过大影响。值得注意的是,针对迁移虽然Redis
做了优化,但是一个集群中拥有很多大key
时,就会在迁移的时候出现延迟。同时完成这一功能,客户端需要支持对moved 、asking
的处理。
Redis
集群支持在集群运行过程中添加或者移除节点。
使用的命令如下
* 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
和 DELSLOTS
,用来添加和移除slot
。 在slot
被指派后,节点会将这个消息通过 gossip
协议向整个集群传播。ADDSLOTS
命令通常是用于在一个集群刚建立的时候快速给所有节点指派哈希槽。
当需要将一个slot
迁移到另一个节点时,需要做以下操作:
1. 使用CLUSTER SETSLOT <slot> IMPORTING <source-node-id>将目标节点插槽设置为导入状态。
2. 使用CLUSTER SETSLOT <slot> MIGRATING <destination-node-id>将源节点插槽设置为迁移状态。
3. 使用 CLUSTER GETKEYSINSLOT 命令从源节点获取密钥,并使用 MIGRATE 命令将它们移动到目标节点中。
4. 使用CLUSTER SETSLOT <slot> NODE <destination-node-id>源或着目标。
当一个槽被设置为MIGRATING
,原来持有该slot
的节点仍然可以接受所有跟这个slot
有关的请求,但只有查询的键还在原节点时,原节点会处理该请求,否则这个查询会通过一个ASK
重定向错误,让客户端转发到真正处理这个key
的节点。
当一个槽被设置为 IMPORTING
,只有在接受到ASKING
命令之后节点才会接受查询该slot的请求。如果客户端一直没有发送 ASKING
命令,那么查询都会通过 -MOVED
重定向,转发到真正处理这个key
的节点。ASKING
请求是不会修改客户端的键空间分布。
注意:在迁移过程中,如果发生故障,很有可能造成整个集群不可用。比如:迁移完成到整个集群获取这个信息,是需要时间的。如果此时发生故障,导致某些新主是无法获取这个信息的。当出现键空间分布不一致,需要使用setslot
手动修复。同时由于键空间分布传播到所有节点需要一段时间(gossip
传播机制决定),导致可能出现成千上百的slot
分布不一致,需要自动化来解决。
三、分布式集群通信
如上文所述Redis Cluster
采用的是Gossip/P2P
的去中心化架构,彼此之间通过Gossip协议通信。
Redis Cluster
主要的消息类型为:PING、PONG、MEET、UPDATE、FAIL
。分别用来维持心跳、创建新节点、更新并统一集群配置、故障处理。
详细说一下UPDAT
:
一个给定的哈希槽是由节点 A 和 B 负责的。节点 A 是一个主节点,然后它在某个时刻失效了,所以节点 B 被提升为主节点。
过了一段时间节点 B 也失效了,集群没有其他备份节点可以来处理这个哈希槽,所以只能开始修复操作。
在一段时间过后节点 A 恢复正常了,并且作为一个可写入的主节点重新加入集群,但它的配置信息是过时的。
此时没有任何备份节点能更新它的配置信息。
这就是 UPDATE 消息存在的目的:当一个节点检测到其他节点在宣传它的哈希槽的时候是用一份过时的配置信息,
那么它就会向这个节点发送一个 UPDATE 消息,这个消息包含新节点的 ID 和它负责的哈希槽。
Gossip
协议是去中心化的协议,所以集群中的所有节点都是对等的,没有特殊的节点,所以任何节点出现问题都不会阻止其他节点继续发送消息。任何节点都可以随时加入或离开,而不会影响系统的整体服务质量。但这种非中心化的架构也会带来一些问题,比如做reshard
需要手动干预(没有中心化的节点去统计)、故障流程会比较慢、信息同步相对于中心化Proxy来说比较大。同时 Gossip
消息会带来大量网络开销,这也是限制Redis Cluster
线性扩展的因素之一。
假设集群总共N个Redis实例,
MIN(3, N/10)*((sizeof(clusterMsg)-sizeof(union clusterMsgData)) + sizeof(myslots)
计算得出一个大小为700个节点的集群,一个gossip消息的大小大约为9kb。
同时gossip
算法选择节点传播消息具有随机性。并且带宽占用跟部署,cluster-node-time
都有关系。所以给出一个测试结果提供参考:
节点704,物理主机 40 台,每台物理主机有 16 个节点。
cluster-node-time设置为30s,集群通信带宽占 107.5MBit/s。
四、集群故障处理
Redis Cluster
主库故障之后,从库可以通过选举提升成为新的主库,继续提供服务。在这段时间内,该节点所负责的slot
是无法提供服务的。
具体流程如下:
1.当一个节点超过NODE_TIMEOUT仍然无法访问,那么就会被标识为PFAIL状态。
在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT时间内,集群中大部分节点都认为标识该节点为PFAIL,则正式下线改节点。
即通过gossip,将FAIL 消息传到集群中。
2.收到该消息节点强制下线该节点。该节点的从库开始发起选举,向主节点请求投票。
3.一旦某个从节点收到了大多数主节点的回应,那么它就赢得了选举。
否则,如果无法在 NODE_TIMEOUT 时间内访问到大多数主节点,那么当前选举会被中断并在 NODE_TIMEOUT * 4 这段时间后由另一个从节点尝试发起选举。
在集群中大多数节点处于存活状态(可以投票),且存在一个备份,集群是可以提供自动的故障转移,不必手动介入。
五、其它实现
分布式键空间分布
键空间被分割成16384
槽,所以理论上集群的最大节点数量是16384
。集群中每个主节点负责16384
个slot中的一部分,每个槽由一个节点处理。
键映射到slot
算法:SLOT = CRC16(key) mod 16384
客户端访问集群
Redis
客户端可以向集群中的任意节点发送查询,如果刚好这个节点就是对应这个slot
,那么这个查询就会被节点处理,否则会给客户端返回一个MOVED
错误。同时更新客户端
MOVED 错误如下
GET x
-MOVED 3999 127.0.0.1:6381
这个错误包括键(3999
)的哈希槽和能处理这个查询的节点的 ip
:端口号(127.0.0.1:6381
),客户端重新发送查询到给定 ip
地址和端口号的节点。
cluster
客户端可以通过cluster Nodes
可以获取Redis cluster
集群键空间分布。根据键空间分布,就可以直接得到key
分布的节点。MOVED
会更新Redis cluster
集群键空间分布。
集群与单机功能
Redis
集群实现了单机版Redis
处理单一键的命令。需要操作多个键的操作都不支持:mset、mget、set
中取交并集以及事务。同时不支持多db
。
本文所有图均来自网上。
参考文章:
Redis cluster tutorial
Redis Cluster Specification
网友评论