什么是负载均衡?
防止单个Netty服务器负载过大,而导致服务不可用,因为搭建了Netty服务的集群,理想的情况下,是每个Netty服务节点都处理连接的数量都大致均衡,当然,如果服务器配置不一样,比如有某些服务器的配置相对较高,可以把更多的流量打到配置高的服务器上,这依赖于具体的负载均衡策略或者算法。
在IM集群中,当用户登录成功之后,短连接网关需要返回给用户一个最佳的Netty节点的地址,让用户来建立Netty长链接。
负载均衡就是为了给用户挑选出一个最佳的Netty节点的地址。
ImLoadBalance实现简易的负载均衡策略:
大致思路:
- 找出指定路径下所有的字节点信息
- ImWorker中存储了一个balance字段用来记录当前Netty节点的连接数量
- 所有的节点按照balance从小到大排序,返回连接数最小的那个节点信息即可
@Data
@Slf4j
public class ImLoadBalance {
//Zk客户端
private CuratorFramework client = null;
private String managerPath;
public ImLoadBalance(CuratorZKclient curatorZKClient) {
this.client = curatorZKClient.getClient();
managerPath = ServerConstants.MANAGE_PATH;
}
/**
* 获取负载最小的IM节点
*
* @return
*/
public ImNode getBestWorker() {
List<ImNode> workers = getWorkers();
log.info("全部节点如下:");
workers.stream().forEach(node ->
{
log.info("节点信息:{}", JsonUtil.pojoToJson(node));
});
ImNode best = balance(workers);
return best;
}
/**
* 按照负载排序
*
* @param items 所有的节点
* @return 负载最小的IM节点
*/
protected ImNode balance(List<ImNode> items) {
if (items.size() > 0) {
// 根据balance值由小到大排序
Collections.sort(items);
// 返回balance值最小的那个
ImNode node = items.get(0);
log.info("最佳的节点为:{}", JsonUtil.pojoToJson(node));
return node;
} else {
return null;
}
}
/**
* 从zookeeper中拿到所有IM节点
*/
public List<ImNode> getWorkers() {
List<ImNode> workers = new ArrayList<ImNode>();
List<String> children = null;
try {
children = client.getChildren().forPath(managerPath);
} catch (Exception e) {
e.printStackTrace();
return null;
}
for (String child : children) {
log.info("child:", child);
byte[] payload = null;
try {
payload = client.getData().forPath(managerPath + "/" + child);
} catch (Exception e) {
e.printStackTrace();
}
if (null == payload) {
continue;
}
ImNode node = JsonUtil.jsonBytes2Object(payload, ImNode.class);
node.setId(getIdByPath(child));
workers.add(node);
}
return workers;
}
/**
* 取得IM 节点编号
*
* @param path 路径
* @return 编号
*/
public long getIdByPath(String path) {
String sid = null;
if (null == path) {
throw new RuntimeException("节点路径有误");
}
int index = path.lastIndexOf(ServerConstants.PATH_PREFIX_NO_STRIP);
if (index >= 0) {
index += ServerConstants.PATH_PREFIX_NO_STRIP.length();
sid = index <= path.length() ? path.substring(index) : null;
}
if (null == sid) {
throw new RuntimeException("节点ID获取失败");
}
return Long.parseLong(sid);
}
/**
* 从zookeeper中删除所有IM节点
*/
public void removeWorkers() {
try {
client.delete().deletingChildrenIfNeeded().forPath(managerPath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里有一个单元测试:
@Test
public void testGetBestWorker() throws Exception {
ImNode bestWorker = imLoadBalance.getBestWorker();
System.out.println("bestWorker = " + bestWorker);
ThreadUtil.sleepSeconds(Integer.MAX_VALUE);
}
轮训是一种非常简单的实现方式,依次把请求分配给可用服务
优点:
实现比较简单,有很多开源中间件都有实现,比如Ribbon默认的策略就是轮训策略
还有一个优点就是,请求能够比较平均的分摊到可用服务上面
缺点:
请求均匀分配,因为后端服务器可能存在性能差异,说白了,就是有些服务器配置好,有些配置一般。
可能会造成某些机器资源利用率低的问题。
加权本质是一种带有优先级的方式,加权轮训就是一种改进的轮训算法,轮训算法是权值相等的加权轮训。
需要给后端每个服务器设置不同的权重,决定分配的请求书的比例,这个算法应用就比较广泛,对于无状态的负载场景,非常适用。
优点:
解决了服务器性能不一的情况
缺点:
权重值需要静态配置无法动态更新或者自动调节,也不适合对长链接和命中率有要求的场景
随机把请求分配给后端服务器,请求分配的均匀程度依赖于随机算法,实现简单,常常用于配合处理一些极端的情况,比如,热点请求,这个时候就可以随机分配到任意可用的后端,以分散热点。
这个其实比较常见,也比较常用。 包括分布式场景下,比较常见的一致性哈希算法。
把请求分配给活动连接数量最少的后端服务器,它通过活动来估计服务器的负载,比较智能,但是需要维护后端服务器的连接列表。
在最小连接数的基础上,考虑权重分配。
一定时间内,记录每个节点的最短响应时间,那个节点响应时间最短,就把请求负载到这个节点上面。