暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

聊聊Redis的集群模式- 常见问题

码农半亩田 2021-12-04
1260

今天就来聊聊使用Redis集群的时候常见问题:


        问:什么是 Cluster 集群?


Redis 集群是 Redis 的一个分布式实现, 通过分片的方式进行数据管理,将数据划分为 16384个 slots,每个节点负责一部分槽位。槽位信息并存储于每个节点中, 可以很方便的进行横向和纵向的扩充节点,在1000个节点的时候仍能表现得很好,并且可扩展性(scalability)是线性的。


节点之间是去中心化的,通过Gossip协议相互交互集群节点信息,并且每个节点都会记录其他节点slot分配情况。


       问:哈希槽又是如何映射到 Redis 实例上呢?


  1. 通过对key使用 CRC16 算法,计算出一个 16 bit 的值   ;

    算法请参考该文章末尾的代码:

    https://redis.io/topics/cluster-spec
  2. 将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。

    HASH_SLOT = CRC16(key) mod 16384
  3. 根据该槽信息定位到对应的实例。


键值对数据、哈希槽、Redis 实例之间的映射关系如下:


       问:客户端又怎么确定访问的数据分布在哪个实例上呢?


在 Redis 集群中,节点负责存储数据、记录集群的状态(包括键值到正确节点的映射)。集群节点通过Gossip 协议来传播集群的信息,这样可以:发现新的节点、 发送ping包(用来确保所有节点都在正常工作中)、在特定情况发生时发送集群消息, 并且在需要的时候在从节点中推选出主节点。


由于每个节点都记录了slot的映射关系,这样当客户端连接任何一个节点实例,实例就将哈希槽与实例的映射关系响应下发给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。


当客户端请求时,会计算出键所对应的哈希槽,再通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。

如下图:

我们看下go-redis连接sdk中的代码:github.com/go-redis/redis/v8/cluster.go



func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) {


  ... ... 省略分代码 


  // 参数addrs内容是配置中的的redis host
for _, idx := range rand.Perm(len(addrs)) {
addr := addrs[idx]


node, err := c.nodes.GetOrCreate(addr)
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}


    // 此处获取slot的映射信息
slots, err := node.Client.ClusterSlots(ctx).Result()
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}


return newClusterState(c.nodes, slots, node.Client.opt.Addr)
}


... ... 省略分代码 


return nil, firstErr
}

我们来看下ClusterSlots实现:github.com/go-redis/redis/v8/commands.go

func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
_ = c(ctx, cmd)
return cmd
}

通过代码可以看到,调用了 cluster slots, 我们在终端可以看到下面内容

$ redis-cli -c -p 7000 cluster slots
1) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
3) "ccc3041e2e21b4f3093e5d5341aafd9b81d091e6"
4) 1) "127.0.0.1"
2) (integer) 7003
3) "8609add25e9c09ae6b0c7e231b8bb480bbf83a41"
2) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 7001
3) "fd3a1d42fba946e0fc8e1ec1daec8a6cbe6af5ab"
4) 1) "127.0.0.1"
2) (integer) 7004
3) "00011dbec79373a402e1bfac70e34af434207333"
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
3) "6c7b18e8966006e203eccfa10a276f3abe4aa1e7"
4) 1) "127.0.0.1"
2) (integer) 7005
      3) "17a13f9fc46ace704ac6abe6c6acb61abd7fb206"


计算slot:github.com/go-redis/redis/v8/cluster.go

func cmdSlot(cmd Cmder, pos int) int {
if pos == 0 {
return hashtag.RandomSlot()
}
firstKey := cmd.stringArg(pos)
  // CRC16 算法
  // github.com/go-redis/redis/v8/internal/hashtag/hashtag.go
return hashtag.Slot(firstKey)
}

根据计算出来的slot定位节点实例:github.com/go-redis/redis/v8/cluster.go

func (c *clusterState) slotNodes(slot int) []*clusterNode {
i := sort.Search(len(c.slots), func(i int) bool {
return c.slots[i].end >= slot
})
if i >= len(c.slots) {
return nil
}
x := c.slots[i]
  // 根据节点分配的slot返回对应的实例
if slot >= x.start && slot <= x.end {
return x.nodes
}
return nil
}


       问:什么是 Redis 重定向机制?


如果集群通过扩容节点或者缩减节点,会重新分配slot对应的关系,当客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。由于集群节点不能代理(proxy)请求,所以客户端会接收到重定向错误(redirections errors) -MOVED 和 -ASK, 然后将命令重定向到其他节点。


理论上来说,客户端是可以自由地向集群中的所有节点发送请求,在需要的时候把请求重定向到其他节点,所以客户端是不需要保存集群状态。不过客户端可以缓存slot的映射关系,这样能明显提高命令执行的效率。


MOVED 重定向    

一个 Redis 客户端可以自由地向集群中的任意节点(包括从节点)发送查询。接收的节点会分析查询,如果这个命令是集群可以执行的(就是查询中只涉及一个键),那么节点实例会找这个键所属的哈希槽对应的节点。


如果刚好这个节点实例就是对应这个哈希槽,那么这个查询就直接被节点处理掉。否则这个节点会查看它内部的 哈希槽 -> 节点ID 映射,然后给客户端返回一个 MOVED 错误。


另外客户端收到该MOVED 错误,需要更新本地缓存,并将该 slot 与 Redis 实例对应关系更新正确


ASK 重定向    

如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移。


如果请求的 key 在当前节点找到就直接执行命令,否则时候就需要 ASK 错误响应了。


槽部分迁移未完成的情况下,如果需要访问的 key 所在 Slot 正在从 实例 1 迁移到 实例 2(如果 key 已经不在实例 1),实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发发送操作命令。


比如客户端请求ke=hello的slot在节点7000上,节点 7000 如果找得到就直接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 7001。


注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。

MOVED指令则更新客户端本地缓存,让后续指令都发往新实例


       问:当某个节点发生故障,对集群有什么影响?


  1. 当某个Slave节点故障的时候,集群可用性不受影响;

  2. 当Master节点故障的时候,大概有cluster-node-timeout配置的时间内不可用,默认值15s;当某个Master节点下有多个Slave节点的时候会进行投票选举新的Master,使用的是Raft协议;

  3. 当Master及其Slave节点都不可用的时候,整个集群都不可用;如果只有Slave节点恢复,集群依然不可用;只有故障的Master节点恢复,集群才恢复可用。

另外Redis 也提供了一个参数cluster-require-full-coverage可以允许部分节点故障,其它节点还可以继续提供对外访问。



       问:发生了故障,Cluster 如何实现故障转移?


在 Redis 集群中,节点负责存储数据、记录集群的状态(包括键值到正确节点的映射)。集群节点通过Gossip 协议来传播集群的信息,这样可以:发现新的节点、 发送ping包(用来确保所有节点都在正常工作中)、在特定情况发生时发送集群消息, 并且在需要的时候在从节点中推选出主节点。


比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。


如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。


切换过程:

  1. 从下线的 Master 及节点的 Slave 节点列表选择一个节点成为新主节点。

  2. 新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己。

  3. 新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。

  4. 新的主节点开始接收处理槽有关的命令请求,故障转移完成。


比如 7000 主节点宕机,作为 slave 的 7003 成为 Master 节点继续提供服务。当下线的节点 7000 重新上线,它将成为当前 70003 的从节点。


       问:新的主节点如何选举产生的?


选举算法基于Raft协议完成:

  1. 集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。

  2. 检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

  3. 这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。

  4. 参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。

  5. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。


       问:有了 Redis Cluster,可以无限水平拓展么?


No, Redis 官方给的 Redis Cluster 的规模上限是 1000 个实例。


       问:发送 PING 消息的频率也会影响集群带宽吧?


Redis Cluster 的实例启动后,默认会每秒从本地的实例列表中随机选出 5 个实例,再从这 5 个实例中找出一个最久没有收到 PING 消息的实例,把 PING 消息发送给该实例。


       问:随机选择 5 个,但是无法保证选中的是整个集群最久没有收到 PING 通信 的实例,有的实例可能一直没有收到消息,导致他们维护的集群信息早就过期了,咋办呢??


Redis Cluster 的实例每 100 ms 就会扫描本地实例列表,当发现有实例最近一次收到 PONG
 消息的时间 > cluster-node-timeout / 2
。那么就立刻给这个实例发送 PING
 消息,更新这个节点的集群状态信息。

当集群规模变大,就会进一步导致实例间网络通信延迟怎加。可能会引起更多的 PING 消息频繁发送。

降低实例间的通信开销

  • 每个实例每秒发送一条 PING
    消息,降低这个频率可能会导致集群每个实例的状态信息无法及时传播。

  • 每 100 ms 检测实例 PONG
    消息接收是否超过 cluster-node-timeout / 2
    ,这个是 Redis 实例默认的周期性检测任务频率,我们不会轻易修改。

所以,只能修改 cluster-node-timeout
的值:集群中判断实例是否故障的心跳时间,默认 15 S。

所以,为了避免过多的心跳消息占用集群宽带,将 cluster-node-timeout
调成 20 秒或者 30 秒,这样 PONG
 消息接收超时的情况就会缓解。

但是,也不能设置的太大。都则就会导致实例发生故障了,却要等待 cluster-node-timeout
时长才能检测出这个故障,影响集群正常服务。



至此,关于Redis集群相关内容都介绍完了!

📢喜欢的小伙伴欢迎一键三连:点赞、在看、+转发,我会不定时的分享一些干货,你们的支持就是我最大的动力。

由于公众号开通时间不久,官方暂时没有给文章评论的权限,如果小伙伴们有疑问或者文章中有不正确的地方可以加我微信告知,或者在公众号中留言,我会及时回复。


往期文章:

1、聊聊Redis的集群模式- 环境搭建

2、聊聊Redis的集群模式- 故障转移


另外推荐一个go-zero官方公众号,喜欢的朋友可以关注一下

参考资料:

https://redis.io/topics/cluster-spec

http://www.redis.cn/topics/cluster-spec.html

https://segmentfault.com/a/1190000039995230

文章转载自码农半亩田,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论