Loading

一致性Hash的原理与实现

应用场景

在了解一致性Hash之前,我们先了解一下一致性Hash适用于什么场景,能解决什么问题?这里先放一下我自己认为适用的场景。一致性Hash适用于服务器动态扩展且需要负载均衡的场景

试想以下场景,某一天,公司的业务不断发展壮大,现有的数据库服务器无法支撑那么大的数据量,我们该怎么办呢?我们会想到把用户请求分散,对数据库服务器做集群,把用户请求分别路由到不同的数据库服务器上, 但是这样会带来一个问题,我们如何精确的知道某一个用户请求的数据在哪一台具体服务器上呢?

解决方案

方案一

A同学讲,用户发来一个请求,对所有的数据库服务器做查询,如果能查询到结果,就返回,否则就是没有。这样是可行,但是这样做的话,一个请求同样会请求N台数据库服务器,我们做数据库集群也就没有意义了,所以是不可取的。

方案二

B同学讲,我们可以根据请求的数据参数做Hash运算,然后利用Hash值对数据库服务器的个数求余,这样就能知道用户的数据在哪一台服务器上了,然后在做具体的操作就可以了。

显然,B同学的思路很正确,但是我们还要考虑一些其他的问题(动态扩展性),假如某一时刻用户请求量非常大,导致其中的某一台数据库服务器宕机了,这样Hash值对数据库服务器的数量求余结果明显就是不正确的了。或者某一天,我们需要对增加该集群服务器的数量,结果也是不正确的,就需要对所有的数据做迁移。

方案三

C同学则是在B同学的基础上,考虑到我们是不是可能把所有的服务器节点放到一个足够大的环上,如图一,当请求到达之后,我们仅仅找到该节点之后的一个节点。req1会到Node1上,req2会到Node2上,req3会到Node3上。假如Node3下线了,那么所有路由到Node3的请求req3都会到Node1上,这样只会有原来请求Node3请求会受到影响,而其他的节点并不会因此受到影响。

 

 

图一

 

那么假设我们要增加一个节点呢?我们希望的情况肯定是下面这样,如图二,但是事实往往不是这样,事实往往是如图三这样的,这就是Hash环的偏斜问题。这样就会导致的Node3的请求量要等比其他三台服务器请求量之和还要大,严重的负载不均衡,那么我们该如何解决这样问题呢?我们可以利用虚拟节点的概念,也就是每一个节点在这一个环上可以表现为多个节点,但是真正处理请求的还是这一个节点。比如图四那样,但是这样并不能做到真正意义上的完全平衡,只能说尽量平均分不到不同的节点上,而虚拟节点的数量越多,平衡的概率就越大。

 

 

 

图二

 

 

 

图三

 

 

 

图四

 

性质

 

上面我们已经说了一致性Hash的应用场景以及它可以解决什么问题,下面我们就来总结一下一致性Hash性质,原理。

  分布式系统每个节点都有可能失效,并且很可能会有新的节点增加进来,那么如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是分布式系统需要考虑的一个重点问题。在分布式系统的设计时,我们就必须要考虑动态性扩展的问题,这样才不至于某一台服务宕机会影响整个系统。如果不采用合适的算法来保证一致性,那么系统中的所有数据都可能会失效,因此一致性hash算法就显得尤为重要。一般情况下一致性Hash要满足以下几个性质。这里可以参考https://brpc.apache.org/docs/rpc-in-depth/consistent-hashing/

  • 平衡性 (Balance) : 每个节点被选到的概率是O(1/n)。

  • 单调性 (Monotonicity) : 当新节点加入时, 不会有请求在老节点间移动, 只会从老节点移动到新节点。当有节点被删除时,也不会影响落在别的节点上的请求。

  • 分散性 (Spread) : 当上游的机器看到不同的下游列表时(在上线时及不稳定的网络中比较常见), 同一个请求尽量映射到少量的节点中。

  • 负载 (Load) : 当上游的机器看到不同的下游列表的时候, 保证每台下游分到的请求数量尽量一致。

实现(golang)

 

  1. 数据准备工作,定义数据结构,PhysicalNode包括IP,端口和节点名称

type UInt32Slice []uint32

func (s UInt32Slice) Len() int {
  return len(s)
}

func (s UInt32Slice) Less(i, j int) bool {
  return s[i] < s[j]
}

func (s UInt32Slice) Swap(i, j int) {
  s[i], s[j] = s[j], s[i]
}

type PhysicalNode struct {
  ip   string
  port int
  name string
}
type PhysicalNodes struct {
  nodeMap     map[uint32]PhysicalNode //key 节点的Hash值 value节点的信息
  nodeList    UInt32Slice             //排序之后的节点的Hash值数据,方便用于二分搜索
  virtualNums int                     //虚拟节点数目
  lock        sync.Mutex              //删除节点时需要用的锁
}
  1. 工具类:哈希值的计算,这里采用网上用的较多的Hash算法,具体可百度

package utils

const p uint32 = 16777619

func Hash(key string) uint32 {
  var hash uint32 = 2166136261
  num := len(key)
  for idx := 0; idx < num; idx++ {
     hash = (hash ^ uint32(key[idx])) * p
  }
  hash += hash << 13
  hash ^= hash >> 7
  hash += hash << 3
  hash ^= hash >> 17
  hash += hash << 5

  if hash < 0 {
     return -hash
  }
  return hash
}
  1. 获取节点的Hash值

func getNodeHash(node PhysicalNode, i int) uint32 {
  hash := utils.Hash(node.ip + strconv.Itoa(node.port) + node.name + "@@" + strconv.Itoa(i))
  return hash
}
  1. 添加节点

func (receiver *PhysicalNodes) AddPhysicalNodes(nodes ...PhysicalNode) {
  for _, node := range nodes {
     for i := 0; i < receiver.virtualNums; i++ {
        hash := getNodeHash(node, i)
        receiver.nodeList = append(receiver.nodeList, hash)
        receiver.nodeMap[hash] = node
    }
  }
  sort.Sort(receiver.nodeList)
}
  1. 删除节点

func (receiver *PhysicalNodes) DeletePhysicalNodes(nodes ...PhysicalNode) {
  willDelNodeSet := make(map[uint32]bool)
  for _, node := range nodes {
    for i := 0; i < receiver.virtualNums; i++ {
        hash := getNodeHash(node, i)
        willDelNodeSet[hash] = true
        delete(receiver.nodeMap, hash)
    }
  }

  //copyOnWrite
  receiver.lock.Lock()
  newNodeList := make([]uint32, 0)
  for _, v := range receiver.nodeList {
    if !willDelNodeSet[v] {
        newNodeList = append(newNodeList, v)
    }
  }
  receiver.nodeList = newNodeList
  receiver.lock.Unlock()
  sort.Sort(receiver.nodeList)
}
  1. 获取节点(采用二分查找)

func (receiver *PhysicalNodes) GetPhysicalNode(key string) PhysicalNode {
  hash := utils.Hash(key)
  idx := sort.Search(len(receiver.nodeList), func(i int) bool {
     return receiver.nodeList[i] >= hash
  })
  if idx == len(receiver.nodeList) {
     idx = 0
  }
  return receiver.nodeMap[receiver.nodeList[idx]]
}
  1. 测试(这里仅仅给出单线程测试版)

const total = 100000000

func TestConsistentHashSingleRoutineOne(t *testing.T) {
  physicalNodes := NewPhysicalNodes(100)
  for i := 0; i < 5; i++ {
     node := PhysicalNode{"10.22.35.6" + strconv.Itoa(i), 8080 + i, "node" + strconv.Itoa(i)}
     physicalNodes.AddPhysicalNodes(node)
  }

  str := "abcfkdhgbofdsphgoqrenvdfajboiergfrjm8ioydctrbcc5fi0-2u24ich20945ch209hc2cj-24rjc2-j4-5j224ic29h4fsg6yfdhgfk"
  n := len(str)

  statistics := make(map[string]int)

  for i := 0; i < total; i++ {
     end := 0
     for end == 0 {
        end = rand.Intn(n)
    }
     start := rand.Intn(end)
     //start := 0
     //end := i % n
     node := physicalNodes.GetPhysicalNode(str[start:end])
     statistics[node.name]++
  }

  for s, i := range statistics {
     fmt.Printf("节点%s出现了%d次,rate:%.2f%%\n", s, i, float32(i)/float32(total)*100)
  }
}

 

  1. 测试结果(上面的测试代码写的不是特别的严谨,仅仅是做一下大概的请求分布测试)

节点node3出现了17760919次,rate:17.76%
节点node4出现了20222723次,rate:20.22%
节点node0出现了22199979次,rate:22.20%
节点node1出现了19649432次,rate:19.65%
节点node2出现了20166947次,rate:20.17%

总结

虽说一致性Hash的原理和算法有很多框架都有自己的实现,但是那终究不是我们的,只有我们自己掌握了他们的原理并加以练习,成为我们自己的一部分,这样才能融入我们自己的知识体系中。

 

参考引用:https://brpc.apache.org/docs/rpc-in-depth/consistent-hashing/

完整代码地址:https://gitee.com/philosoxhy/leetcode/tree/master/code/hash

posted @ 2022-04-10 17:44  Philosophy  阅读(376)  评论(0编辑  收藏  举报