一致性哈希通过哈希环实现KEY到对应Node的映射:
具体算法过程为:先构造一个长为2^32的整数环,根据节点的Hash值将Node(服务器)分配到环的对应位置上,然后计算需要查询数据的键值KEY的哈希值,然后在哈希环上顺时针查询离这个KEY最近的服务器节点,完成映射。
哈希环能够很好的进行负载均衡,而且具有很好的扩展性,如果一个服务器宕机或者需要添加新的服务器,大部分键值以前的映射关系不会改变,只会改变一小部分映射关系。假如Node4服务器意外下线,只有KEY4的映射关系会出现改变,KEY4顺时针查询的第一个Node会变成Node1,而其他键值查询不会变。
下面我们来简单的实现一下哈希环:
class Node
{
public:
Node(string _addr) :NodeAddr(_addr)
{
}
string getNode()
{
return NodeAddr;
}
void setNodeAddr(string _addr)
{
this->NodeAddr = _addr;
}
void addCliNode(Node cli)
{
CliNodes.push_back(cli);
}
void printNodeState()
{
cout << "当前节点地址:" << NodeAddr << " 哈希值:" << hs(NodeAddr) << endl;
for (Node& n : CliNodes)
{
cout << "客户端节点地址:" << n.getNode() << " 哈希值:" << hs(n.getNode()) << endl;
}
cout << endl << endl << endl;
}
private:
string NodeAddr;
vector<Node> CliNodes;
//size_t NodeHashValue;
};
class HashRing
{
public:
void addNode(string _addr)
{
Node newNode(_addr);
ServerNodes.insert(make_pair(hs(_addr), move(newNode)));
}
void removeNode(string _addr)
{
size_t hashvalue = hs(_addr);
ServerNodes.erase(hashvalue);
}
void distributionNode(string _addr)
{
auto it = ServerNodes.lower_bound(hs(_addr));
if (it == ServerNodes.end())
{
it = ServerNodes.begin();
}
it->second.addCliNode(Node(_addr));
}
void printHashRingState()
{
for (auto it = ServerNodes.begin(); it != ServerNodes.end(); ++it)
{
it->second.printNodeState();
}
}
private:
map<size_t, Node> ServerNodes;
};
用下面代码进行测试:
int main()
{
HashRing hr;
hr.addNode("192.168.0.1");
hr.addNode("192.168.10.2");
hr.addNode("192.168.20.3");
hr.addNode("192.168.30.4");
hr.distributionNode("127.0.0.1");
hr.distributionNode("110.0.3.1");
hr.distributionNode("113.51.15.48");
hr.distributionNode("114.48.96.125");
hr.distributionNode("115.47.12.35");
hr.distributionNode("116.87.95.62");
hr.printHashRingState();
system("pause");
}
运行结果如下:
但是这个算法其实还存在小问题,添加的节点如果分布不均匀可能会造成负载不均衡,有的Node需要负责很多的客户端,有的Node只负责很少的客户端。解决的办法也很简单,每添加一个物理机时在哈希环上添加多个虚拟节点,简单实现一下:
class HashRingWithVirtualNode
{
public:
HashRingWithVirtualNode(int num) :VirtualNodeNum(num)
{
}
void addNode(string _addr)
{
for (int i = 0; i < VirtualNodeNum; ++i)
{
_addr.append(1, (i % 128));//为虚拟节点命名
Node newNode(_addr);//
ServerNodes.insert(make_pair(hs(_addr), move(newNode)));
}
}
void removeNode(string _addr)
{
for (int i = 0; i < VirtualNodeNum; ++i)
{
_addr.append(1, (i % 128));
size_t hashvalue = hs(_addr);
ServerNodes.erase(hashvalue);
}
}
void distributionNode(string _addr)
{
auto it = ServerNodes.lower_bound(hs(_addr));
if (it == ServerNodes.end())
{
it = ServerNodes.begin();
}
it->second.addCliNode(Node(_addr));
}
void printHashRingState()
{
for (auto it = ServerNodes.begin(); it != ServerNodes.end(); ++it)
{
it->second.printNodeState();
}
}
private:
map<size_t, Node> ServerNodes;
int VirtualNodeNum;
};
给每个虚拟节点设计更好的名称应该可以获得更好的哈希效果,使节点分布更均匀。
如果想要知道真实物理节点上负载了哪些客户端,对虚拟节点的名字进行解析,很容易得到虚拟节点所属的物理节点。
讨论
哈希环一般使用二叉树来实现,插入节点和寻找节点效率都能达到logn。在我的代码是使用了map,也就是红黑树来实现,插入和删除新节点的时间复杂度是logn,但是为客户端寻找对应的服务器节点的时间复杂度我目前还不太清楚,因为使用了lower_bound来查找对应的节点。
不过猜测应该是logn,实现方法应该是就像正常的二叉搜索树查找,遇到比自己值大的节点就向左查找,反之向右,用一个node来保存查询路径上的hash值大于自己的最小节点,搜索结束时返回指向保存节点的迭代器。
另外,感觉跳表实现哈希环应该也不错,插入、删除和查询节点的效率应该都能到logn.