You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

149 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 10 | 一致哈希算法:如何分群,突破集群的“领导者”限制?
你好,我是韩健。
学完前面几讲后有些同学可能有这样的疑问如果我们通过Raft算法实现了KV存储虽然领导者模型简化了算法实现和共识协商但写请求只能限制在领导者节点上处理导致了集群的接入性能约等于单机那么随着业务发展集群的性能可能就扛不住了会造成系统过载和服务不可用这时该怎么办呢
其实这是一个非常常见的问题。在我看来,这时我们就要通过分集群,突破单集群的性能限制了。
说到这儿有同学可能会说了分集群还不简单吗加个Proxy层由Proxy层处理来自客户端的读写请求接收到读写请求后通过对Key做哈希找到对应的集群就可以了啊。
是的哈希算法的确是个办法但它有个明显的缺点当需要变更集群数时比如从2个集群扩展为3个集群这时大部分的数据都需要迁移重新映射数据的迁移成本是非常高的。那么如何解决哈希算法数据迁移成本高的痛点呢答案就是一致哈希Consistent Hashing
为了帮你更好地理解如何通过哈希寻址实现KV存储的分集群我除了会带你了解哈希算法寻址问题的本质之外还会讲一下一致哈希是如何解决哈希算法数据迁移成本高这个痛点以及如何实现数据访问的冷热相对均匀。
对你来说,学完本讲内容之后,不仅能理解一致哈希的原理,还能掌握通过一致哈希实现数据访问冷热均匀的实战能力。
老规矩,在正式开始学习之前,我们先看一道思考题。
假设我们有一个由A、B、C三个节点组成为了方便演示我使用节点来替代集群的KV服务每个节点存放不同的KV数据
![](https://static001.geekbang.org/resource/image/16/9f/1651257c6ff0194960b6b2f07f51e99f.jpg "图1")
那么,使用哈希算法实现哈希寻址时,到底有哪些问题呢?带着这个问题,让我们开始今天的内容吧。
## 使用哈希算法有什么问题?
通过哈希算法每个key都可以寻址到对应的服务器比如查询key是key-01计算公式为hash(key-01) % 3 经过计算寻址到了编号为1的服务器节点A就像图2的样子
![](https://static001.geekbang.org/resource/image/c6/5a/c6f440a9e77973c3591394d0d896ed5a.jpg "图2")
但如果服务器数量发生变化基于新的服务器数量来执行哈希算法的时候就会出现路由寻址失败的情况Proxy无法找到之前寻址到的那个服务器节点这是为什么呢
想象一下假如3个节点不能满足业务需要了这时我们增加了一个节点节点的数量从3变化为4那么之前的hash(key-01) % 3 = 1就变成了hash(key-01) % 4 = X因为取模运算发生了变化所以这个X大概率不是1可能X为2这时你再查询就会找不到数据了因为key-01对应的数据存储在节点A上而不是节点B
![](https://static001.geekbang.org/resource/image/27/13/270362d08b0ea77437abcf032634b713.jpg "图3")
同样的道理如果我们需要下线1个服务器节点也就是缩容也会存在类似的可能查询不到数据的问题。
而解决这个问题的办法在于我们要迁移数据基于新的计算公式hash(key-01) % 4 ,来重新对数据和节点做映射。需要你注意的是,数据的迁移成本是非常高的。
为了便于你理解我举个例子对于1000万key的3节点KV存储如果我们增加1个节点变为4节点集群则需要迁移75%的数据。
```
$ go run ./hash.go -keys 10000000 -nodes 3 -new-nodes 4
74.999980%
```
**从示例代码的输出,你可以看到,迁移成本是非常高昂的,这在实际生产环境中也是无法想象的。**
那我们如何通过一致哈希解决这个问题呢?
## 如何使用一致哈希实现哈希寻址?
一致哈希算法也用了取模运算但与哈希算法不同的是哈希算法是对节点的数量进行取模运算而一致哈希算法是对2^32进行取模运算。你可以想象下一致哈希算法将整个哈希值空间组织成一个虚拟的圆环也就是哈希环
![](https://static001.geekbang.org/resource/image/a1/89/a15f17c6951dd1e195d5142f5087ef89.jpg "图4")
从图4中你可以看到哈希环的空间是按顺时针方向组织的圆环的正上方的点代表00点右侧的第一个点代表1以此类推2、3、4、5、6……直到2^32-1也就是说0点左侧的第一个点代表2^32-1。
在一致哈希中你可以通过执行哈希算法为了演示方便假设哈希算法函数为“c-hash()”将节点映射到哈希环上比如选择节点的主机名作为参数执行c-hash(),那么每个节点就能确定其在哈希环上的位置了:
![](https://static001.geekbang.org/resource/image/3c/f5/3cb21a553580afbc840b68d4c6b128f5.jpg "图5")
当需要对指定key的值进行读写的时候你可以通过下面2步进行寻址
* 首先将key作为参数执行c-hash()计算哈希值并确定此key在环上的位置
* 然后从这个位置沿着哈希环顺时针“行走”遇到的第一节点就是key对应的节点。
为了帮助你更好地理解如何通过一致哈希进行寻址我举个例子。假设key-01、key-02、key-03 三个key经过哈希算法c-hash()计算后在哈希环上的位置就像图6的样子
![](https://static001.geekbang.org/resource/image/00/3a/00e85e7abdc1dc0488af348b76ba9c3a.jpg "图6")
那么根据一致哈希算法key-01将寻址到节点Akey-02将寻址到节点Bkey-03将寻址到节点C。讲到这儿你可能会问“老韩那一致哈希是如何避免哈希算法的问题呢
别着急接下来我分别以增加节点和移除节点为例具体说一说一致哈希是如何避免上面的问题的。假设现在有一个节点故障了比如节点C
![](https://static001.geekbang.org/resource/image/68/2e/68a09f7bc604040302fb2d3f102b422e.jpg "图7")
你可以看到key-01和key-02不会受到影响只有key-03的寻址被重定位到A。一般来说在一致哈希算法中如果某个节点宕机不可用了那么受影响的数据仅仅是会寻址到此节点和前一节点之间的数据。比如当节点C宕机了受影响的数据是会寻址到节点B和节点C之间的数据例如key-03寻址到其他哈希环空间的数据例如key-01不会受到影响。
那如果此时集群不能满足业务的需求需要扩容一个节点也就是增加一个节点比如D
![](https://static001.geekbang.org/resource/image/91/d9/913e4709c226dae2bec0500b90d597d9.jpg "图8")
你可以看到key-01、key-02不受影响只有key-03的寻址被重定位到新节点D。一般而言在一致哈希算法中如果增加一个节点受影响的数据仅仅是会寻址到新节点和前一节点之间的数据其它数据也不会受到影响。
让我们一起来看一个例子。使用一致哈希的话对于1000万key的3节点KV存储如果我们增加1个节点变为4节点集群只需要迁移24.3%的数据:
```
$ go run ./consistent-hash.go -keys 10000000 -nodes 3 -new-nodes 4
24.301550%
```
**你看,使用了一致哈希后,我们需要迁移的数据量仅为使用哈希算法时的三分之一,是不是大大提升效率了呢?**
总的来说,使用了一致哈希算法后,扩容或缩容的时候,都只需要重定位环空间中的一小部分数据。**也就是说,一致哈希算法具有较好的容错性和可扩展性。**
**需要你注意的是,在哈希寻址中常出现这样的问题:**客户端访问请求集中在少数的节点上,出现了有些机器高负载,有些机器低负载的情况,那么在一致哈希中,有什么办法能让数据访问分布的比较均匀呢?答案就是虚拟节点。
在一致哈希中,如果节点太少,容易因为节点分布不均匀造成数据访问的冷热不均,也就是说大多数访问请求都会集中少量几个节点上:
![](https://static001.geekbang.org/resource/image/d0/14/d044611092965188e28cd2daf8336814.jpg "图9")
你能从图中看到虽然有3个节点但访问请求主要集中的节点A上。**那如何通过虚拟节点解决冷热不均的问题呢?**
其实,就是对每一个服务器节点计算多个哈希值,在每个计算结果位置上,都放置一个虚拟节点,并将虚拟节点映射到实际节点。比如,可以在主机名的后面增加编号,分别计算 “Node-A-01”“Node-A-02”“Node-B-01”“Node-B-02”“Node-C-01”“Node-C-02”的哈希值于是形成6个虚拟节点
![](https://static001.geekbang.org/resource/image/75/d4/75527ae8011c8311dfb29c4b8ac005d4.jpg "图10")
你可以从图中看到增加了节点后节点在哈希环上的分布就相对均匀了。这时如果有访问请求寻址到“Node-A-01”这个虚拟节点将被重定位到节点A。你看这样我们就解决了冷热不均的问题。
最后我想说的是,可能有同学已经发现了,当节点数越多的时候,使用哈希算法时,需要迁移的数据就越多,使用一致哈希时,需要迁移的数据就越少:
```
$ go run ./hash.go -keys 10000000 -nodes 3 -new-nodes 4
74.999980%
$ go run ./hash.go -keys 10000000 -nodes 10 -new-nodes 11
90.909000%
$ go run ./consistent-hash.go -keys 10000000 -nodes 3 -new-nodes 4
24.301550%
$ go run ./consistent-hash.go -keys 10000000 -nodes 10 -new-nodes 11
6.479330%
```
从示例代码的输出中你可以看到当我们向10个节点集群中增加节点时**如果使用了哈希算法需要迁移高达90.91%的数据使用一致哈希的话只需要迁移6.48%的数据。**
我希望你能注意到这个规律,使用一致哈希实现哈希寻址时,可以通过增加节点数降低节点宕机对整个集群的影响,以及故障恢复时需要迁移的数据量。后续在需要时,你可以通过增加节点数来提升系统的容灾能力和故障恢复效率。
## 内容小结
以上就是本节课的全部内容了,本节课我主要带你了解了哈希算法的缺点、一致哈希的原理等内容。我希望你明确这样几个重点。
* 一致哈希是一种特殊的哈希算法,在使用一致哈希算法后,节点增减变化时只影响到部分数据的路由寻址,也就是说我们只要迁移部分数据,就能实现集群的稳定了。
* 当节点数较少时,可能会出现节点在哈希环上分布不均匀的情况。这样每个节点实际占据环上的区间大小不一,最终导致业务对节点的访问冷热不均。需要你注意的是,这个问题可以通过引入更多的虚拟节点来解决。
最后我想说的是一致哈希本质上是一种路由寻址算法适合简单的路由寻址场景。比如在KV存储系统内部它的特点是简单不需要维护路由信息。
## 课堂思考
Raft集群具有容错能力能容忍少数的节点故障那么在多个Raft集群组成的KV系统中如何设计一致哈希实现当某个集群的领导者节点出现故障并选举出新的领导者后整个系统还能稳定运行呢欢迎在留言区分享你的看法与我一同讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。