爲方便自己後期自己查看,把網上Hash映射做了整理:
那節點是怎樣放入這個環中的呢?
//對所有節點,生成nCopies個虛擬結點
for(Node node : nodes) {
//每四個虛擬結點爲一組,爲什麼這樣?下面會說到
for(int i=0; i<nCopies / 4; i++) {
//getKeyForNode方法爲這組虛擬結點得到惟一名稱
byte[] digest=HashAlgorithm.computeMd5(getKeyForNode(node, i));
/** Md5是一個16字節長度的數組,將16字節的數組每四個字節一組,
分別對應一個虛擬結點,這就是爲什麼上面把虛擬結點四個劃分一組的原因*/
for(int h=0;h<4;h++) {
//對於每四個字節,組成一個long值數值,做爲這個虛擬節點的在環中的惟一key
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);
allNodes.put(k, node);
}
}
}
上面的流程大概可以這樣歸納:四個虛擬結點爲一組,以getKeyForNode方法得到這組虛擬節點的name,Md5編碼後,每個虛擬結點對應Md5碼16個字節中的4個,組成一個long型數值,做爲這個虛擬結點在環中的惟一key。第12行k爲什麼是Long型的呢?呵呵,就是因爲Long型實現了Comparator接口。
處理完正式結點在環上的分佈後,可以開始key在環上尋找節點的遊戲了。
對於每個key還是得完成上面的步驟:計算出Md5,根據Md5的字節數組,通過Kemata Hash算法得到key在這個環中的位置。
引文中已詳細描述過這種取節點邏輯:在環上順時針查找,如果找到某個節點,就返回那個節點;如果沒有找到,則取整個環的第一個節點。
上文爲簡單的節點如何放到環上,和節點獲取邏輯講解,下文爲某作者實際用例講解:
http://blog.chinaunix.net/uid-20498361-id-4303232.html
Ketama的Hash算法,以虛擬節點的思想,解決Memcached的分佈式問題。
寫完memcached集羣輕客戶端有一段時間了,使用了ketama的第3方一致性hash算法庫。這裏分析一下它的實現。
1,簡介
若我們在後臺使用NoSQL集羣,必然會涉及到key的分配問題,集羣中某臺機器宕機時如何key又該如何分配的問題。
若我們用一種簡單的方法,n = hash( key)%N來選擇n號服務器,一切都運行正常,若再考慮如下的兩種情況;
(1) 一個 cache 服務器 m down 掉了(在實際應用中必須要考慮這種情況),這樣所有映射到 cache m 的對象都會失效,怎麼辦,需要把 cache m 從 cache 中移除,這時候 cache 是 N-1 臺,映射公式變成了 hash(object)%(N-1) ;
(2) 由於訪問加重,需要添加 cache ,這時候 cache 是 N+1 臺,映射公式變成了 hash(object)%(N+1) ;
1 和 2 意味着什麼?這意味着突然之間幾乎所有的 cache 都失效了。對於服務器而言,這是一場災難,洪水般的訪問都會直接衝向後臺服務器;
(3) 再來考慮一個問題,由於硬件能力越來越強,你可能想讓後面添加的節點多做點活,顯然上面的 hash 算法也做不到。
以上三個問題,可以用一致性hash算法來解決。關於一致性hash算法的理論網上很多,這裏分析幾種一致性hash算法的實現。
2,ketama實現分析
2.1 實現流程介紹
ketama對一致性hash算法的實現思路是:
(1) 通過配置文件,建立一個服務器列表,其形式如:(1.1.1.1:11211, 2.2.2.2:11211,9.8.7.6:11211...)
(2) 對每個服務器列表中的字符串,通過Hash算法,hash成幾個無符號型整數。
注意:如何通過hash算法來計算呢?
(3) 把這幾個無符號型整數放到一個環上,這個換被稱爲continuum。(我們可以想象,一個從0到2^32的鐘表)
(4) 可以建立一個數據結構,把每個數和服務器的ip地址對應在一起,這樣,每個服務器就出現在這個環上的這幾個位置上。
注意:這幾個數,不能隨着服務器的增加和刪除而變化,這樣才能保證集羣增加/刪除機器後,以前的那些key都映射到同樣的ip地址上。後面將會詳細說明怎麼做。
(5) 爲了把一個key映射到一個服務器上,先要對key做hash,形成一個無符號型整數un,然後在環continuum上查找大於un的下一個數值。若找到,就把key保存到這臺服務器上。
(6) 若你的hash(key)值超過continuum上的最大整數值,就直接回饒到continuum環的開始位置。
這樣,添加或刪除集羣中的結點,就只會影響一少部分key的分佈。
注意:這裏說的會影響一部分key是相對的。其實影響的key的多少,由該ip地址佔的權重大小決定的。在ketama的配置文件中,需要指定每個ip地址的權重。權重大的在環上佔的點就多。
2.2 源碼分析
在github上下載源碼後,解壓,進入ketama-master/libketama目錄。一致性hash算法的實現是在ketama.c文件中。
在該文件中,還用到了共享內存,這裏不分析這一部分,只分析一致性hash算法的核心實現部分。
2.2.1 數據結構
// 服務器信息,主要記錄服務器的ip地址和權重值
typedef struct
{
char addr[22]; //服務器ip地址
unsigned long memory; // 權重值
} serverinfo;
// 以下數據結構就是continuum環上的結點,換上的每個點其實代表了一個ip地址,該結構把點和ip地址一一對應起來。
// 環上的結點
typedef struct
{
unsigned int point; //在環上的點,數組下標值
char ip[22]; // 對應的ip地址
} mcs;
2.2.2 一致性hash環的創建
該函數是創建continuum的核心函數,它先從配置文件中讀取集羣服務器ip和端口,以及權重信息。創建continuum環,並把這些服務器信息和環上的數組下標對應起來。
// 其中key是爲了訪問共享內存而設定的,在使用時可以把共享內存部分去掉。
static int
ketama_create_continuum( key_t key, char* filename )
{
// 若不使用共享內存,可以不管
if (shm_ids == NULL) {
init_shm_id_tracker();
}
// 共享內存相關,用不着時,可以去掉
if (shm_data == NULL) {
init_shm_data_tracker();
}
int shmid;
int* data; /* Pointer to shmem location */
// 該變量來記錄共從配置文件中共讀取了多少個服務器
unsigned int numservers = 0;
// 該變量是配置文件中所有服務器權重值得總和
unsigned long memory;
// 從配置文件中讀取到的服務器信息,包括ip地址,端口,權重值
serverinfo* slist;
// 從配置文件filename中讀取服務器信息,把服務器總數保存到變量numservers中,把所有服務器的權重值保存到memory中。
slist = read_server_definitions( filename, &numservers, &memory );
/* Check numservers first; if it is zero then there is no error message
* and we need to set one. */
// 以下幾行是檢查配置文件內容是否正確
// 若總服務器數量小於1,錯誤。
if ( numservers < 1 )
{
sprintf( k_error, "No valid server definitions in file %s", filename );
return 0;
}
else if ( slist == 0 ) // 若服務器信息數組爲空,錯誤
{
/* read_server_definitions must've set error message. */
return 0;
}
// 以下代碼開始構建continuum環
/* Continuum will hold one mcs for each point on the circle: */
// 平均每臺服務器要在這個環上布160個點,這個數組的元素個數就是服務器個數*160。
// 具體多少個點,需要根據事情的服務器權重值進行計算得到。
// 爲什麼要選擇160個點呢?主要是通過md5計算出來的是16個整數,把這個整數分成4等分,每份是4位整數。
// 而每進行一次hash計算,我們可以獲得4個點。
mcs continuum[ numservers * 160 ];
unsigned int i, k, cont = 0;
// 遍歷所有服務器開始在環上部點
for( i = 0; i < numservers; i++ )
{
// 計算服務器i在所有服務器權重的佔比
float pct = (float)slist[i].memory / (float)memory;
// 由於計算一次可以得到4個點,所有對每一臺機器來說,總的計算只需要計算40*numservers次。
// 按權重佔比進行劃分,就是以下的計算得到的次數
unsigned int ks = floorf( pct * 40.0 * (float)numservers );
#ifdef DEBUG
int hpct = floorf( pct * 100.0 );
syslog( LOG_INFO, "Server no. %d: %s (mem: %lu = %u%% or %d of %d)\n",
i, slist[i].addr, slist[i].memory, hpct, ks, numservers * 40 );
#endif
// 計算出總次數,每次可以得到4個點
for( k = 0; k < ks; k++ )
{
/* 40 hashes, 4 numbers per hash = 160 points per server */
char ss[30];
unsigned char digest[16];
// 通過計算hash值來得到下標值,該hash值是字符串:"-n",其中的n是通過權重計算出來的該主機應該部點的總數/4。
sprintf( ss, "%s-%d", slist[i].addr, k );
// 計算其字符串的md5值,該值計算出來後是一個unsigned char [16]的數組,也就是可以保存16個字節
ketama_md5_digest( ss, digest );
/* Use successive 4-bytes from hash as numbers for the points on the circle: */
// 通過對16個字節的每組4個字節進行移位,得到一個0到2^32之間的整數,這樣環上的一個結點就準備好了。
int h;
// 共有16個字節,可以處理4次,得到4個點的值
for( h = 0; h < 4; h++ )
{
// 把計算出來的連續4位的數字,進行移位。
// 把第一個數字一道一個整數的最高8位,後面的一次移動次高8位,後面一次補零,這樣就得到了一個32位的整數值。移動後
continuum[cont].point = ( digest[3+h*4] << 24 )
| ( digest[2+h*4] << 16 )
| ( digest[1+h*4] << 8 )
| digest[h*4];
// 複製對應的ip地址到該點上
memcpy( continuum[cont].ip, slist[i].addr, 22 );
cont++;
}
}
}
free( slist );
// 以下代碼對計算出來的環上點的值進行排序,方便進行查找
// 這裏要注意:排序是按照point的值(計算出來的整數值)進行的,也就是說原來的數組下標順序被打亂了。
/* Sorts in ascending order of "point" */
qsort( (void*) &continuum, cont, sizeof( mcs ), (compfn)ketama_compare );
// 到這裏算法的實現就結束了,環上的點(0^32整數範圍內)都已經建立起來,每個點都是0到2^32的一個整數和ip地址的結構。
// 這樣查找的時候,只是需要hash(key),並在環上找到對應的數的位置,取得該節點的ip地址即可。
2.2.3 在環上查找元素
* 計算key的hash值的實現
unsigned int ketama_hashi( char* inString )
{
unsigned char digest[16];
// 對key的值做md5計算,得到一個有16個元素的unsigned char數組
ketama_md5_digest( inString, digest );
// 取數組中的前4個字符,並移位,形成一個整數作爲hash得到的值返回
return (unsigned int)(( digest[3] << 24 )
| ( digest[2] << 16 )
| ( digest[1] << 8 )
| digest[0] );
}
* 在環上查找相應的結點
mcs* ketama_get_server( char* key, ketama_continuum cont )
{
// 計算key的hash值,並保存到變量h中
unsigned int h = ketama_hashi( key );
// 該變量cont->numpoints是總的數組埋點數
int highp = cont->numpoints;
// 數組結點的值
mcs (*mcsarr)[cont->numpoints] = cont->array;
int lowp = 0, midp;
unsigned int midval, midval1;
// divide and conquer array search to find server with next biggest
// point after what this key hashes to
while ( 1 )
{
// 從數組的中間位置開始找
// 注意此時的數組是按照point的值排好序了
midp = (int)( ( lowp+highp ) / 2 );
// 若中間位置等於最大點數,直接繞回到0位置
if ( midp == cont->numpoints )
return &( (*mcsarr)[0] ); // if at the end, roll back to zeroth
// 取的中間位置的point值
midval = (*mcsarr)[midp].point;
// 再取一個值:若中間位置下標爲0,直接返回0,若中間位置的下標不爲0,直接返回上一個結點的point值
midval1 = midp == 0 ? 0 : (*mcsarr)[midp-1].point;
// 把h的值和取的兩個值point值進行比較,若在這兩個point值之間說明h值應該放在較大的那個point值的下標對應的ip地址上
if ( h <= midval && h > midval1 )
return &( (*mcsarr)[midp] );
// 否則繼續2分
if ( midval < h )
lowp = midp + 1;
else
highp = midp - 1;
// 若沒有找到,直接返回0位置的值,這種情況應該很少
if ( lowp > highp )
return &( (*mcsarr)[0] );
}
<strong>}</strong>
2.2.4 添加刪除機器時會怎樣
先說明一下刪除機器的情況。機器m1被刪除後,以前分配到m1的key需要重新分配,而且最好是均勻分配到現存的機器上。
我們來看看,ketama是否能夠做到?
當m1機器宕機後,continuum環需要重構,需要把m1的ip對應的點從continuum環中去掉。
我們來回顧一下環的創建過程:
按每個ip平均160個點,可以計算出總數t。按每個ip的權重值佔比和總數t的乘積得到該ip應該在該環上部的點數。若一臺機器宕機,那麼每臺機器的權重佔比增加,在該環上部的點數也就相應的增加,當然這個增加也是按每臺機器的佔比來的,佔比多的增加的點數就多,佔比少的增加的點數就少。但,每個ip的點數一定是增加的。
創建環上的點值的過程是:
先計算hash值:
for( k = 0; k < ks; k++ ) { //其中ks是每個ip地址對應的總點數
...
sprintf( ss, "%s-%d", slist[i].addr, k );
ketama_md5_digest( ss, digest );
...
}
循環移位hash值:
continuum[cont].point = ( digest[3+h*4] << 24 )
| ( digest[2+h*4] << 16 )
| ( digest[1+h*4] << 8 )
| digest[h*4];
由於此時每個ip的佔比增加,ks就增加了:
float pct = (float)slist[i].memory / (float)memory; // 此時這個值增加
unsigned int ks = floorf( pct * 40.0 * (float)numservers ); //該值也增加
這樣,每個ip地址對應的point值就多了,但以前的point值不會變。依然在這個環上相同的點值上。也就是說把影響平均分攤到現有的各臺機器上。
當然,刪除的情況和添加的情況相似,都是把影響平均分攤到現有的各個機器上了。
小結:
(1) 環上的點是通過對ip地址加一個整數(形如:-N)作爲一個字符串做hash,然後移位得到4個點數。
(2) 排序後,通過2分查找進行查詢,效率較高。
(3) 這樣,添加ip時,環上以前部的點不會變化,而且把影響分攤到現有的各個ip上。
問題:
這裏我也對該算法提出了兩點疑問,
問題1:創建環和在環上查找,都是使用的hash值4位取數的辦法,那麼是否存在查找某個key時,計算的值在環上不存在?當然這裏也做了處理(找不到直接返回0號位置的ip地址: return &( (*mcsarr)[0] );),但若這種情況比較多時,誤差可能比較大。
通過測試發現,這種情況出現的概率並不大,幾乎沒有。
問題2:其實當ip地址有變動時,還是又可能使原來的key對應的ip地址有變化,只是這種情況概率比較小?那麼能不能使得原來的key對應的ip地址不變化?還有待改進。