作者 | 张冬洪
责编 | 仲培艺
Redis Cluster是分布式Redis的实现。随着Redis版本的更替,以及各种已知bug的fixed,在稳定性和高可用性上有了很大的提升和进步,越来越多的企业将Redis Cluster实际应用到线上业务中,通过从社区获取到反馈社区的迭代,为Redis Cluster成为一个可靠的企业级开源产品,在简化业务架构和业务逻辑方面都起着积极重要的作用。下面从Redis Cluster的基本原理为起点开启Redis Cluster在业界的分析与思考之旅。
基本原理
Redis Cluster的基本原理可以从数据分片、数据迁移、集群通讯、故障检测以及故障转移等方面进行了解,Cluster相关的代码也不是很多,注释也很详细,可自行查看,地址是:
https://github.com/antirez/redis/blob/unstable/src/cluster.c
。这里由于篇幅的原因,主要从数据分片和数据迁移两方面进行详细介绍:
数据分片
Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片(Sharding)引入哈希槽(hash slot)来实现;一个 Redis Cluster包含16384(0~16383)个哈希槽,存储在Redis Cluster中的所有键都会被映射到这些slot中,集群中的每个键都属于这16384个哈希槽中的一个,集群使用公式slot=CRC16(key)/16384来计算key属于哪个槽,其中CRC16(key)语句用于计算key的CRC16 校验和。
集群中的每个主节点(Master)都负责处理16384个哈希槽中的一部分,当集群处于稳定状态时,每个哈希槽都只由一个主节点进行处理,每个主节点可以有一个到N个从节点(Slave),当主节点出现宕机或网络断线等不可用时,从节点能自动提升为主节点进行处理。
如图1,ClusterNode数据结构中的slots和numslots属性记录了节点负责处理哪些槽。其中,slot属性是一个二进制位数组(bitarray),其长度为16384/8=2048 Byte,共包含16384个二进制位。集群中的Master节点用bit(0和1)来标识对于某个槽是否拥有。比如,对于编号为1的槽,Master只要判断序列第二位(索引从0开始)的值是不是1即可,时间复杂度为O(1)。
图1 ClusterNode数据结构
集群中所有槽的分配信息都保存在ClusterState数据结构的slots数组中,程序要检查槽i是否已经被分配或者找出处理槽i的节点,只需要访问clusterState.slots[i]的值即可,复杂度也为O(1)。ClusterState数据结构如图2所示。
图2 ClusterState数据结构
查找关系如图3所示。
图3 查找关系图
数据迁移
数据迁移可以理解为slot和key的迁移,这个功能很重要,极大地方便了集群做线性扩展,以及实现平滑的扩容或缩容。那么它是一个怎样的实现过程?下面举个例子:现在要将Master A节点中编号为1、2、3的slot迁移到Master B节点中,在slot迁移的中间状态下,slot 1、2、3在Master A节点的状态表现为MIGRATING,在Master B节点的状态表现为IMPORTING。
MIGRATING状态
这个状态如图4所示是被迁移slot在当前所在Master A节点中出现的一种状态,预备迁移slot从Mater A到Master B的时候,被迁移slot的状态首先变为MIGRATING状态,当客户端请求的某个key所属的slot的状态处于MIGRATING状态时,会出现以下几种情况:
图4 slot迁移的中间状态
IMPORTING状态
这个状态如图2所示是被迁移slot在目标Master B节点中出现的一种状态,预备迁移slot从Mater A到Master B的时候,被迁移slot的状态首先变为IMPORTING状态。在这种状态下的slot对客户端的请求可能会有下面几种影响:
键空间迁移
这是完成数据迁移的重要一步,键空间迁移是指当满足了slot迁移前提的情况下,通过相关命令将slot 1、2、3中的键空间从Master A节点转移到Master B节点,这个过程由MIGRATE命令经过3步真正完成数据转移。步骤示意如图5。
图5 表空间迁移步骤
经过上面三步可以完成键空间数据迁移,然后再将处于MIGRATING和IMPORTING状态的槽变为常态即可,从而完成整个重新分片的过程。
架构
实现细节:
-
Redis Cluster中节点负责存储数据,记录集群状态,集群节点能自动发现其他节点,检测出节点的状态,并在需要时剔除故障节点,提升新的主节点。
-
Redis Cluster中所有节点通过PING-PONG机制彼此互联,使用一个二级制协议(Cluster Bus) 进行通信,优化传输速度和带宽。发现新的节点、发送PING包、特定情况下发送集群消息,集群连接能够发布与订阅消息。
-
客户端和集群中的节点直连,不需要中间的Proxy层。理论上而言,客户端可以自由地向集群中的所有节点发送请求,但是每次不需要连接集群中的所有节点,只需要连接集群中任何一个可用节点即可。当客户端发起请求后,接收到重定向(MOVED\ASK)错误,会自动重定向到其他节点,所以客户端无需保存集群状态。不过客户端可以缓存键值和节点之间的映射关系,这样能明显提高命令执行的效率。
-
Redis Cluster中节点之间使用异步复制,在分区过程中存在窗口,容易导致丢失写入的数据,集群即使努力尝试所有写入,但是以下两种情况可能丢失数据:
-
命令操作已经到达主节点,但在主节点回复的时候,写入可能还没有通过主节点复制到从节点那里。如果这时主节点宕机了,这条命令将永久丢失。以防主节点长时间不可达而它的一个从节点已经被提升为主节点。
-
分区导致一个主节点不可达,然而集群发送故障转移(failover),提升从节点为主节点,原来的主节点再次恢复。一个没有更新路由表(routing table)的客户端或许会在集群把这个主节点变成一个从节点(新主节点的从节点)之前对它进行写入操作,导致数据彻底丢失。
-
Redis集群的节点不可用后,在经过集群半数以上Master节点与故障节点通信超过cluster-node-timeout时间后,认为该节点故障,从而集群根据自动故障机制,将从节点提升为主节点。这时集群恢复可用。
优势
1. 无中心架构。
2. 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3. 可扩展性,可线性扩展到1000个节点,节点可动态添加或删除。
4. 高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
5. 降低运维成本,提高系统的扩展性和可用性。
不足
1. Client实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅JedisCluster相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。
2. 节点会因为某些原因发生阻塞(阻塞时间大于clutser-node-timeout),被判断下线,这种failover是没有必要的。
3. 数据通过异步复制,不保证数据的强一致性。
4. 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
5. Slave在集群中充当“冷备”,不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率。