基於一致性hash演算法 C++語言的實現詳解

來源:互聯網
上載者:User

一致性hash演算法實現有兩個關鍵問題需要解決,一個是用於結點儲存和尋找的資料結構的選擇,另一個是結點hash演算法的選擇。

首先來談一下一致性hash演算法中用於儲存結點的資料結構。通過瞭解一致性hash的原理,我們知道結點可以想象為是儲存在一個環形的資料結構上(如),結點A、B、C、D按hash值在環形分布上是有序的,也就是說結點可以按hash值儲存在一個有序的隊列裡。如所示,當一個hash值為-2^20的請求點P尋找路由結點時,一致性hash演算法會按hash值的順時針方向路由到第一個結點上(B),也就是相當於要在儲存結點的有序結構中,按查詢的key值找到大於key值中的最小的那個結點。因此,我們應該選擇一種資料結構,它應該高效地支援結點頻繁地增刪,也必須具有理想的查詢效率。那麼,紅/黑樹狀結構可以滿足這些要求。紅/黑樹狀結構是一顆近似平衡的一顆二叉尋找樹,因為操作比如插入、刪除和尋找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅/黑樹狀結構在最壞情況下都是高效的,而不同於普通的二叉尋找樹。 因此,我們選擇使用紅/黑樹狀結構作為結點的儲存結構,除了需要實現紅/黑樹狀結構基本的插入、刪除、尋找的準系統,我們還應該增加另一個查詢lookup函數,用於尋找大於key中最小的結點。

接下來,我們來說hash演算法的選擇。一致性hash演算法最初提出來,就是為瞭解決負載平衡的問題。每個實體結點會包含很多虛擬結點,虛擬結點是平衡負載的關鍵。我們希望虛擬結點可以均衡的散列在整個“環”上,這樣不僅可以負載到不同hash值的路由請求,還可以當某個結點down掉,原來路由到down掉結點的請求也可以較均衡的路由到其他結點而不會對某個結點造成大量的負載請求。這裡,我們選擇使用MD5演算法。通過MD5演算法,可以將一個標示串(用於標示虛擬結點)轉化得到一個16位元組的字元數組,再對該數組進行處理,得到一個整形的hash值。由於MD5具有高度的離散性,所以產生的hash值也會具有很大的離散性,會均衡的散列到“環”上。

筆者用C++語言對一致性hash演算法進行了實現,下面我將會描述下一些關鍵細節。

1、首先定義實體結點類、虛擬結點類。一個實體結點對應多個虛擬結點。

實體結點 CNode_s:

複製代碼 代碼如下:/*實體結點*/
class CNode_s
{
public:
/*建構函式*/
CNode_s();
CNode_s(char * pIden , int pVNodeCount , void * pData);

/*擷取結點標示*/
const char * getIden();

/*擷取實體結點的虛擬結點數量*/
int getVNodeCount();

/*設定實體結點資料值*/
void setData(void * data);

/*擷取實體結點資料值*/
void * getData();
private:
void setCNode_s(char * pIden, int pVNodeCount , void * pData);
char iden[100];/*結點標示串*/
int vNodeCount; /*虛擬結點數目*/
void * data;/*資料結點*/
};

虛擬結點 CVirtualNode_s:虛擬結點有一指標指向實體結點複製代碼 代碼如下:/*虛擬結點*/
class CVirtualNode_s
{
public:
/*建構函式*/
CVirtualNode_s();
CVirtualNode_s(CNode_s * pNode);

/*設定虛擬結點所指向的實體結點*/
void setNode_s(CNode_s * pNode);

/*擷取虛擬結點所指向的實體結點*/
CNode_s * getNode_s();

/*設定虛擬結點hash值*/
void setHash(long pHash);

/*擷取虛擬結點hash值*/
long getHash();
private:
long hash; /*hash值*/
CNode_s * node; /*虛擬結點所指向的實體結點*/
};

2、hash演算法具有可選擇性,定義一個hash演算法介面,方便以後進行其他演算法的擴充。

這裡建立MD5hash類,並繼承該介面,通過MD5演算法求hash值。

類圖:

CHashFun介面:

複製代碼 代碼如下:/*定義Hash函數類介面,用於計算結點的hash值*/

class CHashFun
{
public:
virtual long getHashVal(const char *) = 0;
};

CMD5HashFun 類繼承CHashFun介面,實現擷取hash值的getHashVal函數:複製代碼 代碼如下:/*用MD5演算法計算結點的hash值,繼承CHashFun父類*/
class CMD5HashFun : public CHashFun
{
public:
virtual long getHashVal (const char * );
};

long CMD5HashFun::getHashVal(const char * instr)
{
int i;
long hash = 0;
unsigned char digest[16];

/*調用MD5相關函數,產生instr的MD5碼,存入digest*/
md5_state_t md5state;
md5_init(&md5state);
md5_append(&md5state, (const unsigned char *)instr, strlen(instr));
md5_finish(&md5state, digest);

/* 每四個位元組構成一個32位整數,
將四個32位整數相加得到instr的hash值(可能溢出) */
for(i = 0; i < 4; i++)
{
hash += ((long)(digest[i*4 + 3]&0xFF) << 24)
| ((long)(digest[i*4 + 2]&0xFF) << 16)
| ((long)(digest[i*4 + 1]&0xFF) << 8)
| ((long)(digest[i*4 + 0]&0xFF));
}
return hash;
}

3、擴充紅/黑樹狀結構結構中的尋找函數,用於尋找紅/黑樹狀結構中大於key值中最小的結點。
複製代碼 代碼如下:util_rbtree_node_t* util_rbtree_lookup(util_rbtree_t *rbtree, long key)
{
if((rbtree != NULL) && !util_rbtree_isempty(rbtree))
{
util_rbtree_node_t *node = NULL;
util_rbtree_node_t *temp = rbtree->root;
util_rbtree_node_t *null = _NULL(rbtree);
while(temp != null)
{
if(key <= temp->key)
{
node = temp; /* update node */
temp = temp->left;
}
else if(key > temp->key)
{
temp = temp->right;
}
}
/* if node==NULL return the minimum node */
return ((node != NULL) ? node : util_rbtree_min(rbtree));
}
return NULL;
}

4、建立一致性hash類。使其具有插入、刪除、尋找實體結點的功能。

具體演算法和操作過程已經在代碼注釋中說明。

複製代碼 代碼如下:class CConHash
{
public:
/*建構函式*/
CConHash(CHashFun * pFunc);

/*設定hash函數*/
void setFunc(CHashFun * pFunc);

/*增加實體結點 , 0代表成功 , -1代表失敗*/
int addNode_s(CNode_s * pNode);

/*刪除實體結點 , 0代表成功 , -1代表失敗*/
int delNode_s(CNode_s * pNode);

/*尋找實體結點*/
CNode_s * lookupNode_s(const char * object);

/*擷取一致性hash結構的所有虛擬結點數量*/
int getVNodes();
private:
/*Hash函數*/
CHashFun * func;
/*虛擬結點總個數*/
int vNodes;
/*儲存虛擬結點的紅/黑樹狀結構*/
util_rbtree_t * vnode_tree;
};
/*輔助函數,虛擬結點轉化為紅/黑樹狀結構結點*/
util_rbtree_node_t * vNode2RBNode(CVirtualNode_s * vnode);

CConHash::CConHash(CHashFun * pFunc)
{
/*設定hash函數*/
assert(pFunc!=NULL);
this->func = pFunc;
this->vNodes = 0;
/*初始化紅/黑樹狀結構*/
vnode_tree = new util_rbtree_s();
util_rbtree_init(vnode_tree);
}

int CConHash::addNode_s(CNode_s * pNode)
{
if(pNode==NULL) return -1;
int vCount = pNode->getVNodeCount();
if(vCount<=0) return -1;
CVirtualNode_s * virtualNode ;
util_rbtree_node_t * rbNode;
char str [100];
char num[10];
strcpy(str,pNode->getIden());
long hash = 0;
/*產生虛擬結點並插入到紅/黑樹狀結構中*/
for(int i=0;i<vCount;i++)
{
virtualNode = new CVirtualNode_s(pNode);
/*採用str+“i”的方法產生不同的iden串,用於後面的hash值計算*/
itoa(i,num,10);
strcat(str,num);
hash = func->getHashVal(str);
virtualNode->setHash(hash);
if(!util_rbtree_search(vnode_tree,hash))
{
/*產生紅/黑樹狀結構結點*/
rbNode = vNode2RBNode(virtualNode);
if(rbNode!=NULL)
{
/*將該結點插入到紅/黑樹狀結構中*/
util_rbtree_insert(vnode_tree,rbNode);
this->vNodes++;
}
}
}
return 0;
}

int CConHash::delNode_s(CNode_s * pNode)
{
if(pNode==NULL) return -1;
util_rbtree_node_t * rbNode;
char str [100];
char num [10];
strcpy(str,pNode->getIden());
int vCount = pNode->getVNodeCount();
long hash = 0;
CVirtualNode_s * node = NULL;
/*將該實體結點產生的所有虛擬結點進行刪除*/
for(int i=0;i<vCount;i++)
{
itoa(i,num,10);
strcat(str,num);/*採用該方法產生不同的iden串*/
hash = func->getHashVal(str);
rbNode = util_rbtree_search(vnode_tree,hash);
if(rbNode!=NULL)
{
node = (CVirtualNode_s *) rbNode->data;
if(node->getNode_s()==pNode && node->getHash()==hash)
{
this->vNodes--;
/*將該結點從紅/黑樹狀結構中刪除*/
util_rbtree_delete(vnode_tree,rbNode);
delete rbNode;
delete node;
}
}
}
return 0;
}

CNode_s * CConHash::lookupNode_s(const char * object)
{
if(object==NULL||this->vNodes==0) return NULL;
util_rbtree_node_t * rbNode;
int key = this->func->getHashVal(object);
/*在紅/黑樹狀結構中尋找key值比key大的最小的結點*/
rbNode = util_rbtree_lookup(vnode_tree,key);
if(rbNode!=NULL)
{
return ((CVirtualNode_s *) rbNode->data)->getNode_s();
}
return NULL;
}

int CConHash::getVNodes()
{
return this->vNodes;
}

util_rbtree_node_t * vNode2RBNode(CVirtualNode_s * vnode)
{
if(vnode==NULL) return NULL;
util_rbtree_node_t *rbNode = new util_rbtree_node_t();
rbNode->key = vnode->getHash();
rbNode->data = vnode;
return rbNode;
}

5、建立一個用戶端類,對一致性hash演算法進行測試。

寫了一個getIP的函數,類比隨機產生的IP字串。

複製代碼 代碼如下:#include<iostream>
#include"CNode_s.h"
#include"CVirtualNode_s.h"
#include"CHashFun.h"
#include"CMD5HashFun.h"
#include"CConHash.h"
#include<string.h>
#include<time.h>

using namespace std;

void getIP(char * IP)
{
int a=0, b=0 , c=0 , d=0;
a = rand()%256;
b = rand()%256;
c = rand()%256;
d = rand()%256;
char aa[4],bb[4],cc[4],dd[4];
itoa(a, aa, 10);
itoa(b, bb, 10);
itoa(c, cc, 10);
itoa(d, dd, 10);
strcpy(IP,aa);
strcat(IP,".");
strcat(IP,bb);
strcat(IP,".");
strcat(IP,cc);
strcat(IP,".");
strcat(IP,dd);
}

int main()
{
srand(time(0));
freopen("out.txt","r",stdin);
/*定義hash函數*/
CHashFun * func = new CMD5HashFun();
/*建立一致性hash對象*/
CConHash * conhash = new CConHash(func);

/*定義CNode*/
CNode_s * node1 = new CNode_s("machineA",50,"10.3.0.201");
CNode_s * node2 = new CNode_s("machineB",80,"10.3.0.202");
CNode_s * node3 = new CNode_s("machineC",20,"10.3.0.203");
CNode_s * node4 = new CNode_s("machineD",100,"10.3.0.204");

conhash->addNode_s(node1);
conhash->addNode_s(node2);
conhash->addNode_s(node3);
conhash->addNode_s(node4);

/*動態更改結點資料值*/
// node1->setData("99999999");

int ans1 ,ans2 ,ans3 ,ans4;
ans1=ans2=ans3=ans4=0;

char object[100];
CNode_s * node ;
/*動態刪除結點*/
//conhash->delNode_s(node2);
for(int i =0;i<30;i++)
{
// getIP(object);
// cout<<object<<endl;
cin>>object;
node = conhash->lookupNode_s(object);
if(node!=NULL)
{
cout<<object<<"----->\t"<<node->getIden()<<" \t "<<(char *)node->getData()<<endl;
if(strcmp(node->getIden(),"machineA")==0) ans1++;
if(strcmp(node->getIden(),"machineB")==0) ans2++;
if(strcmp(node->getIden(),"machineC")==0) ans3++;
if(strcmp(node->getIden(),"machineD")==0) ans4++;
}
}

cout<<"Total test cases : "<<ans1+ans2+ans3+ans4<<endl;
cout<<"Map to MachineA : "<<ans1<<endl;
cout<<"Map to MachineB : "<<ans2<<endl;
cout<<"Map to MachineC : "<<ans3<<endl;
cout<<"Map to MachineD : "<<ans4<<endl;
fclose(stdin);
return 0;
}

6、刪除結點對hash路由的影響測試

測試結果:

分析:上面兩幅圖,左邊為原始四個實體結點的路由情況,後面為刪除結點2(Node2)之後的路由情況。不難發現,MachineB down之後,原先的路由請求,較均衡地負載到了其他機器結點,而且對原先路由到其他結點的請求沒有影響。比如139.149.184.125這個請求仍會路由到MachineD,並不會因為結點的減少而造成影響。但是,如果是增加實體結點,可能會造成增加前後路由情況不一致的現象,因為路由區間的更加狹小,但是不會有特別大的影響。 另一方面,可以發現實體結點的虛擬結點個數比例分配情況很大程度影響了結點的負載路由情況,比例大致與虛擬結點個數相一致。

總結:

本文首先通過介紹實現一致性hash演算法的關鍵演算法和資料結構的選擇分析,選擇了紅/黑樹狀結構作為虛擬結點的儲存結構,以及MD5演算法作為Hash函數用於計算結點的hash值。並使用C++語言,對一致性hash演算法進行了實現,實現了一致性hash實體結點的增加、刪除、尋找等準系統,並進行了測試分析。由於筆者水平有限,存在很多有待改進的地方,因此本文僅供大家參考、討論學習。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.