一个简单的一致性哈希的负载均衡

        基于一致性哈希实现的负载均衡。一致性哈希主要需要注意以下几点:

一,根据用户IP或者ID等大体固定的数据进行hash求值,以保证每次相同的用户尽量可以hash到固定的服务器上去。

二,为了保证每台服务器的效能能够充分利用,例如有的服务器性能较好,性能为3,有的服务器性能一般,性能为2。不同的服务器可以对其不同的虚拟节点,以保证效能的充分利用。

三,考虑雪崩现象,一般hash也可以达到前两点,但是如果一台服务器崩溃,那么所有的用户请求压到下一台服务器,很可能该服务器也崩溃,以此类推,造成雪崩现象。

四,属于优化点,当服务器正常运行,此时新加一台服务器,应当适量的为该服务器增加以下映射量。

        大体思路:使用数组模拟虚拟节点,不同的服务器为其传递不同的权值,根据权值映射到该虚拟数组中。例如,开辟一百个大小的数组,初始值均为零,新加一台权值为4的服务器,那么进行hash四次,映射到的位置保存该服务器的编号值。当客户端的连接来到的时候,根据固定值进行hash求值,得到对应的服务器编号,从map表中找到对应的服务器,转发该消息。

        特殊情况:减少服务器时,该服务器在虚拟数组上的hash位置的值存储为其上一个点或者下一个点(有效点,即值非0的点)的值。本代码存储上一个有效点。

        极端情况:如果需要频繁的增加和删除。注意,这里是和,不是或。那么可能会造成虚拟节点趋近于相同化,即某个固定值极对应的位置极可能很相近。

先附上代码,再附上找负载均衡时候总结的资料。



#include 
#include 
#include 
#include 
#include 
#include 


class server
{
public:
	server(int socket, int weight) :_socket(socket),_num(0),_online_num(0),_weight(weight){}
	int get_socket(){ return _socket; }
	int OnlineNum() { return _online_num; }
	void addClient(){ _online_num++; }
	void delClient(){ _online_num--; }
	int get_ServerNum(){ return _num; } //该编号就是balance类中的键值
	void set_ServerNum(int num){ _num = num; }
	int get_Weight(){ return _weight; }
	void sendMsg(std::string &strMsg);
	void recvMsg(std::string &strMsg){ _strMsg = strMsg; }
	//~server(){ close _socket; }
private:
	int _socket;
	int _online_num; //在线人数
	int _num; //编号
	int _weight; //权值
	std::string _strMsg; //Msg消息
};
class client
{
public:
	client(int socket, int id) :_socket(socket),_id(id){}
	void set_num(int iNum){ _num = iNum; }
	int get_num(){ return _num; }
	int getId(){ return _id; }
	void sendMsg(std::string &strMsg); //s->c
	void recvMsg(std::string &strMsg){ _strMsg = strMsg; }
	//~client(){ close(_socket); }
private:
	int _socket;
	int _num; //虚拟数组编号
	int _id; //id
	std::string _strMsg; //消息字符串
};
class balance
{
public:
	static balance *getInstance(int iSize){
		if (instance == NULL){
			instance = new balance(iSize);
		}
		return instance;
	}
	void addOnlineNum(){ _online_num++; }
	void delOnlineNum(){ _online_num--; }
	void delOnlineNum(int iNum){ _online_num -= iNum; }
	int getOnlineNum(){ return _online_num; }
	bool addServer(server *ser);
	bool delServer(server *ser);
	int allWriteNum();
	//映射节点
	void addVirualPoint(server *ser);
	//去掉映射节点
	void delVirualPoint(server *ser);
	//获取一个map中的空位置 error:-1
	int getSpacePos();
	//随机一个数组的位置
	int getVirualVecPos();

	//一个新连接
	void addClient(client *cli);
	void delClient(client *cli);
	client *getClientById(int id);

	//消息转发 s->s
	void sendMsg(server *ser, std::string &strMsg);
	
	//测试相关
	server *testAddNewClient(int iWeight);
	void testDelNuwClient(server *ser);
private:
	balance(int iSize):_online_num(0),iVecMaxSize(iSize){ _veAllNum.resize(iVecMaxSize); }
	static balance *instance;
	//编号和节点的map表
	std::map _balanceMap;
	//节点的数组
	std::vector _veAllNum;
	//总在线人数
	unsigned int _online_num;
	//已经被删除的编号 取的时候直接取就行
	std::set _unUsedSet;
	//数组大小
	int iVecMaxSize;
	//所有客户端
	std::map _allUserMap;
};


#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)。这种最普通的哈希算法,可以在一定程度上达到负载的目的,但是其存在极其的不合理性。首先,每台机器的性能并非完全相同,这样会造成某些机器性能浪费,某些机器性能过载。其次,一般的数据访问方式分为两种:

一个简单的一致性哈希的负载均衡_第1张图片

这样造成了时间的浪费,同时,也造成了数据库的压力。为了解决这两处不足,一致性哈希应运而生。

一致性哈希,以一种固定的方法进行哈希,比如IP地址等,这样的目的是为了保证,同一个对象尽量哈希在固定的服务器上,加快访问速度,减少数据库的压力。

一致性哈希模拟了一个圆,哈希范围从0到2的32次方减一。并通过固定的哈希算法将我们的服务器哈希到此圆上。如下图,红色代表服务器。

一个简单的一致性哈希的负载均衡_第2张图片

此时,客户端的请求也可以哈希到该圆上,如上图绿色的点。这时候,根据客户端哈希的位置(绿点)顺时针方向找到的第一台服务器(红点)就是处理该请求的服务器。这样可以保证基本每次该点都可以哈希到同一台服务器上去。此时,如果某台机器宕机,那么该机器上的点可以以顺时针方向找到处理他的新服务器。关于增加机器和接下来的问题一起讨论。

接下来解决上边提出的另一个问题,每台机器的性能可能不尽相同,这个问题的解决和其他的问题可以归为一类,比如我们增加机器,那么之后的数据在哈希的时候,可能其他机器上的压力已经很大了,但是哈希的平均性并不会专门去照顾这个新来的机器,新机器仍然很空。为了解决类似于此类的问题,一致性哈希引入虚拟节点。虚拟节点的存在很好的解决了这种问题。

一个简单的一致性哈希的负载均衡_第3张图片

如上图。内圈为哈希规则的四台服务器(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的固定比例上。

你可能感兴趣的:(随笔)