一致性哈希算法的原理与实现
分布式系統(tǒng)中對象與節(jié)點的映射關(guān)系,傳統(tǒng)方案是使用對象的哈希值,對節(jié)點個數(shù)取模,再映射到相應編號的節(jié)點,這種方案在節(jié)點個數(shù)變動時,絕大多數(shù)對象的映射關(guān)系會失效而需要遷移;而一致性哈希算法中,當節(jié)點個數(shù)變動時,映射關(guān)系失效的對象非常少,遷移成本也非常小。本文總結(jié)了一致性哈希的算法原理和Java實現(xiàn),并列舉了其應用。
作者:王克鋒
出處:https://kefeng.wang/2018/08/10/consistent-hashing/
版權(quán):自由轉(zhuǎn)載-非商用-非衍生-保持署名,轉(zhuǎn)載請標明作者和出處。
1 概述
1.1 傳統(tǒng)哈希(硬哈希)
分布式系統(tǒng)中,假設有 n 個節(jié)點,傳統(tǒng)方案使用 mod(key, n) 映射數(shù)據(jù)和節(jié)點。
當擴容或縮容時(哪怕只是增減1個節(jié)點),映射關(guān)系變?yōu)?mod(key, n+1) / mod(key, n-1),絕大多數(shù)數(shù)據(jù)的映射關(guān)系都會失效。
1.2 一致性哈希(Consistent Hashing)
1997年,麻省理工學院(MIT)的 David Karger 等6個人發(fā)布學術(shù)論文《Consistent hashing and random trees: distributed caching protocols for relieving hot spots on the World Wide Web(一致性哈希和隨機樹:用于緩解萬維網(wǎng)上熱點的分布式緩存協(xié)議)》,對于 K 個關(guān)鍵字和 n 個槽位(分布式系統(tǒng)中的節(jié)點)的哈希表,增減槽位后,平均只需對 K/n 個關(guān)鍵字重新映射。
1.3 哈希指標
評估一個哈希算法的優(yōu)劣,有如下指標,而一致性哈希全部滿足:
- 均衡性(Balance):將關(guān)鍵字的哈希地址均勻地分布在地址空間中,使地址空間得到充分利用,這是設計哈希的一個基本特性。
- 單調(diào)性(Monotonicity): 單調(diào)性是指當?shù)刂房臻g增大時,通過哈希函數(shù)所得到的關(guān)鍵字的哈希地址也能映射的新的地址空間,而不是僅限于原先的地址空間。或等地址空間減少時,也是只能映射到有效的地址空間中。簡單的哈希函數(shù)往往不能滿足此性質(zhì)。
- 分散性(Spread): 哈希經(jīng)常用在分布式環(huán)境中,終端用戶通過哈希函數(shù)將自己的內(nèi)容存到不同的緩沖區(qū)。此時,終端有可能看不到所有的緩沖,而是只能看到其中的一部分。當終端希望通過哈希過程將內(nèi)容映射到緩沖上時,由于不同終端所見的緩沖范圍有可能不同,從而導致哈希的結(jié)果不一致,最終的結(jié)果是相同的內(nèi)容被不同的終端映射到不同的緩沖區(qū)中。這種情況顯然是應該避免的,因為它導致相同內(nèi)容被存儲到不同緩沖中去,降低了系統(tǒng)存儲的效率。分散性的定義就是上述情況發(fā)生的嚴重程度。好的哈希算法應能夠盡量避免不一致的情況發(fā)生,也就是盡量降低分散性。
- 負載(Load): 負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內(nèi)容映射到不同的緩沖區(qū)中,那么對于一個特定的緩沖區(qū)而言,也可能被不同的用戶映射為不同的內(nèi)容。與分散性一樣,這種情況也是應當避免的,因此好的哈希算法應能夠盡量降低緩沖的負荷。
1.4 資料鏈接
原始論文《Consistent Hashing and Random Trees》鏈接如下:
- 官方鏈接 - PDF 版本
- 本站副本 - PDF 版本
相關(guān)論文《Web Caching with Consistent Hashing》鏈接如下:
- 官方鏈接 - PDF 版本
- 官方鏈接 - HTM 版本
- 本站副本 - PDF 版本
更多資料:
WikiPedia - Consistent hashing
codeproject - Consistent hashing
2 算法原理
2.1 映射方案
2.1.1 公用哈希函數(shù)和哈希環(huán)
設計哈希函數(shù) Hash(key),要求取值范圍為 [0, 2^32)
各哈希值在上圖 Hash 環(huán)上的分布:時鐘12點位置為0,按順時針方向遞增,臨近12點的左側(cè)位置為2^32-1。
2.1.2 節(jié)點(Node)映射至哈希環(huán)
如圖哈希環(huán)上的綠球所示,四個節(jié)點 Node A/B/C/D,
其 IP 地址或機器名,經(jīng)過同一個 Hash() 計算的結(jié)果,映射到哈希環(huán)上。
2.1.3 對象(Object)映射于哈希環(huán)
如圖哈希環(huán)上的黃球所示,四個對象 Object A/B/C/D,
其鍵值,經(jīng)過同一個 Hash() 計算的結(jié)果,映射到哈希環(huán)上。
2.1.4 對象(Object)映射至節(jié)點(Node)
在對象和節(jié)點都映射至同一個哈希環(huán)之后,要確定某個對象映射至哪個節(jié)點,
只需從該對象開始,沿著哈希環(huán)順時針方向查找,找到的第一個節(jié)點,即是。
可見,Object A/B/C/D 分別映射至 Node A/B/C/D。
2.2 刪除節(jié)點
現(xiàn)實場景:服務器縮容時刪除節(jié)點,或者有節(jié)點宕機。如下圖,要刪除節(jié)點 Node C:
只會影響欲刪除節(jié)點(Node C)與上一個(順時針為前進方向)節(jié)點(Node B)與之間的對象,也就是 Object C,
這些對象的映射關(guān)系,按照 2.1.4 的規(guī)則,調(diào)整映射至欲刪除節(jié)點的下一個節(jié)點 Node D。
其他對象的映射關(guān)系,都無需調(diào)整。
2.3 增加節(jié)點
現(xiàn)實場景:服務器擴容時增加節(jié)點。比如要在 Node B/C 之間增加節(jié)點 Node X:
只會影響欲新增節(jié)點(Node X)與上一個(順時針為前進方向)節(jié)點(Node B)與之間的對象,也就是 Object C,
這些對象的映射關(guān)系,按照 2.1.4 的規(guī)則,調(diào)整映射至新增的節(jié)點 Node X。
其他對象的映射關(guān)系,都無需調(diào)整。
2.4 虛擬節(jié)點
對于前面的方案,節(jié)點數(shù)越少,越容易出現(xiàn)節(jié)點在哈希環(huán)上的分布不均勻,導致各節(jié)點映射的對象數(shù)量嚴重不均衡(數(shù)據(jù)傾斜);相反,節(jié)點數(shù)越多越密集,數(shù)據(jù)在哈希環(huán)上的分布就越均勻。
但實際部署的物理節(jié)點有限,我們可以用有限的物理節(jié)點,虛擬出足夠多的虛擬節(jié)點(Virtual Node),最終達到數(shù)據(jù)在哈希環(huán)上均勻分布的效果:
如下圖,實際只部署了2個節(jié)點 Node A/B,
每個節(jié)點都復制成3倍,結(jié)果看上去是部署了6個節(jié)點。
可以想象,當復制倍數(shù)為 2^32 時,就達到絕對的均勻,通常可取復制倍數(shù)為32或更高。
虛擬節(jié)點哈希值的計算方法調(diào)整為:對“節(jié)點的IP(或機器名)+虛擬節(jié)點的序號(1~N)”作哈希。
3 算法實現(xiàn)
一致性哈希算法有多種具體的實現(xiàn),包括 Chord 算法,KAD 算法等,都比較復雜。
這里給出一個簡易實現(xiàn)及其演示,可以看到一致性哈希的均衡性和單調(diào)性的優(yōu)勢。
單調(diào)性在本例中沒有統(tǒng)計數(shù)據(jù),但根據(jù)前面原理可知,增刪節(jié)點后只有很少量的數(shù)據(jù)需要調(diào)整映射關(guān)系。
3.1 源碼
/*** @author: https://kefeng.wang* @date: 2018-08-10 11:08**/ public class ConsistentHashing {// 物理節(jié)點private Set<String> physicalNodes = new TreeSet<String>() {{add("192.168.1.101");add("192.168.1.102");add("192.168.1.103");add("192.168.1.104");}};//虛擬節(jié)點private final int VIRTUAL_COPIES = 1048576; // 物理節(jié)點至虛擬節(jié)點的復制倍數(shù)private TreeMap<Long, String> virtualNodes = new TreeMap<>(); // 哈希值 => 物理節(jié)點// 32位的 Fowler-Noll-Vo 哈希算法// https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_functionprivate static Long FNVHash(String key) {final int p = 16777619;Long hash = 2166136261L;for (int idx = 0, num = key.length(); idx < num; ++idx) {hash = (hash ^ key.charAt(idx)) * p;}hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;if (hash < 0) {hash = Math.abs(hash);}return hash;}// 根據(jù)物理節(jié)點,構(gòu)建虛擬節(jié)點映射表public ConsistentHashing() {for (String nodeIp : physicalNodes) {addPhysicalNode(nodeIp);}}// 添加物理節(jié)點public void addPhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.put(hash, nodeIp);}}// 刪除物理節(jié)點public void removePhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.remove(hash);}}// 查找對象映射的節(jié)點public String getObjectNode(String object) {long hash = FNVHash(object);SortedMap<Long, String> tailMap = virtualNodes.tailMap(hash); // 所有大于 hash 的節(jié)點Long key = tailMap.isEmpty() ? virtualNodes.firstKey() : tailMap.firstKey();return virtualNodes.get(key);}// 統(tǒng)計對象與節(jié)點的映射關(guān)系public void dumpObjectNodeMap(String label, int objectMin, int objectMax) {// 統(tǒng)計Map<String, Integer> objectNodeMap = new TreeMap<>(); // IP => COUNTfor (int object = objectMin; object <= objectMax; ++object) {String nodeIp = getObjectNode(Integer.toString(object));Integer count = objectNodeMap.get(nodeIp);objectNodeMap.put(nodeIp, (count == null ? 0 : count + 1));}// 打印double totalCount = objectMax - objectMin + 1;System.out.println("======== " + label + " ========");for (Map.Entry<String, Integer> entry : objectNodeMap.entrySet()) {long percent = (int) (100 * entry.getValue() / totalCount);System.out.println("IP=" + entry.getKey() + ": RATE=" + percent + "%");}}public static void main(String[] args) {ConsistentHashing ch = new ConsistentHashing();// 初始情況ch.dumpObjectNodeMap("初始情況", 0, 65536);// 刪除物理節(jié)點ch.removePhysicalNode("192.168.1.103");ch.dumpObjectNodeMap("刪除物理節(jié)點", 0, 65536);// 添加物理節(jié)點ch.addPhysicalNode("192.168.1.108");ch.dumpObjectNodeMap("添加物理節(jié)點", 0, 65536);} }3.2 復制倍數(shù)為 1 時的均衡性
修改代碼中 VIRTUAL_COPIES = 1(相當于沒有虛擬節(jié)點),運行結(jié)果如下(可見各節(jié)點負荷很不均衡):
======== 初始情況 ======== IP=192.168.1.101: RATE=45% IP=192.168.1.102: RATE=3% IP=192.168.1.103: RATE=28% IP=192.168.1.104: RATE=22% ======== 刪除物理節(jié)點 ======== IP=192.168.1.101: RATE=45% IP=192.168.1.102: RATE=3% IP=192.168.1.104: RATE=51% ======== 添加物理節(jié)點 ======== IP=192.168.1.101: RATE=45% IP=192.168.1.102: RATE=3% IP=192.168.1.104: RATE=32% IP=192.168.1.108: RATE=18%3.2 復制倍數(shù)為 32 時的均衡性
修改代碼中 VIRTUAL_COPIES = 32,運行結(jié)果如下(可見各節(jié)點負荷比較均衡):
======== 初始情況 ======== IP=192.168.1.101: RATE=29% IP=192.168.1.102: RATE=21% IP=192.168.1.103: RATE=25% IP=192.168.1.104: RATE=23% ======== 刪除物理節(jié)點 ======== IP=192.168.1.101: RATE=39% IP=192.168.1.102: RATE=37% IP=192.168.1.104: RATE=23% ======== 添加物理節(jié)點 ======== IP=192.168.1.101: RATE=35% IP=192.168.1.102: RATE=20% IP=192.168.1.104: RATE=23% IP=192.168.1.108: RATE=20%3.2 復制倍數(shù)為 1M 時的均衡性
修改代碼中 VIRTUAL_COPIES = 1048576,運行結(jié)果如下(可見各節(jié)點負荷非常均衡):
======== 初始情況 ======== IP=192.168.1.101: RATE=24% IP=192.168.1.102: RATE=24% IP=192.168.1.103: RATE=25% IP=192.168.1.104: RATE=25% ======== 刪除物理節(jié)點 ======== IP=192.168.1.101: RATE=33% IP=192.168.1.102: RATE=33% IP=192.168.1.104: RATE=33% ======== 添加物理節(jié)點 ======== IP=192.168.1.101: RATE=25% IP=192.168.1.102: RATE=24% IP=192.168.1.104: RATE=24% IP=192.168.1.108: RATE=24%4 應用
一致性哈希是分布式系統(tǒng)組件負載均衡的首選算法,它既可以在客戶端實現(xiàn),也可以在中間件上實現(xiàn)。其應用有:
- 分布式散列表(DHT)的設計;
- 分布式關(guān)系數(shù)據(jù)庫(MySQL):分庫分表時,計算數(shù)據(jù)與節(jié)點的映射關(guān)系;
- 分布式緩存:Memcached 的客戶端實現(xiàn)了一致性哈希,還可以使用中間件 twemproxy 管理 redis/memcache 集群;
- RPC 框架 Dubbo:用來選擇服務提供者;
- 亞馬遜的云存儲系統(tǒng) Dynamo;
- 分布式 Web 緩存;
- Bittorrent DHT;
- LVS。
總結(jié)
以上是生活随笔為你收集整理的一致性哈希算法的原理与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TX2刷机和软件安装过程记录
- 下一篇: 思想学习——细节决定成败