一致性哈希(Consistent Hashing)原理和实现(整理)

前言:对于一致性哈希已经不是罕见概念,在此只是对原有理论概念的一个整理和用自己的理解讲述,希望对新手有些许帮助,利人利己足矣。

1.概念

        一致哈希是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对 K/n 个关键字重新映射,其中 K是关键字的数量,n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。

        一致哈希将每个对象映射到圆环边上的一个点,系统再将可用的节点机器映射到圆环的不同位置。查找某个对象对应的机器时,需要用一致哈希算法计算得到对象对应圆环边上位置,沿着圆环边上查找直到遇到某个节点机器,这台机器即为对象应该保存的位置。当删除一台节点机器时,这台机器上保存的所有对象都要移动到下一台机器。添加一台机器到圆环边上某个点时,这个点的下一台机器需要将这个节点前对应的对象移动到新机器上。更改对象在节点机器上的分布可以通过调整节点机器的位置来实现。

2.应用场景

        在使用N台缓存服务器时,一种常用的负载均衡方式是,对资源Object的请求使用 Hash(Object)%N 来映射到某一台缓存服务器。但是缺点有二:

1.一个服务器(服务器A)Down掉之后,服务器A中的缓存资源都会失效,映射公式变成了Hash(Object)%(N-1), 缓存失效,这会使得缓存服务器大量集中地向原始内容服务器更新缓存。

2.当增加或减少一台缓存服务器时,映射公式变成了Hash(Object)%(N+1),这种方式可能会改变所有资源对应的Hash值,也就是所有的缓存都失效了,这会使得缓存服务器大量集中地向原始内容服务器更新缓存。

        因些,需要一致哈希算法来避免这样的问题。 一致哈希尽可能使同一个资源映射到同一台缓存服务器。这种方式要求增加一台缓存服务器时,新的服务器尽量分担存储其他所有服务器的缓存资源。减少一台缓存服务器时,其他所有服务器也可以尽量分担存储它的缓存资源。一致哈希算法的主要思想是将每个缓存服务器与一个或多个哈希值域区间关联起来,其中区间边界通过计算缓存服务器对应的哈希值来决定。(定义区间的哈希函数不一定和计算缓存服务器哈希值的函数相同,但是两个函数的返回值的范围需要匹配。)如果一个缓存服务器被移除,则它会从对应的区间会被并入到邻近的区间,其他的缓存服务器不需要任何改变。

3.原理简析

3.1环形Hash空间

        考虑通常的Hash算法都是将value 映射到一个32位的 key 值上,也就是 0~2^32-1次方的数值空间。我们可以将这个空间想象成一个首( 0 )尾( 2^32-1 )相接的圆环,如下图1所示:

一致性哈希(Consistent Hashing)原理和实现(整理)

 

图1 环形Hash空间

3.2把对象映射到环形Hash空间

        下面考虑 4 个资源对象 object1,object2,object3,bject4 ,通过 Hash 函数计算出的 Hash 值(key)在环上的分布如图2 所示:

一致性哈希(Consistent Hashing)原理和实现(整理)

 

图2 资源对象的Key值分布

如此:

Key1 = Hash(Object1);

······

Key4 = Hash(Object4);

 

3.3把资源服务器映射到环形Hash空间

         一致性Hash的基本思想就是将资源对象和缓存服务器(cache)都映射到同一个Hash数值空间中,并且使用相同的Hash算法。缓存服务器(cache)的Hash计算,一般的方法可以使用服务器(cache)机器的IP地址或者机器名作为Hash输入。

         假设当前有 A,B 和 C 共 3 台缓存服务器(cache),那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash值排列。

一致性哈希(Consistent Hashing)原理和实现(整理)

 

图3 服务器和资源对象的Key值分布

如此:

KeyA = Hash(CacheA);

······

KeyC = Hash(CacheC);

3.4把资源对象映射到缓存服务器

        现在服务器(cache)和资源对象都已经通过同一个Hash算法映射到Hash数值空间中了,接下来要考虑的就是如何将对象映射到服务器(cache)上面。

在这个环形空间中,如果沿着顺时针方向从对象的 key 值出发,直到遇见一个服务器(cache),那么就将该对象存储在这个缓存服务器上,因为对象和服务器的Hash 值是固定的,因此这个服务器必然是唯一和确定的。这就是对象和服务器的映射方法。

         依然继续上面的例子(参见图 3 ),那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2和object3 对应到 cache C ; object4 对应到 cache B ;

3.5考虑服务器的增减

        根据上面的映射方法(顺时针寻找)我们可以很容易的想象服务器的增减导致资源对象映射的改变情况。如图4和5所示:

一致性哈希(Consistent Hashing)原理和实现(整理)

 

图4 缓存服务器B被移除后的映射改变


一致性哈希(Consistent Hashing)原理和实现(整理)

图5 添加缓存服务器D后的映射改变

        根据上文所述,一致性哈希中节点的改变需要K/N个关键字的映射,N是缓存服务器个数,K是资源对象的个数,显然4/3和4/4取整都为1,与以上结果相符。

3.6采用虚拟节点保证平衡性

          在分布式缓存中,都希望资源对象能够尽可能的均匀分布到缓存服务器中,这也是平衡性的要求。但是,一致性哈希并不能保证绝对的平衡性。举个例子,在服务器(cache)较少情况下,资源对象并不能被均匀的映射到服务器上,比如在上面的例子中,仅部署 cache A 和 cache C 的情况下,在 4 个资源对象中, cache A 仅存储了 object1 ,而 cache C 则存储了 object2 、 object3 和object4 。显然,分布是很不平衡的。

          那么,为解决这种情况,一致性哈希引进了虚拟节点的概念。

          虚拟节点( virtual node )是实际服务器节点在Hash空间中的一个或多个复制品(replica),一实际个节点对应了若干个“虚拟节点”,这个对应个数也就是“复制个数”,“虚拟节点”在Hash空间中以Hash值排列。

         仍以仅部署 cache A 和 cache C 的情况为例,在图 4 中我们已经看到,cache节点分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了cache A; cache C1, cache C2 代表了 cache C;假设一种比较理想的情况,如图6:

一致性哈希(Consistent Hashing)原理和实现(整理)

图6 引入虚拟节点后的映射关系

         此时,资源对象到“虚拟节点”的映射关系为:objec1->cache A2 ; objec2->cache A1 ; objec3->cache C1 ; objec4->cache C2 ;因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高。

        引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache时的映射关系如图 7 所示:

一致性哈希(Consistent Hashing)原理和实现(整理)

图7 查询资源对象的计算过程

虚拟节点的Hash值计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A 的 IP 地址为202.168.14.241 。

引入虚拟节点前,计算cache A 的 Hash 值:Hash(“202.168.14.241”);

引入虚拟节点后,计算虚拟节点cache A1 和的Hash值:Hash(“202.168.14.241#1”);

下面这个URL中讲的很详细:http://www.codeproject.com/Articles/56138/Consistent-hashing

4.C++实现

         一致性哈希还必须解决两个问题,一个是用于节点存储和查找的数据结构的选择,另一个是Hash算法的选择。

        我们可以想象,节点是均匀的分布在你一个圆环上,也就是说节点Hash值可以存储在一个有序队列上。同时,该数据结构应该高效地支持节点频繁的增删,和理想的查询效率,那么自然会想到红黑树,它是一颗近似平衡的二叉树,因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。因此,用于节点存储和查找的数据结构我们选择红黑树,并实现其插入、删除、查找功能,并增加一个lookup函数,用于查找key中最小的节点。

         Hash算法的选择,一致性哈希很重要的一点就是为了解决负载均衡问题,而虚拟节点是负载均衡的关键,所以,我们希望虚拟节点可以均匀地散步在圆环上。这里我们选择MD5算法,通过MD5算法,可以将一个标识串(用于标示虚拟结点)进行转化,得到一个16字节的字符数组,再对该数组进行处理,得到一个整型的Hash值。由于MD5具有高度的离散性,所以生成的Hash值也会具有很大的离散性,会均匀地散列到“环”上。

百闻不如一见,下面实际代码:

1.红黑树的定义实现在此不赘述,我将专门写一个关于红黑树实现的博客,下面是实体节点的定义,实现比较简单。

 

/*实体结点*/
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;/*数据结点*/
};

2.虚拟节点的定义

 

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; /*虚拟结点所指向的实体结点*/
};
//虚拟节点的实现
CVirtualNode_s::CVirtualNode_s()
{
    node = NULL;
}

CVirtualNode_s::CVirtualNode_s(CNode_s * pNode)
{
    setNode_s(pNode);
}

void CVirtualNode_s::setNode_s(CNode_s * pNode)
{
    assert(pNode!=NULL);
    node = pNode;
}

CNode_s * CVirtualNode_s::getNode_s()
{
    return node;
}

void CVirtualNode_s::setHash(long pHash)
{
    hash = pHash;
}

long CVirtualNode_s ::getHash()
{
    return hash;
}

3.Md5的数据结构

 

/* Define the state of the MD5 Algorithm. */
typedef struct md5_state_s {
    md5_word_t count[2];	/* message length in bits, lsw first */
    md5_word_t abcd[4];		/* digest buffer */
    md5_byte_t buf[64];		/* accumulate block */
} md5_state_t;

void md5_init(md5_state_t *pms);
void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes);
void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);

/*用MD5算法计算结点的hash值,继承CHashFun父类*/
class CMD5HashFun : public CHashFun
{
public:
    virtual long getHashVal (const char * );
};


4.Md5的实现不赘述,下面给出用Md5计算Hash值的实现

 

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;
}


5.一致性哈希类的定义

 

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);

6.一致性哈希类的实现

 

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 [1000];
	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 [1000];
	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;
}

7.哈希函数类的接口定义

/*定义Hash函数类接口,用于计算结点的hash值*/
class CHashFun
{
public:
	virtual long getHashVal(const char *) = 0;
};

8.测试函数

 

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<<"----->	"<<node->getIden()<<" 	 "<<(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;
}

至此,所有的核心代码已经列出,详细仔细看完的朋友对一致性哈希的原理及实现有了更进一步的理解。

源码下载:https://files.cnblogs.com/coser/ConsistentHashAlgorithm.rar

5.PHP实现

下面是转自的PHP实现:http://blog.csdn.net/mayongzhan/article/details/4298834