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)上,這樣保證上傳后立刻下載的請(qǐng)求都能路由到同一個(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), 對(duì)其他節(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ù)相對(duì)均勻的在hash環(huán)上分布。
網(wǎng)上關(guān)于一致性hash的討論已經(jīng)有很多了,在這里就放一張圖便于大家理解。
其他的不說,先上代碼
1.首先創(chuàng)建NacosClient,監(jiān)聽對(duì)應(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")); //注冊(cè)監(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; //只需要簡(jiǎn)單的從hash環(huán)中獲取第一個(gè)節(jié)點(diǎn) public Instance doSelect(String key) { return nodelistener.getInstance(key); } }
測(cè)試下結(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次請(qǐng)求被均勻的分布到了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),均是對(duì)數(shù)階,已經(jīng)算相當(dāng)不錯(cuò)了。
2.在Nodelistener 中我們兩個(gè)方法都使用了synchronized 這樣會(huì)有什么影響?
首先因?yàn)閠reeMap是線程不安全的,所以我們都使用了方法級(jí)別的synchronized,所以兩個(gè)方法不會(huì)同時(shí)執(zhí)行,這樣使用treeMap時(shí),不會(huì)造成線程不安全問題,其次可以保證我們?cè)讷@取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(); //測(cè)試請(qǐng)求 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)變化只需要改變對(duì)應(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ù)載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)添加條形碼到PDF表格的方法詳解
條碼的應(yīng)用已深入生活和工作的方方面面。本文以操作PDF文件為例,介紹如何利用Java語言在編輯表格時(shí),向單元格中添加條形碼,感興趣的可以學(xué)習(xí)一下2022-06-06SpringBoot2.0整合Shiro框架實(shí)現(xiàn)用戶權(quán)限管理的示例
這篇文章主要介紹了SpringBoot2.0整合Shiro框架實(shí)現(xiàn)用戶權(quán)限管理的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08java多線程實(shí)現(xiàn)服務(wù)器端與多客戶端之間的通信
本篇文章主要介紹了java多線程實(shí)現(xiàn)服務(wù)器端與多客戶端之間的通信,介紹了多線程來實(shí)現(xiàn)服務(wù)器與多線程之間的通信的基本步驟,有需要的小伙伴可以參考下。2016-10-10談?wù)凧ava利用原始HttpURLConnection發(fā)送POST數(shù)據(jù)
這篇文章主要給大家介紹java利用原始httpUrlConnection發(fā)送post數(shù)據(jù),設(shè)計(jì)到httpUrlConnection類的相關(guān)知識(shí),感興趣的朋友跟著小編一起學(xué)習(xí)吧2015-10-10使用C3P0改造JDBC對(duì)數(shù)據(jù)庫的連接
這篇文章主要為大家詳細(xì)介紹了使用C3P0改造JDBC對(duì)數(shù)據(jù)庫的連接,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08Eclipse新建項(xiàng)目不可選擇Java Project問題解決方案
這篇文章主要介紹了Eclipse新建項(xiàng)目不可選擇Java Project問題解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07基于OpenCv與JVM實(shí)現(xiàn)加載保存圖像功能(JAVA?圖像處理)
openCv有一個(gè)名imread的簡(jiǎn)單函數(shù),用于從文件中讀取圖像,本文給大家介紹JAVA?圖像處理基于OpenCv與JVM實(shí)現(xiàn)加載保存圖像功能,感興趣的朋友一起看看吧2022-01-01