【Java高階工程師蛻變之路】026 一致性hash問題及解決方案
分散式和叢集的區別
分散式一定是叢集,但是叢集不一定是分散式。叢集是多個例項一起工作,分散式啊將一個系統拆分,拆分之後就是多個例項。複製性的叢集不是拆分,是複製。
更清晰的解釋
https://cloud.tencent.com/developer/article/1579435
大白話的解釋
專業案例解釋
hash演算法回顧
什麼是hash演算法
雜湊演算法(Hash)又稱摘要演算法(Digest),它的作用是:對任意一組輸入資料進行計算,得到一個固定長度的輸出摘要。
WikiPedia的解釋
A hash function is any function that can be used to map data of arbitrary size to fixed-size values.
為什麼要使用hash
Hash演算法較多的應用在資料儲存和查詢領域。最經典的就是Hash表,它的查詢效率非常之高,其中的 雜湊演算法如果設計的比較ok的話,那麼Hash表的資料查詢時間複雜度可以接近於O(1)。
hash演算法例項
需求
提供一組資料 1,5,7,6,3,4,8,對這組資料進行儲存,然後隨便給定一個數n,請你判斷n是否存在於剛才的資料集中
分析與實現
順序查詢法
直接遍歷
list:List[1,5,7,6,3,4,8] // 通過迴圈判斷來實現 for(int element: list) { if(element == n) { // 如果相等,說明n存在於資料集中 } }
程式碼如下
/** * @name: FindNum * @author: terwer * @date: 2022-01-26 14:57 **/ public class FindNum { public static void main(String[] args) { int[] nums = new int[]{1, 5, 7, 6, 3, 4, 8}; int num = 3; boolean result = isExist1(nums, num); System.out.println(num + "是否存在:" + result); } // 順序查詢 public static boolean isExist1(int[] nums, int n) { for (int num : nums) { if (num == n) { return true; } } return false; } }
缺點:通過迴圈來完成,比較原始,效率不高
折半查詢法(二分查詢)
// 折半查詢
public static boolean isExist2(int[] nums, int target) {
System.out.println("折半查詢");
// 先排序,jdk使用的是快速排序
Arrays.sort(nums);
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
if (target == nums[middle]) {
return true;
} else if (target < nums[middle]) {
right = middle - 1;
} else if (target > nums[middle]) {
left = middle - 1;
}
}
return false;
}
排序之後折半查詢,相對於順序查詢法會提高一些效率,但是效率也並不是特別好
缺點:還是需要迴圈
直接定址法
直接把資料和陣列的下標繫結到一起,查詢的時候,直接array[n]就取出了資料
優點:速度快,一次查詢得到結果
缺點
1、浪費空間。例如,1,2,100
2、有重複資料的時候,儲存不下。例如,1,2,3,3,2,1,6
hash定址法
如果資料是3,5,7,12306,一共4個數據,可以用下面方式儲存
對資料求模 (資料%空間位置數) 。
這是一個hash演算法,叫做除留餘數法
開放定址法
1放進去了,6再來的時候,向前或者向後找空閒位置存放
缺點:如果陣列長度固定,可能出現向前或者向後查詢也儲存不下的情況
拉鍊法
可以在元素的地方放一個連結串列。
Hash表的查詢效率高不高取決於Hash演算法,hash演算法的理想實現是能夠讓資料平均分佈,既能夠節省空間又能提高查詢效率。
常見的hash演算法
-
[ ] 除留餘數法 3%5
-
[ ] 線性構造Hash演算法
直接定址法也是一種構造Hash的方式,只不過更簡單,表示式:H(key)=key 比如H(key)=a*key + b(a,b是常量)
- [ ] hashcode其實也是通過一個Hash演算法得來的
hash演算法應用場景
Hash演算法在分散式叢集架構中的應用場景
Hash演算法在很多分散式叢集產品中都有應用,比如分散式叢集架構Redis、Hadoop、ElasticSearch,
Mysql分庫分表,Nginx負載均衡等
最主要的應用場景
- 請求的負載均衡(比如nginx的ip_hash策略)
Nginx的IP_hash策略可以在客戶端ip不變的情況下,將其發出的請求始終路由到同一個目標伺服器上,實現會話粘滯,避免處理session共享問題
如果沒有IP_hash策略,那麼如何實現會話粘滯? 可以維護一張對映表,儲存客戶端IP或者sessionid與具體目標伺服器的對映關係
<ip,tomcat1>
缺點
1)那麼,在客戶端很多的情況下,對映表非常大,浪費記憶體空間
2)客戶端上下線,目標伺服器上下線,都會導致重新維護對映表,對映表維護成本很大
如果使用雜湊演算法,事情就簡單很多,我們可以對ip地址或者sessionid進行計算雜湊值,雜湊值與服務 器數量進行取模運算,得到的值就是當前請求應該被路由到的伺服器編號,如此,同一個客戶端ip傳送過來的請求就可以路由到同一個目標伺服器,實現會話粘滯。
- 分散式儲存
以分散式記憶體資料庫Redis為例,叢集中有redis1,redis2,redis3 三臺Redis伺服器
那麼,在進行資料儲存時,<key1,value1>資料儲存到哪個伺服器當中呢?
針對key進行hash處理 hash(key1)%3=index, 使用餘數index鎖定儲存的具體伺服器節點
普通hash演算法存在的問題
以ip_hash為例,假定下載使用者ip固定沒有發生改變,現在tomcat3出現 了問題,down機了,伺服器數量由3個變為了2個,之前所有的求模都需要重新計算。
如果在真實生產情況下,後臺伺服器很多臺,客戶端也有很多,那麼影響是很大的,縮容和擴容都會存
在這樣的問題,大量使用者的請求會被路由到其他的目標伺服器處理,使用者在原來伺服器中的會話都會丟
失。
一致性hash演算法
首先有一條直線,直線開頭和結尾分別定為為1和2的32次方減1,這相當於一個地址,對於這樣一條線,彎過來構成一個圓環形成閉環,這樣的一個圓環稱為hash環。
我們把伺服器的ip或者主機名求hash值然後對應到hash環上,那麼針對客戶端使用者,也根據它的ip進行hash求值,對應到環上某個位置。
然後如何確定一個客戶端路由到哪個伺服器處理呢?按照順時針方向找最近的伺服器節點。
假如將伺服器3下線,伺服器3下線後,原來路由到3的客戶端重新路由到伺服器4,對於其他客戶端沒有 影響只是這一小部分受影響(請求的遷移達到了最小,這樣的演算法對分散式叢集來說非常合適的,避免
了大量請求遷移 )
增加伺服器5之後,原來路由到3的部分客戶端路由到新增伺服器5上,對於其他客戶端沒有影響只是這 一小部分受影響(請求的遷移達到了最小,這樣的演算法對分散式叢集來說非常合適的,避免了大量請求遷移 )
- 每一臺伺服器負責一段,一致性雜湊演算法對於節點的增減都只需重定位環空間中的一小部分資料,具有較好的容錯性和可擴充套件性。
缺點:資料的傾斜問題
一致性雜湊演算法在服務節點太少時,容易因為節點分部不均勻而造成資料傾斜問題。例如系統中 只有兩臺伺服器,其環分佈如下,節點2只能負責非常小的一段,大量的客戶端
請求落在了節點1上,這就是資料(請求)傾斜問題
- 為了解決這種資料傾斜問題,一致性雜湊演算法引入了虛擬節點機制,即對每一個服務節點計算多個雜湊,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。
具體做法可以在伺服器ip或主機名的後面增加編號來實現。比如,可以為每臺伺服器計算三個虛擬節點,於是可以分別計算 “節點1的ip#1”、“節點1的ip#2”、“節點1的ip#3”、“節點2的ip#1”、“節點2的 ip#2”、“節點2的ip#3”的雜湊值,於是形成六個虛擬節點,當客戶端被路由到虛擬節點的時候其實是被 路由到該虛擬節點所對應的真實節點
手寫實現一致性hash演算法
- 普通Hash演算法實現
/**
* 普通hash演算法
*
* @name: GeneraHash
* @author: terwer
* @date: 2022-01-26 15:46
**/
public class GeneraHash {
public static void main(String[] args) {
// 定義客戶端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
// 定義伺服器數量
int serverCount = 5;// (編號對應0,1,2)
// hash(ip)%node_counts=index //根據index鎖定應該路由到的tomcat伺服器
for (String client : clients) {
int hash = Math.abs(client.hashCode());
int index = hash % serverCount;
System.out.println("客戶端:" + client + " 被路由到伺服器編號為:" + index);
}
}
}
- 一致性Hash演算法實現(不含虛擬節點)
/**
* @name: 一致性Hash演算法不含虛擬節點
* @author: terwer
* @date: 2022-01-26 15:52
**/
public class ConsitanceHashNoVirtual {
public static void main(String[] args) {
//step1 初始化:把伺服器節點IP的雜湊值對應到雜湊環上 // 定義伺服器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
for (String tomcatServer : tomcatServers) {
// 求出每一個ip的hash值,對應到hash環上,儲存hash值與ip的對應關係
int serverHash = Math.abs(tomcatServer.hashCode());
// 儲存hash值與ip的對應關係
hashServerMap.put(serverHash, tomcatServer);
}
//step2 針對客戶端IP求出hash值
// 定義客戶端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 針對客戶端,找到能夠處理當前客戶端請求的伺服器(雜湊環上順時針最近)
// 根據客戶端ip的雜湊值去找出哪一個伺服器節點能夠處理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取雜湊環上的順時針第一臺伺服器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客戶端:" + client + " 被路由到伺服器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客戶端:" + client + " 被路由到伺服器:" + hashServerMap.get(firstKey));
}
}
}
}
- 一致性hash包含虛擬節點
/**
* @name: 一致性hash包括虛擬節點
* @author: terwer
* @date: 2022-01-26 15:58
**/
public class ConstanceHashVirtual {
public static void main(String[] args) {
//step1 初始化:把伺服器節點IP的雜湊值對應到雜湊環上
// 定義伺服器ip
String[] tomcatServers = new String[]{"123.111.0.0", "123.101.3.1", "111.20.35.2", "123.98.26.3"};
SortedMap<Integer, String> hashServerMap = new TreeMap<>();
// 定義針對每個真實伺服器虛擬出來⼏個節點
int virtaulCount = 3;
for (String tomcatServer : tomcatServers) {
// 求出每⼀個ip的hash值,對應到hash環上,儲存hash值與ip的對應關係
int serverHash = Math.abs(tomcatServer.hashCode());
// 儲存hash值與ip的對應關係
hashServerMap.put(serverHash, tomcatServer);
// 處理虛擬節點
for (int i = 0; i < virtaulCount; i++) {
int virtualHash = Math.abs((tomcatServer + "#" + i).hashCode());
hashServerMap.put(virtualHash, "----由虛擬節點" + i + "對映過來的請求:" + tomcatServer);
}
}
//step2 針對客戶端IP求出hash值
// 定義客戶端IP
String[] clients = new String[]{"10.78.12.3", "113.25.63.1", "126.12.3.8"};
for (String client : clients) {
int clientHash = Math.abs(client.hashCode());
//step3 針對客戶端,找到能夠處理當前客戶端請求的伺服器(雜湊環上順時針最近)
// 根據客戶端ip的雜湊值去找出哪⼀個伺服器節點能夠處理()
SortedMap<Integer, String> integerStringSortedMap =
hashServerMap.tailMap(clientHash);
if (integerStringSortedMap.isEmpty()) {
// 取雜湊環上的順時針第⼀臺伺服器
Integer firstKey = hashServerMap.firstKey();
System.out.println("==========>>>>客戶端:" + client + " 被路由到伺服器:" + hashServerMap.get(firstKey));
} else {
Integer firstKey = integerStringSortedMap.firstKey();
System.out.println("==========>>>>客戶端:" + client + " 被路由到伺服器:" + hashServerMap.get(firstKey));
}
}
}
}
Nginx配置一致性hash負載均衡策略
ngx_http_upstream_consistent_hash 模組是一個負載均衡器,使用一個內部一致性hash演算法來選擇
合適的後端節點。
該模組可以根據配置引數採取不同的方式將請求均勻對映到後端機器,
consistent_hash $remote_addr:可以根據客戶端ip對映
consistent_hash $request_uri:根據客戶端請求的uri對映
consistent_hash $args:根據客戶端攜帶的引數進行映
ngx_http_upstream_consistent_hash 模組是一個第三方模組,需要我們下載安裝後使用
1、github下載nginx一致性hash負載均衡模組 https://github.com/replay/ngx_http_consistent_hash
2、將下載的壓縮包上傳到nginx伺服器,並解壓
3、我們已經編譯安裝過nginx,此時進入當時nginx的原始碼目錄,執行如下命令
./configure —add-module=/root/ngx_http_consistent_hash-master
make
make install
4、在nginx.conf檔案中配置即可
# 配置負載均衡
upstream loginServer {
consistent_hash $request_uri;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}