假如有一個業務快速增長,流量巨大,服務器壓力也隨之增加,直接讀寫數據庫的方案已經不合適了,這時候我們就會想到引入分佈式緩存機制,從而將許多熱點數據放到緩存層,穿透到數據庫層的請求就並不多了。此時,緩存的重要性就不言而喻了。但是,由於緩存數據量很大,緩存的快速查詢又是基於內存高速存取實現,而服務器的內存資源又是十分稀缺的,所以如何讓請求高效命中,分佈式緩存集羣如何優雅的伸縮就變成了亟待解決的問題。下文將會就這一問題的以下兩種解決方案進行探討:
- Naive Hash-based Distributed Dictionary
- Consistent Hashing
1. Naive Hash-based Distributed Dictionary
現在我們先看看 Naive Hash-based Distributed Dictionary 工作方式。假設我們有一個包含 n 個節點的集羣,節點編號是 ,有一個哈希函數 ,節點 k 存儲的鍵值對 。如果選擇的 比較理想,那麼結果會比較均勻的分佈在 。如此一來,便可以將 Distributed Dictionary 均勻地分佈在集羣的各個節點上。
Naive Hash-based Distributed Dictionary 實現非常簡單,但也有嚴重的缺陷。想象一下,此時向集羣添加一個節點,那麼 將會變成 ,所以幾乎每個鍵值對結果都會在集羣中重新分配。同時 的數據會被移到新節點,幾乎是全部。這種操作是非常慢的,而且開銷也很大。如果重分配是在任務運行中發生,這可能會帶來不便。同理,當集羣中剔除某個節點時,也會有類似的情況發生。
2. Consistent Hashing
Consistent Hashing 和 Naive Hashing 一樣,會把數據均勻地分佈在集羣的節點上。但與 Naive Hashing 不同的是,Consistent Hashing 只需要移動相對較少的數據,例如向集羣中添加一臺機器,只需要移動該機器上的數據,其他所有數據都留在原來的位置。Consistent Hashing 有以下特性:
- 平衡性(Balance):是指將哈希值儘可能均勻地分佈到各個緩存區,平衡性是標準哈希函數的優勢。
- 單調性(Monotonicity):是指如果哈希值都分佈到各個緩存區之後,又有新的緩存區加入到分佈式緩存中時,哈希值會從一箇舊的緩存區移到新的緩存區,但不會從一箇舊的緩存區移到另一箇舊的緩存區。換言之,當一組可用的緩存區發生更改時,只有在必要時才移動哈希值,以此保持均勻的分佈。例如,上一節的取模的線性哈希函數就不滿足單調性。
- 傳播性(Spread):表示在客戶端上,哈希值被分佈到不同緩存區的總數很小。在分佈式系統中,由於有些客戶端可能看不到所有的緩存區,當有些客戶端將哈希值分佈到緩存區時,可能造成不一致。傳播性就行應對這種不一致現象的,顯而易見,一個優秀的一致性哈希函數應該具有較低的傳播性。
- 負載性(Load):表示對於一個特定的緩存區,分佈在它上的不同哈希值應該很少。因爲客戶端可能同一個哈希值分佈到不同緩存區,同理對於同一個緩存區也可能被不同客戶端分佈不同的哈希值。負載性和傳播性類似,只不過傳播性是從哈希值的角度看,而負載性是從緩存區的角度討論。因此,比較好的一致性哈希函數應該具有較低的負載性。
- 平滑性(Smoothness):將緩存區添加到緩存集或從其中刪除時,必須移動到新緩存中的對象的預期比例是維持緩存之間平衡負載所需的最低要求。簡而言之,緩存集中的平滑變化與緩存對象位置的變化應該一致。
2.1 哈希環
由於 Hash 算法的結果一般是 unsigned int,因此 Consistent Hashing 的哈希值空間是 ,並且會將整個哈希值空間組織成一個虛擬的環,即哈希環(Hash Ring),如圖 1 所示。
得到哈希環之後,就可以對緩存服務器的 IP 或主機名等唯一標識進行哈希運算定位到在哈希環中的位置。在圖 2 中的哈希環中有三臺服務器。
爲了使服務器和數據的哈希值在同一個哈希值空間,就需要對數據使用相同的哈希函數計算出哈希值,從而得到數據在哈希環上的位置。然後,從此位置沿順時針方向出發,遇到的第一臺服務器就是其對應的服務器。根據這種計算邏輯,可以找到在圖 3 中數據 Object 和緩存服務器的對應關係:Object1 → Cache B,Object2 → Cache C,Object 3 → Cache A,Object 4 → Cache A。
2.2 增刪服務器
在分佈式環境中,不可避免地會由於某些原因出現增刪服務器的情況。當增加服務器的時候,對於一致性哈希,只會影響從新增服務器所在哈希環位置開始,沿逆時針方向遇到第一臺服務器的位置之間的數據,而這些數據會映射到新增的服務器。如圖 4 哈希環中新增服務器 Cache D,原本映射到 Cache A 的數據 Object 3 會映射到 Cache D,而其他數據包括 Object 4 的映射關係不會改變。
同理,刪除服務器(如服務器宕機等)時,根據一致性哈希算法,也只會影響從所刪除服務器的位置開始,沿逆時針方向遇到的第一臺服務的位置之間的數據,同時這些數據會映射到沿順時針方向遇到的第一臺服務器。在圖 5 中,服務器 Cache B 由於某些原因下線了,此時數據 Object 1 會重新映射到服務器 Cache C,而其他數據的映射關係不變。
2.3 虛擬節點
一致性哈希算法在服務器比較少的時候,會出現分佈不均勻的熱點(Hot Spotting)現象,從而造成數據傾斜的問題。圖 6 中有三臺服務器,但是卻有 4 份數據映射到了服務器 Cache A,而服務器 Cache B 和 Cache C 都只映射到了一份數據,造成 Cache A 出現了熱點問題。
爲了解決熱點問題,一致性哈希算法引入了虛擬節點(Virtual Nodes)。虛擬節點是哈希環中服務器的副本,每個物理的服務器會對應於環中一個或多個虛擬節點。當添加緩存服務器時,會在哈希環中爲它創建了一些虛擬節點;當緩存服務器被移除時,會移除哈希環中其對應所有的虛擬節點。如圖 7 中,物理服務器和虛擬節點的對應關係如下: Cache A → Cache A#1 和 Cache A#2,Cache B → Cache B#1 和 Cache B#2、Cache C → Cache C#1 和 Cache C#2。
下面給出一致性哈希算法的 Java 簡單實現:
public class ConsistentHashing {
private static final String SEPARATOR = "#";
private final int replicaFactor;
private final HashFunction hashFunction;
private TreeMap<Long, String> hashCircle = new TreeMap<>();
public ConsistentHashing(int replicaFactor, HashFunction hashFunction) {
this.replicaFactor = replicaFactor;
this.hashFunction = hashFunction;
}
public String get(String node) {
if (hashCircle.isEmpty()) {
return null;
}
Long key = hashFunction.hash(node);
System.out.println("key=>" + key);
SortedMap<Long, String> virtualNodes = hashCircle.tailMap(key);
return virtualNodes.isEmpty()
? hashCircle.get(hashCircle.firstKey())
: hashCircle.get(virtualNodes.firstKey());
}
public void add(String node) {
for (int i = 0; i < replicaFactor; i++) {
hashCircle.put(hashFunction.hash(node + SEPARATOR + i), node + SEPARATOR + i);
}
}
public void remove(String node) {
for (int i = 0; i < replicaFactor; i++) {
hashCircle.remove(hashFunction.hash(node + SEPARATOR + i));
}
}
private interface HashFunction {
Long hash(String key);
}
private static class FVNHashFunction implements HashFunction {
@Override
public Long hash(String key) {
final int p = 16777619;
Long hash = 2166136261L;
for (int i = 0, length = key.length(); i < length; i++) {
hash = (hash ^ key.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return hash < 0 ? -hash : hash;
}
}
}
3. 總結
使用一致性哈希算法時,當發生增刪服務器的時候,並不能徹底杜絕數據遷移,但是卻可以有效地避免全量數據遷移。同時,一致性哈希算法使用虛擬節點可以解決熱點問題。
掃碼關注公衆號:冰山烈焰的黑板報