您的位置:首页 > 其它

一致性hash算法分析

2014-11-30 18:04 211 查看
本文分析一致性哈希算法的特点和使用场景。

1、 来历

 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。

2、特点

一致性hash算法提出了在动态变化的Cache环境中,好的哈希算法含有的特点:

(1)平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

(2)单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。

(3)分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
(4)负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

3、动态cache

普通的哈希算法(也称硬哈希)采用简单取模的方式,将机器进行散列,这在cache环境不变的情况下能取得让人满意的结果,但是当cache环境动态变化时,这种静态取模的方式显然就不满足单调性的要求(当增加或减少一台机子时,几乎所有的存储内容都要被重新散列到别的缓冲区中)。

一致性哈希算法有多种具体的实现,包括Chord算法,KAD算法等实现,以上的算法的实现都比较复杂,这里介绍一种网上广为流传的一致性哈希算法的基本实现原理,感兴趣的同学可以根据上面的链接或者去网上查询更详细的资料。

一致性哈希算法的基本实现原理是将机器节点和key值都按照一样的hash算法映射到一个0~2^32的圆环上。当有一个写入缓存的请求到来时,计算Key值k对应的哈希值Hash(k),如果该值正好对应之前某个机器节点的Hash值,则直接写入该机器节点,如果没有对应的机器节点,则顺时针查找下一个节点,进行写入,如果超过2^32还没找到对应节点,则从0开始查找(因为是环状结构)。

如图1所示



图1中Key K的哈希值在A与B之间,于是K就由节点B来处理。

另外具体机器映射时,还可以根据处理能力不同,将一个实体节点映射到多个虚拟节点。

经过一致性哈希算法散列之后,当有新的机器加入时,将只影响一台机器的存储情况,例如新加入的节点H的散列在B与C之间,则原先由C处理的一些数据可能将移至H处理,而其他所有节点的处理情况都将保持不变,因此表现出很好的单调性。而如果删除一台机器,例如删除C节点,此时原来由C处理的数据将移至D节点,而其它节点的处理情况仍然不变。而由于在机器节点散列和缓冲内容散列时都采用了同一种散列算法,因此也很好得降低了分散性和负载。而通过引入虚拟节点的方式,也大大提高了平衡性。

4、应用场景

(1)服务器需求结构

3个应用服务器类型A,3个应用服务器类型B,1个负载均衡服务器类型H。

每个服务器A都建立到每个服务器B的连接。服务器A需要跟服务器B通信,需要选择合适的服务器B,服务器B是属于动态cache服务器类型的应用服务器。

每个应用服务器(包括A、B)都主动连接到负载均衡服务器H。

(2)服务器哈希表

服务器哈希表中包含:实际的服务器节点链表,每个实际节点对应的多个虚节点(如80个),虚节点对应的键为ip:port:#虚节点数字。每个虚节点都会包含实节点中的数据。

(3)负载均衡服务器

负载均衡服务器负责维护更新应用服务器的服务器哈希表。当服务器B动态增加或者减少时,需要广播更新后的服务器id列表到服务器A。

当应用服务器A或B建立与负载均衡服务器H连接时,服务器H把该服务器实节点对应的多个虚节点,加入本服务器的哈希表的虚节点列表,并同步新的哈希表中的服务器列表到之前建立了连接的所有服务器A和B。

(4)应用服务器

应用服务器需要更新来自负载均衡服务器发来的服务器id列表。

服务器A和B接到同步服务器列id表消息时,重新把列表中的所有服务器id分别以虚节点方式插入本服务器的哈希表。

服务器B需要移除不是属于本服务器的内存对象。根据内存对象id哈希值会对应指定的哈希表的虚节点,虚节点对应的实节点对象如果不是本服务器的,则说明该内存对象不是属于本服务器的,则需要移除。

应用服务器A发送消息到应用服务器B,先根据需求对象id获取哈希值,查询得哈希表中的虚节点对象,因为虚节点对象含实节点数据,也就是服务器id, 这样就可以发送到指定的服务器B,需求操作的内存对象也会在该服务器B中。

 5、一致性哈希列表实现

根据一致性哈希的平衡性、单调性,减少分散性和负载,实现了根据缓存内容计算服务器ID。
代码如下:字符串哈希函数
uint32_t FNVHash(const std::string& str)
{
const uint32_t fnv_prime = 0x811C9DC5;
uint32_t hash = 0;
for(std::size_t i = 0; i < str.length(); i++)
{
hash *= fnv_prime;
hash ^= str[i];
}

return hash;
}

频道哈希列表
zChannelConHash::zChannelConHash()
{
setHashFunc(&FNVHash);//设置哈希函数
}

zChannelConHash::~zChannelConHash()
{
ServerInfoMap_IT it = server_info_map_.begin();
for (; it!=server_info_map_.end(); ++it)
delete (it->second);
server_info_map_.clear();
}
//加入新服务器节点到哈希列表
int zChannelConHash::addNode(const ServerEntry &entry)
{
if (entry.dwInuse==0)
return -1;
ServerInfo *pnewinfo = new ServerInfo(entry.wdServerID, entry.pstrIP, entry.wdPort);// 服务器信息
if (pnewinfo == NULL)
return -1;
server_info_map_.insert(ServerInfoMap_VT(entry.wdServerID,pnewinfo));//加入服务器节点到列表

return proxy_.addNode(pnewinfo->getAddrString(),
zConHash::kVirtualNodeDefault,
pnewinfo);
}

//移除服务器对应的节点
int zChannelConHash::removeNode(WORD serverid)
{
ServerInfoMap_IT it = server_info_map_.find(serverid);
if (it == server_info_map_.end())
return 0;
std::string serveripstring = it->second->getAddrString();

int status = proxy_.removeNode(serveripstring);
if (status < 0)
return status;

delete (it->second);
server_info_map_.erase(it);
return 0;
}

//删除服务器列表信息
void zChannelConHash::clear()
{
proxy_.clear();

ServerInfoMap_IT it = server_info_map_.begin();
for (; it!=server_info_map_.end(); ++it)
delete (it->second);
server_info_map_.clear();
}
//由频道ID(缓存内容)哈希获取服务器ID
WORD zChannelConHash::lookupNode(QWORD channelid) const
{
if (channelid == 0)
return 0;
char channelidstr[32] = {0};
snprintf(channelidstr, sizeof(channelidstr)-1, "%lu", channelid);

void *pnodedata = proxy_.lookupNode(std::string(channelidstr));
if (pnodedata == NULL)
return 0;
ServerInfo *pinfo = static_cast<ServerInfo*>(pnodedata);
return pinfo->getServerID();
}
//获取服务器ID列表
uint32_t zChannelConHash::getServerIDList(std::list<WORD> &idlist) const
{
ServerInfoMap_CIT cit = server_info_map_.begin();
for (; cit!=server_info_map_.end(); ++cit)
{
idlist.push_back(cit->first);
}
return idlist.size();
}

//一致性哈希列表
zConHash::zConHash()
:hash_func_(NULL)
{
}
//删除服务器实节点和虚节点
zConHash::~zConHash()
{
VNodeMap_IT it = vnode_map_.begin();
for (; it!=vnode_map_.end(); ++it)
{
delete it->second;
}
vnode_map_.clear();

NodeList_IT nodeit = node_list_.begin();
for (; nodeit!=node_list_.end(); ++nodeit)
{
delete (*nodeit);
}
node_list_.clear();
}

//设置哈希函数
void zConHash::setHashFunc(HashFunction func)
{
hash_func_ = func;
}

//添加节点(传输地址、虚节点数量、实节点)
int zConHash::addNode(const std::string &node_iden, uint32_t virtual_node_count, void *nodedata)
{
zBaseGlobal::logger->debug("%s(iden=%s, vcount=%u)", __FUNCTION__, node_iden.c_str(), virtual_node_count);

if (node_iden=="" || virtual_node_count==0)
return -1;
zNode *pnode = new zNode(node_iden, virtual_node_count, nodedata);
if (pnode == NULL)
return -1;
node_list_.push_back(pnode);

for (uint32_t i=0; i<pnode->getVNodeCount(); i++)
{
char vstr[16] = {0};
snprintf(vstr, sizeof(vstr)-1, "#%u", i);
std::string vnodestr = pnode->getNodeIden() + vstr;
uint32_t vhash = hash_func_(vnodestr);//哈希服务器虚节点
zVirtualNode *vnode = new zVirtualNode(pnode, vhash);
if (vnode == NULL)
return -1;
vnode_map_.insert(VNodeMap_VT(vhash, vnode));
}
return 0;
}

//移除节点
int zConHash::removeNode(const std::string &node_iden)
{
NodeList_IT nodeit = node_list_.begin();
for (; nodeit!=node_list_.end(); ++nodeit)
{
if ((*nodeit)->getNodeIden() == node_iden)//移除实节点
break;
}
if (nodeit == node_list_.end())
return 0;
zNode *pnode = *nodeit;

VNodeMap_IT it = vnode_map_.begin();
while (it != vnode_map_.end())
{
if (it->second->getNode() == pnode)//移除虚节点
{
delete (it->second);
vnode_map_.erase(it++);
}
else
{
++it;
}
}
delete pnode;
node_list_.erase(nodeit);
return 0;
}

//移除所有实节点和虚节点
void zConHash::clear()
{
VNodeMap_IT it = vnode_map_.begin();
for (; it!=vnode_map_.end(); ++it)
{
delete it->second;
}
vnode_map_.clear();

NodeList_IT nodeit = node_list_.begin();
for (; nodeit!=node_list_.end(); ++nodeit)
{
delete (*nodeit);
}
node_list_.clear();
}

//根据缓存内容(频道ID)获取最相近的服务器哈希值
void* zConHash::lookupNode(const std::string &object) const
{
if (vnode_map_.empty())
return NULL;
uint32_t objecthash = hash_func_(object);
VNodeMap_CIT cit = vnode_map_.lower_bound(objecthash);
if (cit == vnode_map_.end())
cit = vnode_map_.begin();
#ifdef HAVE_LOG
zBaseGlobal::logger->debug("%s(%s)-->hash=%u->%s", __FUNCTION__,
object.c_str(), objecthash, cit->second->getNode()->getNodeIden().c_str());
#endif
return cit->second->getNode()->getData();
}

//输出所有实节点和虚节点的列表
void zConHash::dump() const
{
#ifdef HAVE_LOG
NodeList_CIT nodecit = node_list_.begin();
for (; nodecit!=node_list_.end(); ++nodecit)//遍历实节点列表
{
zNode *pnode = *nodecit;
std::ostringstream vnodess;

VNodeMap_CIT cit = vnode_map_.begin();
for (; cit!=vnode_map_.end(); ++cit)//虚节点列表
{
if (cit->second->getNode() == pnode)
{
vnodess << cit->second->getHash() << " ";
}
}
zBaseGlobal::logger->debug("[node]%s--[vnodecount=%u]:%s",
pnode->getNodeIden().c_str(), pnode->getVNodeCount(),
vnodess.str().c_str());
}
#endif
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: