nacos客戶端一致性hash負(fù)載需求實(shí)現(xiàn)
最近接到一個(gè)需求,由于文件服務(wù)器上傳文件后,不同節(jié)點(diǎn)之間共享文件需要延遲,上游上傳文件后立刻去下載,如果負(fù)載到其他節(jié)點(diǎn)上可能會(huì)找不到文件,所以使用文件服務(wù)器接入nacos根據(jù)相同的trace_id路由到一個(gè)節(jié)點(diǎn)上,這樣保證上傳后立刻下載的請求都能路由到同一個(gè)節(jié)點(diǎn)上,研究了兩天nacos原生的client發(fā)現(xiàn)并沒有提供相關(guān)功能,于是便產(chǎn)生了一個(gè)想法,手?jǐn)]一個(gè)客戶端負(fù)載。
要想同一個(gè)trace_id需要路由到相同的節(jié)點(diǎn)上,首先想到的方法就是利用hash算法,目前常用于分布式系統(tǒng)中負(fù)載的哈希算法分為兩種:
1.普通hash取模
2.一致性hash
普通hash雖然開發(fā)起來快,看起來也滿足需求,但是當(dāng)集群擴(kuò)容或者縮容的時(shí)候,就會(huì)造成trace_id的hash結(jié)果與之前不同,可能不會(huì)路由到一個(gè)節(jié)點(diǎn)上。
而一致性hash在擴(kuò)容縮容時(shí)只會(huì)影響哈希環(huán)中順時(shí)針方向的相鄰的節(jié)點(diǎn), 對其他節(jié)點(diǎn)無影響。但是缺點(diǎn)時(shí)數(shù)據(jù)的分布和節(jié)點(diǎn)的位置有關(guān),因?yàn)檫@些節(jié)點(diǎn)不是均勻的分布在哈希環(huán)上的,不過根據(jù)選擇適當(dāng)?shù)膆ash算法也可以避免這個(gè)缺點(diǎn),讓數(shù)據(jù)相對均勻的在hash環(huán)上分布。
網(wǎng)上關(guān)于一致性hash的討論已經(jīng)有很多了,在這里就放一張圖便于大家理解。

其他的不說,先上代碼
1.首先創(chuàng)建NacosClient,監(jiān)聽對應(yīng)的服務(wù):
public class NacosClient {
//nacos監(jiān)聽器,處理初始化hash環(huán)等邏輯
private Nodelistener nodelistener;
//初始化nacosClient,并且監(jiān)聽服務(wù)
public void init() {
NamingService naming = null;
try {
System.out.println(System.getProperty("serveAddr"));
naming = NamingFactory.createNamingService(System.getProperty("serveAddr"));
//注冊監(jiān)聽器,當(dāng)集群節(jié)點(diǎn)變化的時(shí)候調(diào)用nodelistener處理節(jié)點(diǎn)信息
naming.subscribe("test", event -> {
if (event instanceof NamingEvent) {
nodelistener.handlerChange((NamingEvent)event);
}
});
} catch (NacosException e) {
e.printStackTrace();
}
}
}2.創(chuàng)建Nodelistener,主要處理構(gòu)建hash環(huán)等邏輯:
public class Nodelistener {
private List<Instance> servers;
//利用treeMap構(gòu)建hash環(huán)
private volatile SortedMap<Long, Instance> sortedMap = new TreeMap<Long, Instance>();
//虛擬節(jié)點(diǎn)
private int virtualNodeCount = 100;
public synchronized void handlerChange(NamingEvent event) {
List<Instance> servers = new ArrayList<Instance>();
event.getInstances().stream().filter(instance -> {
return instance.isEnabled() && instance.isHealthy();
}).forEach(instance -> servers.add(instance));
this.onChange(servers);
}
//每次集群節(jié)點(diǎn)變化時(shí),重新構(gòu)建hash環(huán)
public void onChange(List<Instance> servers) {
//只有一個(gè)節(jié)點(diǎn)的時(shí)候這里暫不考慮,讀者可以自行處理
if(servers.size() != 1) {
SortedMap<Long, Instance> newSortedMap = new TreeMap<Long, Instance>();
for (int i = 0; i < servers.size(); i ++) {
for (int j = 0; j < this.virtualNodeCount; j++) {
//計(jì)算虛擬節(jié)點(diǎn)的hash,這里用到的是MurMurHash,網(wǎng)上還有很多其他hash實(shí)現(xiàn),
//有興趣可以自行查閱,具體實(shí)現(xiàn)細(xì)節(jié)就不列出了
Long hash = HashUtil.getHash(servers.get(i).getIp() + ":" +
servers.get(i).getPort() + j);
//把虛擬節(jié)點(diǎn)加入hash環(huán)
sortedMap.put(hash, servers.get(i));
}
}
sortedMap = newSortedMap;
}
this.servers = servers;
}
//根據(jù)傳入的key獲取hash環(huán)上順時(shí)針到hash環(huán)尾部部分所有節(jié)點(diǎn)
public Instance getInstance(String str) {
Long hash = HashUtil.getHash(str);
SortedMap<Long, Instance> map = sortedMap.tailMap(hash);
//這里證明剛好獲取的是尾部,所以返回所有的節(jié)點(diǎn),其實(shí)是獲取第一個(gè)節(jié)點(diǎn)
if (map.isEmpty()) {
map = sortedMap;
}
return map.get(map.firstKey());
}
}其中,虛擬節(jié)點(diǎn)是一致性hash經(jīng)常用到的,主要是用于解決hash傾斜問題,即節(jié)點(diǎn)數(shù)比較少時(shí),數(shù)據(jù)落在hash環(huán)上會(huì)造成不均衡,下圖即沒有虛擬節(jié)點(diǎn)的情況:

有虛擬節(jié)點(diǎn)的情況,這樣hash環(huán)就均勻分割,相應(yīng)數(shù)據(jù)落入的區(qū)間也會(huì)平衡:

3.負(fù)載均衡器:
public class LoadBalance {
private Nodelistener nodelistener;
//只需要簡單的從hash環(huán)中獲取第一個(gè)節(jié)點(diǎn)
public Instance doSelect(String key) {
return nodelistener.getInstance(key);
}
}測試下結(jié)果:
public void test2() {
Map<String, Integer> map = new HashMap<String, Integer>();
Random random = new Random();
for (int i = 0; i < 10000; i ++) {
String key = String.valueOf(random.nextLong());
Instance instance = loadBalance.doSelect(key);
if(!map.containsKey(instance.getIp())) {
map.put(instance.getIp(), 0);
}else {
map.put(instance.getIp(), map.get(instance.getIp()) + 1);
}
System.out.println("test2 count :" + i);
System.out.printf("select IP is :" + instance.getIp());
}
System.out.println(map.toString());
}此處為了方便就直接用隨機(jī)數(shù)模擬trace_id,結(jié)果如下:
select IP is :127.0.0.0{127.0.0.4=2031, 127.0.0.3=2144, 127.0.0.2=1925, 127.0.0.1=1931, 127.0.0.0=1964}
可以看到10000次請求被均勻的分布到了4個(gè)節(jié)點(diǎn)上。
思考:
1. 本次我們使用到了treeMap構(gòu)建hash環(huán),那么treeMap構(gòu)建的hash具體的查找效率如何呢?
treeMap是由紅黑樹構(gòu)成的,其 containsKey(),get(),put(), remove() 方法時(shí)間復(fù)雜度均為O(logn),均是對數(shù)階,已經(jīng)算相當(dāng)不錯(cuò)了。
2.在Nodelistener 中我們兩個(gè)方法都使用了synchronized 這樣會(huì)有什么影響?
首先因?yàn)閠reeMap是線程不安全的,所以我們都使用了方法級(jí)別的synchronized,所以兩個(gè)方法不會(huì)同時(shí)執(zhí)行,這樣使用treeMap時(shí),不會(huì)造成線程不安全問題,其次可以保證我們在獲取hash環(huán)中節(jié)點(diǎn)的時(shí)候,treeMap不會(huì)因?yàn)楣?jié)點(diǎn)變化而變化。但是這樣處理的話就會(huì)產(chǎn)生一個(gè)問題,我們正在計(jì)算trace_id的路由節(jié)點(diǎn)時(shí),機(jī)器不巧縮容了,treeMap還沒進(jìn)行更新,剛好路由到的節(jié)點(diǎn)時(shí)下線的機(jī)器,那么就會(huì)訪問失敗,筆者這里解決這個(gè)問題的思路是重試,如果失敗獲取下一個(gè)節(jié)點(diǎn),此時(shí)文件服務(wù)器不同節(jié)點(diǎn)之間文件已經(jīng)同步完畢,所以不同節(jié)點(diǎn)訪問是沒問題的。
public void test(String key) throws InterruptedException {
//這里可以設(shè)置重試次數(shù)
for (;;) {
Instance instance = loadBalance.doSelect(key);
String addr = instance.getIp() + ":" + instance.getPort();
//測試請求
if(post(addr)) {
//成功邏輯
.....
break;
}else {
//等待兩秒,即可以使文件服務(wù)器不同節(jié)點(diǎn)之間同步文件,還可以等待更新本地hash環(huán)
Thread.sleep(2000);
//失敗則選取下一個(gè)節(jié)點(diǎn)
instance = loadBalance.doSelect(key);
//此處可以增加重試次數(shù)邏輯和如果重試到hash環(huán)上最后一個(gè)節(jié)點(diǎn)則重新獲取hash環(huán)第一個(gè)節(jié)點(diǎn)邏輯,
// 在此就不做論述,讀者可以自由發(fā)揮
continue;
}
}
}那么我們考慮當(dāng)我們計(jì)算trace_id路由時(shí),正好擴(kuò)容的情況,此時(shí)treeMap還沒有進(jìn)行更新,情況如下圖,我們路由到的節(jié)點(diǎn)如果不是圖中標(biāo)記的受影響區(qū)域則不會(huì)有影響,如果是圖中受影響的區(qū)域計(jì)算得出的路由是擴(kuò)容前的也就是 127.0.0.2-1(真實(shí)節(jié)點(diǎn)是127.0.0.2),那么下次相同的trace_id則會(huì)路由到新節(jié)點(diǎn),此時(shí)會(huì)出現(xiàn)同一個(gè)trace_id路由到的節(jié)點(diǎn)不一樣的問題,筆者在此處也使用的重試機(jī)制。(其實(shí)這個(gè)地方可以使用緩存Key和節(jié)點(diǎn)的關(guān)系,擴(kuò)容后關(guān)系改變之后再改變圖中受影響的hash環(huán),但是因?yàn)閠race_id比較特殊,并不適合緩存所有,所以使用了重試機(jī)制)

3.每次探知到服務(wù)器節(jié)點(diǎn)變化的時(shí)候都需要重新構(gòu)建hash環(huán),這樣操作會(huì)比較耗時(shí),可以修改成每次節(jié)點(diǎn)變化只需要改變對應(yīng)虛擬節(jié)點(diǎn)信息,更新本地hash環(huán)時(shí)間,可以將onChange方法改造下。
public void onChange(List<Instance> newServers) {
//單節(jié)點(diǎn)時(shí)這里暫不考慮
if(servers.size() != 1 ) {
//TODO ..
}
Map<String, Instance> oldAddrs =
this.servers.stream()
.collect(Collectors.toMap(Instance::toInetAddr, instance -> instance));
Map<String, Instance> newAddrs =
newServers.stream()
.collect(Collectors.toMap(Instance::toInetAddr, instance -> instance));
//remove
oldAddrs.forEach((key, value) -> {
if (!newAddrs.containsKey(key)) {
for(int j = 0; j < virtualNodeCount; j++) {
Long hash = HashUtil.getHash( value.toInetAddr() + "&&VM"+ j);
sortedMap.remove(hash);
}
}
});
//add
newAddrs.forEach((key, value) -> {
if (!oldAddrs.containsKey(key)) {
for(int j = 0; j < virtualNodeCount; j++) {
Long hash = HashUtil.getHash(value.toInetAddr() + "&&VM" + j);
sortedMap.put(hash, value);
}
}
});
this.servers = newServers;
}以上就是nacos客戶端一致性hash負(fù)載需求實(shí)現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于nacos客戶端一致性hash負(fù)載的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JAVA簡單實(shí)現(xiàn)MD5注冊登錄加密實(shí)例代碼
本篇文章主要介紹了JAVA簡單實(shí)現(xiàn)MD5注冊登錄加密實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03
詳解Java如何判斷ResultSet結(jié)果集是否為空
ResultSet 表示 select 語句的查詢結(jié)果集。這篇文章主要為大家詳細(xì)介紹了Java如何判斷ResultSet結(jié)果集是否為空,感興趣的可以了解一下2023-02-02
IDEA使用jformdesigner插件做管理系統(tǒng)MVC架構(gòu)的步驟和實(shí)現(xiàn)思路
在?IntelliJ?IDEA?中結(jié)合?JFormDesigner?插件,通過?Swing?框架實(shí)現(xiàn)一個(gè)管理系統(tǒng)的?MVC?架構(gòu)是一種經(jīng)典的開發(fā)方式,以下是具體的步驟和實(shí)現(xiàn)思路,包含從項(xiàng)目創(chuàng)建到?MVC?架構(gòu)的核心代碼實(shí)現(xiàn),需要的朋友可以參考下2024-12-12
Java中面向?qū)ο蟮闹R(shí)點(diǎn)總結(jié)
Java是一門面向?qū)ο蟮恼Z言。對象是Java程序中的基本實(shí)體。除了對象之外Java程序同樣處理基本數(shù)據(jù)。下面這篇文章主要給大家總結(jié)了關(guān)于Java中面向?qū)ο蟮闹R(shí)點(diǎn),需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02
Java中LinkedList數(shù)據(jù)結(jié)構(gòu)的詳細(xì)介紹
這篇文章主要介紹了Java中LinkedList,Linked List 是 java.util 包中 Collection 框架的一部分,文中提供了詳細(xì)的代碼說明,需要的朋友可以參考下2023-05-05
rabbitmq學(xué)習(xí)系列教程之消息應(yīng)答(autoAck)、隊(duì)列持久化(durable)及消息持久化
這篇文章主要介紹了rabbitmq學(xué)習(xí)系列教程之消息應(yīng)答(autoAck)、隊(duì)列持久化(durable)及消息持久化,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
如何解決SpringBoot集成百度UEditor圖片上傳后直接訪問404
在本篇文章里小編給大家整理的是一篇關(guān)于如何解決SpringBoot集成百度UEditor圖片上傳后直接訪問404相關(guān)文章,需要的朋友們學(xué)習(xí)下。2019-11-11
SpringBoot實(shí)現(xiàn)讀取YML,yaml,properties文件
yml,yaml,properties三種文件都是用來存放配置的文件,一些靜態(tài)數(shù)據(jù),配置的數(shù)據(jù)都會(huì)存放到里邊。本文主要為大家整理了SpringBoot實(shí)現(xiàn)讀取YML,yaml,properties文件的方法,需要的可以參考一下2023-04-04

