一致性Hash在负载均衡中的应用Word格式文档下载.docx
- 文档编号:17786069
- 上传时间:2022-12-10
- 格式:DOCX
- 页数:15
- 大小:893.85KB
一致性Hash在负载均衡中的应用Word格式文档下载.docx
《一致性Hash在负载均衡中的应用Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《一致性Hash在负载均衡中的应用Word格式文档下载.docx(15页珍藏版)》请在冰豆网上搜索。
最终对分布式缓存造成的影响就是,集群的每个实例上储存的缓存数据量不一致,会发生严重的数据倾斜。
缓存雪崩
如果每个节点在环上只有一个节点,那么可以想象,当某一集群从环中消失时,它原本所负责的任务将全部交由顺时针方向的下一个集群处理。
例如,当group0退出时,它原本所负责的缓存将全部交给group1处理。
这就意味着group1的访问压力会瞬间增大。
设想一下,如果group1因为压力过大而崩溃,那么更大的压力又会向group2压过去,最终服务压力就像滚雪球一样越滚越大,最终导致雪崩。
引入虚拟节点
解决上述两个问题最好的办法就是扩展整个环上的节点数量,因此我们引入了虚拟节点的概念。
一个实际节点将会映射多个虚拟节点,这样Hash环上的空间分割就会变得均匀。
同时,引入虚拟节点还会使得节点在Hash环上的顺序随机化,这意味着当一个真实节点失效退出后,它原来所承载的压力将会均匀地分散到其他节点上去。
如下图:
代码测试
现在我们尝试编写一些测试代码,来看看一致性hash的实际效果是否符合我们预期。
首先我们需要一个能够对输入进行均匀散列的Hash算法,可供选择的有很多,memcached官方使用了基于md5的KETAMA算法,但这里处于计算效率的考虑,使用了FNV1_32_HASH算法,如下:
publicclassHashUtil{
/**
*计算Hash值,使用FNV1_32_HASH算法
*@paramstr
*@return
*/
publicstaticintgetHash(Stringstr){
finalintp=16777619;
inthash=(int)2166136261L;
for(inti=0;
i<
str.length();
i++){
hash=(hash^str.charAt(i))*p;
}
hash+=hash<
<
13;
hash^=hash>
>
7;
3;
17;
5;
if(hash<
0){
hash=Math.abs(hash);
returnhash;
}
实际使用时可以根据需求调整。
接着需要使用一种数据结构来保存hash环,可以采用的方案有很多种,最简单的是采用数组或链表。
但这样查找的时候需要进行排序,如果节点数量多,速度就可能变得很慢。
针对集群负载均衡状态读多写少的状态,很容易联想到使用二叉平衡树的结构去储存,实际上可以使用TreeMap(内部实现是红黑树)来作为Hash环的储存结构。
先编写一个最简单的,无虚拟节点的Hash环测试:
publicclassConsistentHashingWithoutVirtualNode{
*集群地址列表
privatestaticString[]groups={
"
192.168.0.0:
111"
"
192.168.0.1:
192.168.0.2:
192.168.0.3:
192.168.0.4:
};
*用于保存Hash环上的节点
privatestaticSortedMap<
Integer,String>
sortedMap=newTreeMap<
();
*初始化,将所有的服务器加入Hash环中
static{
//使用红黑树实现,插入效率比较差,但是查找效率极高
for(Stringgroup:
groups){
inthash=HashUtil.getHash(group);
System.out.println("
["
+group+"
]launched@"
+hash);
sortedMap.put(hash,group);
*计算对应的widget加载在哪个group上
*
*@paramwidgetKey
privatestaticStringgetServer(StringwidgetKey){
inthash=HashUtil.getHash(widgetKey);
//只取出所有大于该hash值的部分而不必遍历整个Tree
SortedMap<
subMap=sortedMap.tailMap(hash);
if(subMap==null||subMap.isEmpty()){
//hash值在最尾部,应该映射到第一个group上
returnsortedMap.get(sortedMap.firstKey());
returnsubMap.get(subMap.firstKey());
publicstaticvoidmain(String[]args){
//生成随机数进行测试
Map<
String,Integer>
resMap=newHashMap<
100000;
IntegerwidgetId=(int)(Math.random()*10000);
Stringserver=getServer(widgetId.toString());
if(resMap.containsKey(server)){
resMap.put(server,resMap.get(server)+1);
}else{
resMap.put(server,1);
resMap.forEach(
(k,v)->
{
group"
+k+"
:
+v+"
("
+v/1000.0D+"
%)"
);
);
生成10000个随机数字进行测试,最终得到的压力分布情况如下:
[192.168.0.1:
111]launched@8518713
[192.168.0.2:
111]launched@1361847097
[192.168.0.3:
111]launched@1171828661
[192.168.0.4:
111]launched@1764547046
group192.168.0.2:
111:
8572(8.572%)
group192.168.0.1:
18693(18.693%)
group192.168.0.4:
17764(17.764%)
group192.168.0.3:
27870(27.87%)
group192.168.0.0:
27101(27.101%)
可以看到压力还是比较不平均的,所以我们继续,引入虚拟节点:
publicclassConsistentHashingWithVirtualNode{
*真实集群列表
privatestaticList<
String>
realGroups=newLinkedList<
*虚拟节点映射关系
virtualNodes=newTreeMap<
privatestaticfinalintVIRTUAL_NODE_NUM=1000;
//先添加真实节点列表
realGroups.addAll(Arrays.asList(groups));
//将虚拟节点映射到Hash环上
for(StringrealGroup:
realGroups){
VIRTUAL_NODE_NUM;
StringvirtualNodeName=getVirtualNodeName(realGroup,i);
inthash=HashUtil.getHash(virtualNodeName);
+virtualNodeName+"
virtualNodes.put(hash,virtualNodeName);
privatestaticStringgetVirtualNodeName(StringrealName,intnum){
returnrealName+"
&
VN"
+String.valueOf(num);
privatestaticStringgetRealNodeName(StringvirtualName){
returnvirtualName.split("
"
)[0];
subMap=virtualNodes.tailMap(hash);
StringvirtualNodeName;
virtualNodeName=virtualNodes.get(virtualNodes.firstKey());
}else{
virtualNodeName=subMap.get(subMap.firstKey());
returngetRealNodeName(virtualNodeName);
IntegerwidgetId=i;
Stringgroup=getServer(widgetId.toString());
if(resMap.containsKey(group)){
resMap.put(group,resMap.get(group)+1);
resMap.put(group,1);
+v/100000.0D+"
这里真实节点和虚拟节点的映射采用了字符串拼接的方式,这种方式虽然简单但很有效,memcached官方也是这么实现的。
将虚拟节点的数量设置为1000,重新测试压力分布情况,结果如下:
18354(18.354%)
20062(20.062%)
20749(20.749%)
20116(20.116%)
20719(20.719%)
可以看到基本已经达到平均分布了,接着继续测试删除和增加节点给整个服务带来的影响,相关测试代码如下:
privatestaticvoidrefreshHashCircle(){
//当集群变动时,刷新hash环,其余的集群在hash环上的位置不会发生变动
virtualNodes.clear();
for(inti=0;
StringvirtualNodeName=getVirtualNodeName(realGroup,i);
privatestaticvoidaddGroup(Stringidentifier){
realGroups.add(identifier);
refreshHashCircle();
privatestaticvoidremoveGroup(Stringidentifier){
inti=0;
for(Stringgroup:
realGroups){
if(group.equals(identifier)){
realGroups.remove(i);
i++;
测试删除一个集群前后的压力分布如下:
runningthenormaltest.
19144(19.144%)
20244(20.244%)
20923(20.923%)
19811(19.811%)
19878(19.878%)
removedagroup,runtestagain.
23409(23.409%)
25628(25.628%)
25583(25.583%)
25380(25.38%)
同时计算一下消失的集群上的Key最终如何转移到其他集群上:
111-192.168.0.4:
111]:
5255
111-192.168.0.3:
5090
111-192.168.0.2:
5069
111-192.168.0.0:
4938
可见,删除集群后,该集群上的压力均匀地分散给了其他集群,最终整个集群仍处于负载均衡状态,符合我们的预期,最后看一下添加集群的情况。
压力分布:
18890(18.89%)
20293(20.293%)
21000(21.0%)
19816(19.816%)
20001(20.001%)
addagroup,runtestagain.
15524(15.524%)
group192.168.0.7:
16928(16.928%)
16888(16.888%)
16965(16.965%)
16768(16.768%)
16927(16.927%)
压力转移:
[192.168.0.0:
111-192.168.0.7:
3102
4060
3313
3292
3261
综上可以得出结论,在引入足够多的虚拟节点后,一致性hash还是能够比较完美地满足负载均衡需要的。
优雅缩扩容
缓存服务器对于性能有着较高的要求,因此我们希望在扩容时新的集群能够较快的填充好数据并工作。
但是从一个集群启动,到真正加入并可以提供服务之间还存在着不小的时间延迟,要实现更优雅的扩容,我们可以从两个方面出发:
1.高频Key预热
负载均衡器作为路由层,是可以收集并统计每个缓存Key的访问频率的,如果能够维护一份高频访问Key的列表,新的集群在启动时根据这个列表提前拉取对应Key的缓存值进行预热,便可以大大减少因为新增集群而导致的Key失效。
具体的设计可以通过缓存来实现,如下:
不过这个方案在实际使用时有一个很大的限制,那就是高频Key本身的缓存失效时间可能很短,预热时储存的Value在实际被访问到时可能已经被更新或者失效,处理不当会导致出现脏数据,因此实现难度还是有一些大的。
2.历史Hash环保留
回顾一致性Hash的扩容,不难发现新增节点后,它所对应的Key在原来的节点还会保留一段时间。
因此在扩容的延迟时间段,如果对应的Key缓存在新节点上还没有被加载,可以去原有的节点上尝试读取。
举例,假设我们原有3个集群,现在要扩展到6个集群,这就意味着原有50%的Key都会失效(被转移到新节点上),如果我们维护扩容前和扩容后的两个Hash环,在扩容后的Hash环上找不到Key的储存时,先转向扩容前的Hash环寻找一波,如果能够找到就返回对应的值并将该缓存写入新的节点上,找不到时再透过缓存,如下图:
这样做的缺点是增加了缓存读取的时间,但相比于直接击穿缓存而言还是要好很多的。
优点则是可以随意扩容多台机器,而不会产生大面积的缓存失效。
谈完了扩容,再谈谈缩容。
1.熔断机制
缩容后,剩余各个节点上的访问压力都会有所增加,此时如果某个节点因为压力过大而宕机,就可能会引发连锁反应。
因此作为兜底方案,应当给每个集群设立对应熔断机制来保护服务的稳定性。
2.多集群LB的更新延迟
这个问题在缩容时比较严重,如果你使用一个集群来作为负载均衡,并使用一个配置服务器比如ConfigServer来推送集群状态以构建Hash环,那么在某个集群退出时这个状态并不一定会被立刻同步到所有的LB上,这就可能会导致一个暂时的调度不一致,如下图:
如果某台LB错误地将请求打到了已经退出的集群上,就会导致缓存击穿。
解决这个问题主要有以下几种思路:
-缓慢缩容,等到Hash环完全同步后再操作。
可以通过监听退出集群的访问QPS来实现
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 一致性 Hash 负载 均衡 中的 应用