原文
有關一致性哈希算法原理及其應用討論的文章已經足夠多,如果對一致性哈希算法一點概念都沒有的同學可以先參考這篇文章:一致性哈希。
相對來說,一致性哈希算法的原理還是比較容易理解的,但在日常開發過程中發現雖然大部分同事對一致性哈希算法的原理有個大概的認識,然而能知道該算法具體實現的人卻寥寥無幾。當然一致性哈希算法的實現不同語言有不同的實現方式,其中較為有名的一種實現叫Ketama算法,該算法最初是由Last.fm的程序員實現的并得到了廣泛的應用,一些開源框架譬如spymemcached,twemproxy等都內置了該算法的實現。
本文主要從spymemcached的源碼出發,分析Ketama算法的具體實現。
在類KetamaNodeLocator.java里有個setKetamaNodes()方法負責一致性哈希環的初始化工作, 代碼如下:
protected void setKetamaNodes(List<MemcachedNode> nodes) {
TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
int numReps = config.getNodeRepetitions();
for (MemcachedNode node : nodes) {
// Ketama does some special work with md5 where it reuses chunks.
if (hashAlg == DefaultHashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
byte[] digest = DefaultHashAlgorithm.computeMd5(config.getKeyForNode(node, i));
for (int h = 0; h < 4; h++) {
Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)
| ((long) (digest[2 + h * 4] & 0xFF) << 16)
| ((long) (digest[1 + h * 4] & 0xFF) << 8)
| (digest[h * 4] & 0xFF);
newNodeMap.put(k, node);
getLogger().debug("Adding node %s in position %d", node, k);
}
}
} else {
for (int i = 0; i < numReps; i++) {
newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
}
}
}
assert newNodeMap.size() == numReps * nodes.size();
ketamaNodes = newNodeMap;
}
下面我們來具體分析下setKetamaNodes函數的實現,首先MemcachedNode這個類對Memcached節點的網絡連接參數及方法進行了封裝。TreeMap在這里用于模擬一致性哈希環的環狀結構。
int numReps = config.getNodeRepetitions();
getNodeRepetitions()方法負責讀取配置信息,返回一個真實的Memcached節點對應的虛擬節點數,默認情況下返回160,也就是說一個Memcached節點在一致性哈希環上對應有160個虛擬節點。
config.getKeyForNode(node, i)
getKeyForNode()根據傳進去的MemcacheNode對象和變量i生成key值,返回值示例:“127.0.0.1:11311-0”
computeMd5()根據key生成16位的MD5摘要, 因此digest數組共16位:
byte[] digest = DefaultHashAlgorithm.computeMd5(config.getKeyForNode(node, i));
將digest數組按每四位一組,通過位操作產生一個最大32位的長整數。之所以是32位是因為一致性哈希環取值范圍為0~2^32; 回到上面的例子,對于一個Memcached節點譬如“127.0.0.1:11311”, 將通過for循環產生“127.0.0.1:11311-0”,“127.0.0.1:11311-1”... “127.0.0.1:11311-39”共40個副本,對于每個副本譬如“127.0.0.1:11311-0”, 將會產生4個長整數,對應一致性哈希環上的4個位置,所以默認配置的情況下,一個Memcached節點將在一致性哈希環上占據4×40=160個位置。
Long k = ((long) (digest[3 + h * 4] & 0xFF) << 24)
| ((long) (digest[2 + h * 4] & 0xFF) << 16)
| ((long) (digest[1 + h * 4] & 0xFF) << 8)
| (digest[h * 4] & 0xFF);
以k為key將MemcacheNode對象放到TreeMap里:
newNodeMap.put(k, node);
由于TreeMap中的value是按Key排序的,因此可以通過TreeMap來模擬一致性哈希的環狀結構,k值小的排在前,k值大的排在后。
以上就是一致性哈希環初始化過程的的基本分析,下面我們來看看查詢的過程, getPrimary()函數傳入一個key,譬如"123", 先計算出該key的哈希值。
public MemcachedNode getPrimary(final String k) {
MemcachedNode rv = getNodeForKey(hashAlg.hash(k));
assert rv != null : "Found no node for key " + k;
return rv;
}
MemcachedNode getNodeForKey(long hash) {
final MemcachedNode rv;
if (!ketamaNodes.containsKey(hash)) {
// Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5
// in a lot of places, so I'm doing this myself.
SortedMap<Long, MemcachedNode> tailMap = getKetamaNodes().tailMap(hash);
if (tailMap.isEmpty()) {
hash = getKetamaNodes().firstKey();
} else {
hash = tailMap.firstKey();
}
}
rv = getKetamaNodes().get(hash);
return rv;
}
重點在于下面這句, TreeMap的tailMap()方法會返回一個SortedMap對象tailMap, tailMap中包含的所有key值都比傳參hash大,這個操作相當于給定一個hash值,從一致性哈希環中按順時針順序查找節點,直到查找到第一個key值比傳參hash大的節點,該節點就是該hash值所對應的Memcached節點。
SortedMap<Long, MemcachedNode> tailMap = getKetamaNodes().tailMap(hash);
以上就是對Sypmencached源碼中Ketama算法的實現分析。