1. 程式人生 > >Consistent Hashing一致性雜湊演算法

Consistent Hashing一致性雜湊演算法

一、簡單介紹一致性雜湊演算法
分散式儲存中,常常涉及到負載均衡問題,由於有多個數據儲存伺服器。因此當一個物件被儲存時候,它究竟應該存放到哪個資料儲存伺服器上面呢?這就是負載均問題。         又例如:現在假設有一個網站,最近發現隨著流量增加,伺服器壓力越來越大,之前直接讀寫數據庫的方式已經不能滿足使用者的訪問,於是想引入 Memcached 作為快取機制。現在一共引入三臺機器可以作為 Memcached 伺服器,那麼究竟快取到哪臺伺服器上面呢? 可以用簡單雜湊計算:h = Hash(object key) % 3 ,其中 Hash 是一個從字串到正整數的雜湊對映函式,這樣能夠保證對相同 key 的訪問會被髮送到相同的伺服器。現在如果我們將
Memcached Server 分別編號為 0、1、2,那麼就可以根據上式和 key 計算出伺服器編號 h,然後去訪問。但是,由於這樣做只是採用了簡單的求模運算,使得簡單雜湊計算存在很多不足: 1)增刪節點時,更新效率低。當系統中儲存節點數量發生增加或減少時,對映公式將發生變化為 Hash(object key)%(N±1),這將使得所有 object 的對映位置發生變化,整個系統數據物件的對映位置都需要重新進行計算,系統無法對外界訪問進行正常響應,將導致系統處於崩潰狀態。 2)平衡性差,未考慮節點效能差異。由於硬體效能的提升,新新增的節點具有更好的承載能力,如何對演算法進行改進,使節點效能可以得到較好利用,也是亟待解決的一個問題。
3)單調性不足。衡量資料分佈技術的一項重要指標是單調性,單調性是指如果已經有一些內容通過雜湊計算分派到了相應的緩衝中,當又有新的緩衝加入到系統中時,雜湊的結果應能夠保證原有已分配的內容可以被對映到新的緩衝中去,而不會被對映到舊的緩衝集合中的其他緩衝區。 由上述分析可知,簡單地採用模運算來計算 object 的 Hash 值的演算法顯得過於簡單,存在節點衝突,且難以滿足單調性要求。         於是,我們引入一致性雜湊演算法:Consistent Hashing, 是一種 hash 演算法,簡單地說,在移除/新增一個伺服器時,它能夠儘可能小地改變已存在的 key 對映關係,儘可能地滿足單調性的要求。下面就按照 5 個
步驟簡單講講 Consistent Hashing 演算法的基本原理。 步驟一:環形 hash 空間 考慮通常的 hash 演算法都是將 value 對映到一個 32 位的 key 值,即 0~232-1 的數值空間。我們可以將這個空間想象成一個首( 0 )尾(232-1)相接的圓環,如下圖所示。 circle space
步驟二:把物件對映到 hash 空間 接下來考慮 4 個物件 object1~object4,通過 hash 函式計算出的 hash 值 key 在環上的分布如下圖所示。 object 步驟三:把伺服器對映到 hash 空間 Consistent Hashing 的基本思想就是將物件和伺服器都對映到同一環形的hash 數值空間區域(並存在這個區間內部)並且使用相同的 hash 演算法。 假設當前有 A、B 和 C 共 3 臺伺服器(如下圖中藍色圓圈所示的Cache A,Cache B,Cache C),那麼其對映結果將如圖所示,它們在hash空間中,以對應的 hash 值排列。 cache 步驟四:把物件對映到伺服器(這一步是核心,實現物件和伺服器的對映關係) 現在 cache 和物件都已經通過同一個 hash 演算法對映到環形 hash數值空間中了,接下來要考慮的就是如何將物件對映到 cache 上面。在這個環形空間中,如果沿著順時針方向從物件的 key 值出發,直到遇見一個伺服器,那麼就將該物件儲存在這個伺服器上。因為物件和伺服器的 hash 值是固定的,因此這個服務器必然是唯一和確定的。這樣找到了物件和伺服器的對映方法。那麼根據上面的方法,物件 object1 將被儲存到服務器 A 上,object2 和 object3 對應到伺服器 C,object4 對應到伺服器 B。如上圖所示,帶箭頭的虛線表示對映關係。 步驟五:考察伺服器的變動 前面講過,通過 hash 演算法然後求餘的方法帶來的最大問題就在於不能滿足單調性,當伺服器有所變動時,伺服器會失效,進而對後臺伺服器造成巨大的衝擊,現在就來分析Consistent Hashing 演算法的單調性。 (1) 移除伺服器。 考慮假設伺服器 B 掛掉了,根據上面講到的對映方法,這時受影響的將只是那些沿cache B 逆時針遍歷直到下一個伺服器(伺服器 C )之間的物件,也即是本來對映到伺服器 B上的那些物件。 因此這裡僅需要變動物件 object4,將其重新對映到伺服器 C 上即可,如下圖所示。 remove (2) 新增伺服器。 再考慮新增一臺新的伺服器 D 的情況,假設在這個環形 hash 空間中,伺服器 D 被映射在物件 object2 和 object3 之間。這時受影響的僅是那些沿 cache D 逆時針遍歷直到下一個伺服器(伺服器 B )之間的物件,將這些物件重新對映到伺服器 D 上即可。 因此這裡僅需要變動物件 object2,將其重新對映到伺服器 D 上,如下圖所示。 add 考量 hash 演算法的另一個指標是平衡性(Balance),定義:平衡性是指雜湊的結果能夠儘可能分佈到所有的緩衝中,這樣可以使所有的緩衝空間都得到利用。hash 演算法並不能保證絕對的平衡,如果伺服器較少,物件並不能被均勻地對映到服務器上,比如在上面的例子中,僅部署伺服器 A 和伺服器 C 的情況下,在 4 個物件中, 服務器 A 僅儲存了 object1,而伺服器 C 則儲存了 object2、object3 和 object4,分佈是很不均衡的。 remove 為了解決這種情況,Consistent Hashing 引入了“虛擬節點”的概念,它可以如下定義:“虛擬節點”( virtual node )是實際節點在 hash 空間的複製品,一個實際節點對應若干個“虛擬節點”,這個對應個數也稱為“複製個數”,“虛擬節點”在 hash 空間中以 hash 值排列。仍以僅部署伺服器 A 和 伺服器 C 的情況為例。現在我們引入虛擬節點,並設定“複製個數”為 2,這就意味著一共會存在4個“虛擬節點”,伺服器 A1 和伺服器 A2 代表伺服器 A;伺服器 C1 和伺服器 C2 代表服務器 C,假設一種比較理想的情況如下圖所示。 此時,物件到“虛擬節點”的對映關係為:objec1->伺服器 A2;objec2->伺服器 A1;objec3->伺服器 C1;objec4->伺服器 C2。因此物件 object1 和 object2 都被對映到伺服器 A 上,而 object3 和 object4 對映到伺服器 C 上,平衡性有了很大提高引入“虛擬節點”後,對映關係就從 { 物件 -> 節點 } 轉換到了 { 物件 -> 虛擬節點 ->節點} 。 自此,一致性雜湊演算法的主要精髓就介紹完畢了,要想理解該演算法,必然得親自動手實現一個才能掌握。下面我們來實現一下Consistent Hashing演算法。 二、實現一個簡單的一致性雜湊演算法 1、在寫Consistent Hashing演算法之前,我們要先寫一個物件和伺服器共用的雜湊函式,該雜湊函式可以使物件和伺服器都對映到同一hash數值空間區域。該hash演算法我們用MD5壓縮演算法來實現。程式碼如下,關鍵地方參考註釋:
package webspider.consistenthash;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * hash函式,根據key生成hash
 * 
 * @author typ
 * 
 */
public class HashFunction {
	/**
	 * 用MD5壓縮演算法,生成hashmap的key值
	 * 
	 * @param source
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public String hash(String key) {
		String s = null;

		MessageDigest md;
		try {
			md = MessageDigest.getInstance("MD5");
			md.update(key.getBytes());
			// MD5的結果:128位的長整數,存放到tmp中
			byte tmp[] = md.digest();
			s = toHex(tmp);

		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return s;
	}

	/**
	 * 將二進位制的長整數轉換為16進位制的數字,以字串表示
	 * 
	 * @param bytes
	 * @return
	 */
	public String toHex(byte[] bytes) {
		// hexDigits的用處是:將任意位元組轉換為十六進位制的數字
		char hexDigits[] = "0123456789abcdef".toCharArray();
		// MD5的結果:128位的長整數,用位元組表示就是16個位元組,用十六進位制表示的話,使用兩個字元,所以表示成十六進位制需要32個字元
		char str[] = new char[16 * 2];
		int k = 0;
		for (int i = 0; i < 16; i++) {
			byte b = bytes[i];
			// 邏輯右移4位,與0xf(00001111)相與,為高四位的值,然後再hexDigits陣列中找到對應的16進位制值
			str[k++] = hexDigits[b >>> 4 & 0xf];
			// 與0xf(00001111)相與,為低四位的值,然後再hexDigits陣列中找到對應的16進位制值
			str[k++] = hexDigits[b & 0xf];

		}
		String s = new String(str);
		return s;
	}
}


輸出結果如下: 物件 google 存放於伺服器: 192.0.0.1 物件 163 存放於伺服器: 192.0.0.3 物件 baidu 存放於伺服器: 192.0.0.5 物件 sina 存放於伺服器: 192.0.0.4