redis ring


随着玩家越来越多,不可能把所有cache都放在一个单点上,那么就涉及到cache扩展。我们使用了以下两种方法来扩展cache
按照不同的业务访问不同的redis作为cache
水平扩展,增加服务器数量,线性扩充系统性能。相同业务的按照类似Hash(Key)的方法,路由到不同的redis上
方法1比较简单和常见,下面详细介绍一下方法2

Hash

水平扩展为什么用hash?
当输入值确定时,哈希算法的输出也是唯一的,确保同一存储对象每次能访问同一节点。
哈希算法的输出具有离散性,好的哈希算法下,即使输入数据只有微小的变化,其哈希值也应该有很大的不同。因此哈希的输出域具有均匀分布的特性,和负载均衡的需求对应上,有助于避免热点问题。

简单的水平扩展方法

水平扩展redis一种简单的方法就是,按照cache key做hash,用hash得到的结果对Redis节点数取模,得到的最终结果就是路由到的redis节点ID,大概逻辑如下

// 把cacheKey路由到第n个redis节点上
h = hash(cacheKey)
n = h % nodeCnt + 1

上述方法的优点:

  1. 实现简单
  2. 分布均匀

存在的问题:

  1. 依赖nodeCnt,当nodeCnt发生变化时,cacheKey会重新分配,导致大量数据迁移
  2. 由于1的问题,所以无法很好的在线扩展redis以及拆除故障节点
    在这里插入图片描述

经典Consistent Hash

步骤:

  1. 把所有处理区间视为一个环。
  2. 对每个redis节点的唯一标识做hash运算,映射到环上。
  3. 对需要存储的对象的key做同样的hash运算,映射到环上。
  4. 根据环上的位置顺时针决定每个存储对象归属于哪个redis节点(二分法查找)。

在这里插入图片描述

然而,要确保每个redis节点负载均衡,需要所有节点均匀地分布在环上,这在机器数量不够多的情况下无法保证。
因此引入虚拟节点技术:
对每个节点的唯一标识加上后缀,比如对NodeA,加上后缀成为NodeA1,NodeA2,…,NodeAm.这些就是A的虚拟节点。
对A的虚拟节点做hash运算,映射到环上。
其他步骤同上述1.3,1.4.

在这里插入图片描述

增加节点:
对新节点的唯一标识做hash运算并映射到环上,仅需重新决定部分存储对象的归属,迁移少量数据。
删除节点:
将被删除节点持有的对象按顺时针分配至下一节点,同样仅需迁移受影响的数据。

在这里插入图片描述

优点:
不依赖nodeCnt,当某个节点故障时,只有故障节点上的数据需要迁移至下一节点;
根据1的逻辑,线上操作扩容和拆除故障节点时,不会发生数据整体迁移;
根据节点的性能可以动态分配虚拟节点数量。
缺点:
增加虚拟节点映射表,有额外内存开销、计算开销;
负载均衡的效果受限于虚拟节点的数量。当节点数量较少时效果不稳定。

以下为go-redis v6.15.7 中的代码摘录

// 初始化hashMap, 其中keys为每个Node的名字,replicas为虚拟Node的倍数,相当于最后生成一个nodeNum * replicas的hash map
func (m *Map) Add(keys ...string) {
    for _, key := range keys {
        for i := 0; i < m.replicas; i++ {
            hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
            m.keys = append(m.keys, hash)
            m.hashMap[hash] = key
        }
    }
    sort.Ints(m.keys)
}
 
 
// 查询key 所在的node就是通过已经排序过的hash map进行2分法查找最符合的node
func (m *Map) Get(key string) string {
    if m.IsEmpty() {
        return ""
    }
 
    hash := int(m.hash([]byte(key)))
 
    // Binary search for appropriate replica.
    idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })
 
    // Means we have cycled back to the first replica.
    if idx == len(m.keys) {
        idx = 0
    }
 
    return m.hashMap[m.keys[idx]]
} 

Rendezvous Hashing(HRW)

rendezvous hashing(集合哈希),或称highest random weight(HRW) hashing。
基本思想是为每个存储对象算出每个节点的权重,并将该对象分配给权重最高的节点。

  1. 所有客户端都使用同一哈希函数h();(该函数算出的hash值不代表在环上的位置,而代表了权重)
  2. 对于存储对象Oi,节点Sj,用h计算出权重wi,j=h(Oi,Sj);
  3. HRW将Oi分配给权重wi,n最大的节点Sn。

在这里插入图片描述
当某一台节点下线时,只需要转移下线节点中的存储对象至递补节点。

在这里插入图片描述

在线增加节点的操作则比较复杂,需要为当前所有存储对象重新计算hash值,判断它们是否需要去新节点。(但是我们的使用场景是cache,可以接受先不管旧节点上的数据,等新的请求来了再判断是否要去新节点)
优点:

  1. 干扰程度小:若删除站点S,则仅将映射到S的对象重新映射到不同站点。
  2. 可以引入k-agreement策略: 客户端可以选择权重topk的节点。当优选节点挂掉时,立刻可以访问递补节点。

与一致哈希相比较

  • 集合哈希更易于理解和编码。
  • 集合哈希能将key更均匀地分布在各个node上。一致哈希中负载均衡的稳定性取决于虚拟节点的数量。
  • 一致哈希的查找在使用二分查找后时间复杂度为O(logN),集合哈希则需要O(N)。但由于一致哈希需要创建虚拟节点,其N会更大(Free Fire中设置为1000)。
  • 一致哈希对于一个key只需要进行一次哈希计算,集合哈希需要进行O(N)次哈希计算。如果用的哈希函数比较慢,或者ring规模大,那对计算速度有较大的影响。
  • 一致哈希相对需要额外的内存,用于将节点映射到虚拟节点,并提前对所有节点进行hash运算h(Sj)。集合哈希不需要,只需实时地计算h(Oi,Sj)。
  • 在有节点故障时,集合哈希能将故障节点的数据均匀分布至其他节点;但一致哈希会将故障节点的数据全部转移至下一节点。

二者比较实例:在Consistent hash和Rendezvous hash的比较中,在删除5节点环中的几个节点后,考虑以下负载分布:

在这里插入图片描述

只有node4承担被删除的2个节点的负载。然而,使用HRW,分布仍然是均匀的。

在这里插入图片描述

这个例子使用一个简单的环来实现,然而这个极端的不平衡可以通过在整个环中多次添加虚拟节点来减轻。

下面为go-redis v8.8中使用的go-rendezvous库中代码

 // 通过node的名字初始化hash表
func New(nodes []string, hash Hasher) *Rendezvous {
    r := &Rendezvous{
        nodes: make(map[string]int, len(nodes)),
        nstr:  make([]string, len(nodes)),
        nhash: make([]uint64, len(nodes)),
        hash:  hash,
    }
 
    for i, n := range nodes {
        r.nodes[n] = i
        r.nstr[i] = n
        r.nhash[i] = hash(n)
    }
 
    return r
}
 
// 通过计算k与每个node的hash值,选择到值最大的一个node
func (r *Rendezvous) Lookup(k string) string {
    // short-circuit if we're empty
    if len(r.nodes) == 0 {
        return ""
    }
 
    khash := r.hash(k)
 
    var midx int
    var mhash = xorshiftMult64(khash ^ r.nhash[0])
 
    for i, nhash := range r.nhash[1:] {
        if h := xorshiftMult64(khash ^ nhash); h > mhash {
            midx = i + 1
            mhash = h
        }
    }
 
    return r.nstr[midx]
}

Redis Ring对HRW的具体实现

在这里插入图片描述

  1. Ring的基本结构与初始化
    Ring通过匿名字段方式继承了ring(ring中包含选项配置、ring shards分片中的信息等);cmdable是一个函数类型,它用于处理操作命令(比如我们常用到的Get、Set等操作);hooks用于提供钩子函数,可以在处理命令前或处理后进行一些操作。

  2. 初始化hash
    先看NewRing()的第一步,opt.init(),这里会初始化redis ring用到的一致性哈希算法,默认使用的是rendezvous hash。

  3. 用hash查找节点
    我们平时对redis操作(举个例子,Get),其实都会调用到cmdable(代码如下)。

在NewRing()中可以看到把Ring.Process方法赋值给了cmdable,也就是实际会调用Ring.Process().

Ring.process()中有一行代码:shard, err := c.cmdShard(ctx, cmd),这里就是为存储对象查找应当分配的节点位置,cmdShard方法里最后会走到Rendezvous.Lookup()。

Lookup里实现逻辑很简单:为当前key和每一个node都算出一个hash值,找到最大的hash值对应的节点,即这个key应当被分配的节点(也就是在本文Hash这一章讲的rendezvous的逻辑)。

  1. 心跳监测&节点状态更新
    在NewRing()时有看到起了一个goroutine去做心跳监测。

故障节点摘除:redis ring client对所有redis shard有一个心跳检测,超过3次没有收到心跳回复就认为这个redis shard状态为down,就会自动摘除;
故障节点恢复:同样,心跳包检测到故障reids shard恢复心跳,则会自动加回ring中。
.

总结

看代码后会发现,Hash那一章中讲的理论与go-redis中的实际实现也有些差别:
理论中rendezvous hash只需在寻找节点时实时计算hash(key, node), 但代码里实际会存一份各个node的hash值以便省去重复计算;
理论中rendezvous hash可以自由地扩展、删除节点,但代码里实际希望各节点字段是“read only”。节点挂掉后不会将其从管理的分片中删掉,只是在计算时忽略它的存在。同样地,等节点重启后又可以通过心跳检测将其重新加入计算范围内。
总的来说,我们需要解决的痛点主要是:负载均衡,将存储对象均匀稳定地分配至各个节点;当有节点挂掉时,不会出现只有某个节点承担挂掉节点负载的情况。而使用了rendezvous hash的redis ring能很好地符合我们的预期。

FAQ

Redis Ring扩容后,数据将会怎么迁移

在redis v6的consistent hash算法中,每个节点会被分配为若干个虚拟节点用来管理地址空间(使用节点的key值),当一个key值准备操作时,会使用该Key的hash值在所有排序后的虚拟节点中找到一个合适的虚拟节点作为访问目标。扩容后,相当与增加了部分的虚拟节点,将原有地址空间的一部分分配给了新的虚拟节点,key的hash值未发生变化,但如果hash值在扩容后进入了新的虚拟节点的地址空间,将会被安排到新节点上操作。
在redis v8的rdv consistent hash算法中,每个节点会被分配给一个hash值,当一个key值准备操作时,会将该key值和每个节点hash值进行hash,并选中最终结果最大的的节点作为目标节点。扩容后,同样的计算,但由于增加了新的节点,有可能与该节点的hash值会成为最大,然后会被分配给新的节点操作,还有部分key值的目标不会发生变化。

如何无感知的在线进行Redis Ring扩容和替换故障的节点?

在线扩容Redis(缩容同理)步骤如下:
运维准备好redis实例,启动
修改相关service的config.yaml文件,添加准备好的redis实例
灰度重启相关service,让新加的redis生效,中心服是无状态的,灰度重启玩家无感知
遇到问题,参考Consistent Hash,如果扩容redis实例比较多,期间会发生一定的数据迁移,会造成一些数据冗余(迁移后原有cache key永远无法被访问到),占用一定内存资源,可能会造成redis内存被打满
解决方法,重启相关redis实例释放内存,期间玩家无感知
在线替换故障Redis步骤如下:
监控到有一个redis实例故障
参考Redis Ring的机制,会自动把该故障的redis摘除(Consistent Hash中,故障节点的cache数据会迁移到前一个节点上;Rendezvous Hash会使故障节点的cache数据较为均匀地分布在其他节点上)
运维替换掉故障的redis
Redis Ring检测到心跳包,自动把该redis加入到ring当中,cache数据会迁移回来,期间玩家无感知

  • 26
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值