基于一致性哈希实现的负载均衡。一致性哈希主要需要注意以下几点:
一,根据用户IP或者ID等大体固定的数据进行hash求值,以保证每次相同的用户尽量可以hash到固定的服务器上去。
二,为了保证每台服务器的效能能够充分利用,例如有的服务器性能较好,性能为3,有的服务器性能一般,性能为2。不同的服务器可以对其不同的虚拟节点,以保证效能的充分利用。
三,考虑雪崩现象,一般hash也可以达到前两点,但是如果一台服务器崩溃,那么所有的用户请求压到下一台服务器,很可能该服务器也崩溃,以此类推,造成雪崩现象。
四,属于优化点,当服务器正常运行,此时新加一台服务器,应当适量的为该服务器增加以下映射量。
大体思路:使用数组模拟虚拟节点,不同的服务器为其传递不同的权值,根据权值映射到该虚拟数组中。例如,开辟一百个大小的数组,初始值均为零,新加一台权值为4的服务器,那么进行hash四次,映射到的位置保存该服务器的编号值。当客户端的连接来到的时候,根据固定值进行hash求值,得到对应的服务器编号,从map表中找到对应的服务器,转发该消息。
特殊情况:减少服务器时,该服务器在虚拟数组上的hash位置的值存储为其上一个点或者下一个点(有效点,即值非0的点)的值。本代码存储上一个有效点。
极端情况:如果需要频繁的增加和删除。注意,这里是和,不是或。那么可能会造成虚拟节点趋近于相同化,即某个固定值极对应的位置极可能很相近。
先附上代码,再附上找负载均衡时候总结的资料。
#include
#include
#include "head_balance.h"
#include
#include
std::string ERROR = "ERROR";
std::string DEBUG = "DEBUG";
balance *balance::instance = NULL;
#define VEC_ALL_MAX_SIZE 100
void showlog(std::string &strWord, std::string &strLevel)
{
if (strLevel.compare("DEBUG") == 0)
{
std::cout << "--DEBUG--: " << strWord.c_str() << std::endl;
}
else if (strLevel.compare("ERROR") == 0)
{
std::cout << "ERROR: " << strWord.c_str() << std::endl;
}
}
int balance::getSpacePos()
{
//检查被删去编号的set
if (_unUsedSet.size() > 0)
{
std::set::iterator iter = _unUsedSet.begin();
if (iter == _unUsedSet.end())
{
std::string strWord = "getSpacePos时发现位置错误";
showlog(strWord, ERROR);
}
int iPos = *iter;
_unUsedSet.erase(iter);
return iPos;
}
//在map表中获取新位置
int iMaxSize = _balanceMap.size();
std::string strWord = "getSpacePos: " + std::to_string(iMaxSize+1);
showlog(strWord, DEBUG);
return iMaxSize+1;
}
int balance::getVirualVecPos()
{
//最好在本函数中根据IP或者ID等用户固定的数据进行hash求值
//srand((int)time(NULL));
int iPos = rand() % VEC_ALL_MAX_SIZE;
while (iPos < VEC_ALL_MAX_SIZE && _veAllNum[iPos]) iPos++;
if (iPos == VEC_ALL_MAX_SIZE)
{
iPos = 0;
while (iPos < VEC_ALL_MAX_SIZE && _veAllNum[iPos]) iPos++;
}
if (iPos == VEC_ALL_MAX_SIZE)
{
std::string strWord = "writeVirualPoint时该数组已满";
showlog(strWord, ERROR);
return -1;
}
return iPos;
}
void balance::addVirualPoint(server *ser)
{
if (!ser)
{
std::string strWord = "writeVirualPoint时该服务器为空 放弃本次写入 编号:" + std::to_string(ser->get_ServerNum());
showlog(strWord, ERROR);
return;
}
int iWeight = ser->get_Weight();
while (iWeight > 0)
{
iWeight--;
int iPos = getVirualVecPos();
std::string strWord = "writeVirualPoint时写入数组 位置:" + std::to_string(iPos) + "编号:" + std::to_string(ser->get_ServerNum());
showlog(strWord, DEBUG);
if (iPos != -1)
_veAllNum[iPos] = ser->get_ServerNum();
}
//测试打印整个环的信息
std::cout << "-----------addserver---------" << std::endl;
for (int i = 0; i < VEC_ALL_MAX_SIZE; ++i)
{
if (_veAllNum[i])
{
std::cout << "pos:" << i << " num:" << _veAllNum[i] << std::endl;
}
}
std::cout << "----------addserver---------" << std::endl;
}
bool balance::addServer(server *ser)
{
if (!ser)
return false;
if (ser->get_ServerNum())
{
std::string strWord = "addServer时发现重复新加同一台:" + std::to_string(ser->get_ServerNum());
showlog(strWord, ERROR);
return false;
}
if (allWriteNum() + ser->get_Weight() >= VEC_ALL_MAX_SIZE)
{
std::string strWord = "addServer时 数组大小不够 当前大小:" + std::to_string(_veAllNum.size()) +
"新加服务器大小:" + std::to_string(ser->get_Weight());
showlog(strWord, ERROR);
return false;
}
//map中获取一个空位置
int iPos = this->getSpacePos();
//空位置赋值给该server的_num,并插入map
_balanceMap[iPos] = ser;
ser->set_ServerNum(iPos);
//将权值加入到该数组中
addVirualPoint(ser);
return true;
}
void balance::delVirualPoint(server *ser)
{
if (!ser)
{
std::string strWord = "delVirualPoint时该服务器为空 放弃本次删除 服务器编号:" + std::to_string(ser->get_ServerNum());
showlog(strWord, ERROR);
return;
}
int iNewSerNum = 0;
for (int i = 0; i < VEC_ALL_MAX_SIZE; ++i)
{
if (_veAllNum[i] != 0 && _veAllNum[i] != ser->get_ServerNum())
iNewSerNum = _veAllNum[i];
if (iNewSerNum != 0 && ser->get_ServerNum() == _veAllNum[i])
_veAllNum[i] = iNewSerNum;
}
if (_balanceMap.size() == 1) //最后一个
iNewSerNum = 0;
for (int i = 0; i < VEC_ALL_MAX_SIZE; ++i)
{ //需要两次循环,因为是用前一个的值去覆盖下一个的值。可能会忽略开头。同时,即便删除最后一个,也没关系,第二次循环会处理
if (ser->get_ServerNum() == _veAllNum[i])
_veAllNum[i] = iNewSerNum;
}
//测试打印整个环的信息
std::cout << "----------delserver---------" << std::endl;
for (int i = 0; i < VEC_ALL_MAX_SIZE; ++i)
{
if (_veAllNum[i])
{
std::cout << "pos:" << i << " num:" << _veAllNum[i] << std::endl;
}
}
std::cout << "----------delserver---------" << std::endl;
}
bool balance::delServer(server *ser)
{
if (!ser)
return false;
if (ser->get_ServerNum() <= 0)
{
std::string strWord = "delServer时 编号非法:" + std::to_string(ser->get_ServerNum());
showlog(strWord, ERROR);
return false;
}
//在数组中修改该编号指向的服务器
delVirualPoint(ser);
//从map表中删掉 并加入无用set集合
std::map::iterator it = _balanceMap.find(ser->get_ServerNum());
if (it == _balanceMap.end())
{
std::string strWord = "delServer时 未找到该服务器:" + std::to_string(ser->get_ServerNum());
showlog(strWord, ERROR);
return false;
}
int iNum = it->first;
_balanceMap.erase(it);
_unUsedSet.insert(iNum);
//释放内存 减少该服务器上人数
delOnlineNum(ser->OnlineNum());
std::string strWord = "delServer 删除服务器:" + std::to_string(iNum) + " 减少在线人数:" + std::to_string(ser->OnlineNum())
+ " 目前在线人数:" + std::to_string(getOnlineNum());
showlog(strWord, DEBUG);
delete ser;
ser = NULL;
return true;
}
int balance::allWriteNum()
{
int ret = 0;
for (unsigned int i = 0; i < _veAllNum.size(); ++i)
{
if (_veAllNum[i])
ret++;
}
return ret;
}
////连接
void balance::addClient(client *cli)
{
if (!cli)
return;
std::map::iterator it = _allUserMap.find(cli->getId());
if (it != _allUserMap.end())
{
std::string strWord = "addClient 新加的连接已经存在:" + std::to_string(cli->getId());
showlog(strWord, ERROR);
return;
}
_allUserMap.erase(it);
std::string strWord = "addClient 新加连接:" + std::to_string(cli->getId());
showlog(strWord, DEBUG);
//分配服务器
int iPos = getVirualVecPos();
if (iPos == 0)
return;
cli->set_num(iPos); //分配虚拟节点
_allUserMap[cli->getId()] = cli;
}
void balance::delClient(client *cli)
{
if (!cli)
return;
std::map::iterator it = _allUserMap.find(cli->getId());
if (it == _allUserMap.end())
{
std::string strWord = "delClient 该客户端不存在:" + std::to_string(cli->getId());
showlog(strWord, ERROR);
return;
}
_allUserMap.erase(it);
delete cli;
cli = NULL;
}
client *balance::getClientById(int id)
{
if (id <= 0)
return NULL;
std::map::iterator it = _allUserMap.find(id);
if (it == _allUserMap.end())
return NULL;
return it->second;
}
////测试
server *balance::testAddNewClient(int iWeight)
{
if (iWeight <= 0)
return NULL;
server *ser = new server(0, iWeight);
if (ser == NULL)
{
std::string strWord = "new space error";
showlog(strWord, ERROR);
return NULL;
}
addServer(ser);
return ser;
}
void balance::testDelNuwClient(server *ser)
{
if (!ser)
return;
delServer(ser);
}
<测试代码>
#include "head_balance.h"
#include
int main()
{
//std::vector veNum = {1,2,3,4};
std::vector veWeight = {4,3,5};
std::vector veSer;
balance *bal = balance::getInstance(100);
for (int i = 0; i < (int)veWeight.size(); ++i)
{
server *tmp = bal->testAddNewClient(veWeight[i]);
veSer.push_back(tmp);
}
int i = 0;
for (std::vector::iterator it = veSer.begin(); it != veSer.end(); )
{
bal->testDelNuwClient(*it);
it = veSer.erase(it);
server *tmp = NULL;
if (i == 0)
tmp = bal->testAddNewClient(4);
else if (i = 1)
tmp = bal->testAddNewClient(3);
else if (i == 2)
tmp = bal->testAddNewClient(3);
//veSer.push_back(tmp);
i++;
}
server *tmp = bal->testAddNewClient(4);
veSer.push_back(tmp);
tmp = bal->testAddNewClient(6);
veSer.push_back(tmp);
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
为了达到负载的目的,可以采用哈希算法(hash % m)。这种最普通的哈希算法,可以在一定程度上达到负载的目的,但是其存在极其的不合理性。首先,每台机器的性能并非完全相同,这样会造成某些机器性能浪费,某些机器性能过载。其次,一般的数据访问方式分为两种:
这样造成了时间的浪费,同时,也造成了数据库的压力。为了解决这两处不足,一致性哈希应运而生。
一致性哈希,以一种固定的方法进行哈希,比如IP地址等,这样的目的是为了保证,同一个对象尽量哈希在固定的服务器上,加快访问速度,减少数据库的压力。
一致性哈希模拟了一个圆,哈希范围从0到2的32次方减一。并通过固定的哈希算法将我们的服务器哈希到此圆上。如下图,红色代表服务器。
此时,客户端的请求也可以哈希到该圆上,如上图绿色的点。这时候,根据客户端哈希的位置(绿点)顺时针方向找到的第一台服务器(红点)就是处理该请求的服务器。这样可以保证基本每次该点都可以哈希到同一台服务器上去。此时,如果某台机器宕机,那么该机器上的点可以以顺时针方向找到处理他的新服务器。关于增加机器和接下来的问题一起讨论。
接下来解决上边提出的另一个问题,每台机器的性能可能不尽相同,这个问题的解决和其他的问题可以归为一类,比如我们增加机器,那么之后的数据在哈希的时候,可能其他机器上的压力已经很大了,但是哈希的平均性并不会专门去照顾这个新来的机器,新机器仍然很空。为了解决类似于此类的问题,一致性哈希引入虚拟节点。虚拟节点的存在很好的解决了这种问题。
如上图。内圈为哈希规则的四台服务器(1-4),假设四台服务器上的虚拟节点分别为外圈的黑色小圆,3个,2,个,4个,1个。此时新加了一个服务器(蓝色),蓝色由于新加入的,所以并不存在对他的连接,而其他的1,2,3,4号服务器都已经存在了或多或少的连接,所以为新加的服务器(蓝色)添加五个虚拟节点(外圈绿色)。以便能够多为5号服务器分配一些连接。
正文结束。
所以,按照我的理解,虚拟节点遵循了某种变化规则的。这种规则保证虚拟节点的动态调整。最简单的,虚拟节点的这种变化规则需要保证,每个服务器的有效连接比率在一个定值,比如,假设没加入5号服务器之前,正好有10个连接,正好分配给四个服务器的连接数为3,2,4,1。但是,在新加入5号之后,3号服务器的连接数突然间全部释放,也就是说,长连接的用户可能释放连接的时间不确定,根据这种不同的释放时间,所以在调整虚拟节点的时候,必然需要为3号和5号多增加虚拟节点,以保证各个服务器的有效连接比率保持在3:2:4:1:5的固定比例上。