Redis Cluster 实现高可用

Redis Cluster

Redis Cluster(集群)是 Redis 官方提供的分布式数据库方案(从 Redis 3.0 开始),集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

Redis 集群采用了P2P的模式,完全去中心化。Redis 把所有的 Key 分成了 16384 个 slot(槽),每个 Redis 实例负责其中一部分 slot 。集群中的所有信息(节点、端口、slot等),都通过节点之间定期的数据交换而更新。
Redis 客户端可以在任意一个 Redis 实例发出请求,如果所需数据不在该实例中,通过重定向命令引导客户端访问所需的实例。

集群的作用,可以分为两点:

  • 数据分区:数据分区(或称数据分片)是集群最核心的功能。

  • 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务。

数据分布

常用的分区方式有两种:顺序哈希以及哈希分区

分布方式 特点 典型产品
哈希分布 数据分散性高、键值分布业务无关、无法顺序访问、支持批量操作 一致性哈希Memcache、Redis Cluster、其他缓存产品
顺序分布 数据分散度易倾斜、键值业务相关、可顺序访问、支持批量操作 BigTable、HBase

Redis Cluster是使用哈希分布,着重看看哈希分布

哈希分布

哈希分布的方法有很多种,常见的有:节点取余分区,一致性哈希分区、虚拟槽分区

节点取余分区

节点取余的方式是先求数据的哈希函数值,然后再对节点数量取余,(这样的结果就能落在响应的结点上),这种分区方式实现简单,但是缺点是如果对结点进行扩容,比如新增一个结点,数据会发生很大的偏移,迁移率很高。使用这种方法如果要进行扩容,建议使用多倍扩容(比如原本是三个结点扩容至六个结点),这样迁移率会有一定的降低。就算如此,不建议使用这种方式

一致性哈希分区

一致性哈希的原理是将整个数据范围(0~2^32)看做一个环,为每个结点去分配一个token,如果key的最终哈希值落在了两个结点之间,则这个结点会找顺时针离他最近的结点,如下图所示。这种方法从一定程度解决了节点取余数据迁移的问题,如果新增了一个结点在原来两个结点之间,这时候数据依然要进行迁移,但是只需要处理原来两个结点之间的数据,而其他节点的数据时不需要考虑的,当节点数量很多的时候,这种方法能很大程度上减小数据的迁移。但是这种方法会出现流量不均匀的情况,还是无法实现负载均衡,所以在扩容的时候建议使用翻倍扩容。

redis.png

虚拟槽分区

预设虚拟槽:每个槽映射一个数据子集,一般比节点数大(Redis的槽的数量为16384(0-16383)个,因此Redis最多只能有16384个结点)。每个Master节点都会负责一部分的槽,Redis对数据计算哈希值,(使用良好的哈希函数:CRC16),然后对16383取余,会发送给Redis Cluster任意一个结点,每个节点都会记录自己是不是负责这个槽的,如果是,就将该数据保存并返回结果,如果不是,由于Redis 结点之间是共享消息的模式,每个节点都知道每个节点负责哪些槽,这时候会返回负责该槽的结点。

redis2.png

集群伸缩

扩容集群

准备新结点

  • 集群模式

  • 配置和其他结点同意

  • 启动后是孤儿结点

加入集群

将两个孤儿结点加入集群,在原来的客户端中使用cluster meet 127.0.0.1 xxxx (xxxx为孤儿结点的端口号)

加入之后再任意结点上使用cluster nodes 可以查看集群配置

在加入集群的时候需要注意加入的结点是否是孤儿结点,如果将两个非孤儿结点进行meet操作,会将两个不相干的集群meet到一起,后果是不堪设想的。建议使用 Redis官方工具 redis-trib.rb 进行扩容,扩容时会进行孤立结点检测。

迁移槽和数据

迁移数据
  1. 对目标结点发送 cluster setsolt {solt} importing {sourceNodeId} 命令,让目标结点准备导入槽的数据
  2. 对源节点发送: cluster setsolt {solt} migrating {targetNodeId} 命令,让源节点准备迁出槽的数据
  3. 源节点循环执行 cluster getkeysinsolt {solt} {count} 命令,每次获得count个属性槽的键
  4. 在源节点上执行 migrate {targetIp} {targetPort} key 0 {timeout} 命令把制定key迁移
  5. 重复执行3~4知道槽下所有数据迁移到目标结点
  6. 向集群内所有主节点发送 cluster setsolt {solt} node {targetNodeId} 命令,通知槽分配给目标结点

除了以上,还可以使用Redis官方工具 redis-trib.rb进行迁移。

缩容集群

集群的扩容和缩容是由很多相似的地方的。主要有:下线迁移槽、忘记结点、忘记结点

并且在下线过程中应该先下线从节点,再下线主节点,如果先下线主节点会触发故障的自动转移。

下线迁移槽

迁移槽的命令与扩容时迁移槽的命令时一样的

忘记结点

对每一个结点执行 cluster forget {downNodeId}

关闭结点

客户端路由

moved重定向

当需要的数据所在的槽并不在当前结点时,会抛出MOVED异常表示数据不再当前结点。如果使用集群模式(redis-cli -c) 客户端会自动捕获异常,并完成跳转工作,然后重新发送命令,发送命令后,客户端会连接到跳转后的结点。

1.png

命中

2.png

未命中

3.png

ask重定向

在集群缩容扩容的时候,要对槽进行迁移,槽迁移过程中要遍历进行migrate,迁移时间比较长,
此时在此过程中访问一个key,但是key已经迁移到目标节点。
5.png

Redis Cluster会返回ask异常,客户端会重定向至目标的结点。

6.png

MOVED和ask

  • 两者都是客户端重定向
  • moved:槽已确定迁移
  • ask:槽还在迁移中

smart客户端

redis-cli这一类客户端称为Dummy客户端,因为它们在执行命令前不知道数据在哪个节点上,因此需要借助moved异常重定向。为了追求性能,我们不可能每次都随机访问一个节点,再根据moved或ask异常去重定向到目标节点,因此需要实现一个Smart客户端,比如说JedisCluster。JedisCluster的基本原理大致如下:

  1. 从集群中选一个可运行结点,使用cluster slots初始化槽和结点映射
  2. 将cluster slots的结果映射到本地,为每个结点创建JedisPool
  3. 准备执行命令

7.png

故障转移

集群对故障发现与故障转移的实现与Sentinel思路类似:通过定时任务发送ping消息检测其他节点状态,若某个主节点发现另一个主节点不可用(与参数cluster-node-timeout有关),则标记该节点进行主观下线,而当半数以上持有槽的主节点都标记该节点主观下线,则对该节点进行客观下线,并向集群广播fail消息,让集群中所有节点都将其标记为客观下线,并触发从节点的故障转移。

在故障转移阶段,主要有以下几个步骤:

  • 检查资格:每个从节点都会检查与故障主节点的断线时间,如果超过默认值150s(cluster-node-timeout * cluster-slave-validity-factor)则会取消资格。
  • 准备选举时间:为了保证偏移量比较大的从节点更有可能成为主节点,会将该从节点的延迟时间设置更小一些。
  • 选举投票:从节点选举胜出需要的票数为N/2+1,其中N为主节点数量(包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要3个主节点。
  • 替换主节点。

与哨兵一样,集群只实现了主节点的故障转移,从节点故障时只会被下线,不会进行故障转移。因此,使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。

参考资料