负载均衡策略
一、负载均衡的场景
负载均衡简单的讲就是讲多个请求(通过中间节点)合理的分配给多个响应节点 ,依据不同场景下节点的处理能力和请求及响应特性等产生了不同的分配请求方式。
其中对后侧的响应节点包含了一个假设 ,就是对于一个请求任意一个节点理论上都是能够处理的(可能受限于节点当前已经在处理请求的压力并不能够及时处理或响应 ),对于有请求偏好的也就是有些节点能处理有些节点不能处理,这种涉及路由选择的方式不在此文讨论。
常见的负载均衡方式有随机方式、加权随机方式、轮询方式、加权轮询方式、最小连接数方式、一致性哈希方式
二、 不同方式的特点
随机方式: 按概率等可能性的讲请求分配给后侧的节点,算法只考虑节点的总数量,实现非常简单
加权随机方式: 从后侧的服务节点考量处理能力,给不同的节点进行多分或少分,在随机方式上做进一步加强,可以更好的利用不同节点的处理能力 。
轮询方式: 维护一个环形服务节点队列,将请求依次分给节点处理,和随机方式的分配从概率上具有一样的等可能性
加权轮询方式: 考虑了服务节点不同的处理能力,是对轮询方式的进一步加强
最小连接数方式: 对于一些长连接的请求,往往以连接数量的多寡来相应的评估服务器的处理压力,此时最小连接数对应的服务器可能是压力最小的,将新请求分配给这个节点,通常较为合适。
一致性哈希方式: 对于一些有状态的请求,往往将同一请求来源的分配到同一服务节点,能够减少服务器的处理时间,或者是对于一些请求端和服务节点存在地域远近的情况,分配近的服务节点能够减少网络延时,所以从依据请求中的特定来源信息做分配是有很多应用的
三、 代码的简略实现
代码示例只展示部分表示核心功能的代码,非必要或者很简单的逻辑不做展示,以便更好的理解要点。
1、模拟
用几个Java类来模拟负载均衡场景涉及的关键角色:请求信息、负载均衡器、选择器(算法策略实现)、服务节点 。
![](https://img-blog.csdnimg.cn/img_convert/d17b00019609a82abd7d697b8df21b33.png)
请求信息:
// 代表请求
public class Request implements Cloneable {
// 模拟地域 或 userType 类型
private String area ;
// 地址 以便后续做一致性哈希映射
private int ip ;
}
基础的负载均衡器
/**
* @author : wangchaodee
* @Description: 基础的负载均衡器
* 1、初始化时就注册选择器(设定负载均衡的算法方式)
* 2、然后注册服务器列表
* 3、执行请求
*/
public class LoadBalance {
/**
* 先简化逻辑 就用List 表示服务器列表 ,
* 如需扩展服务器的动态维护功能 需要用Map结构更为合适
*/
private List<Server> serverList;
private Selector selector ;
public LoadBalance(Selector selector){
this.selector = selector ;
}
/**
* 将服务器集群 注册到负载均衡器中
* @param serverList 不为空
*/
public void registerServerList(List<Server> serverList){
this.serverList = serverList;
count = serverList.size() ;
selector.registerServerList(getServerList());
}
/**
* 给一个请求分配一个处理节点 // 让具体子类实现
* @param request
* @return
*/
Server handleRequest(Request request){
Server server = serverList.get(selector.generateIdx());
server.handle(request);
return server;
}
}
选择器
/**
* @author : wangchaodee
* @Description: 选择器
*/
public interface Selector {
/**
* 返回按特定策略计算出的服务器ID
* @return
*/
int generateIdx() ;
void registerServerList(List<Server> serverList);
}
服务节点
/**
* @author : wangchaodee
* @Description: 服务节点
*/
public class Server {
// 代表server 的唯一标识, 模拟 , 要求不重复, 相当于ip 或 uri,
// 简化逻辑下直接用位序 ,不用id查找
private int id ;
//已处理的连接数量
private int processed;
// 假定可设置 100 ~ 1000 范围 服务器的最大处理链接 用于加权
private int maxConnect ;
private int currentConnect ;
public void handle(Request request){
processed++;
}
/**
* 模拟返回 当前连接的数量
* @return
*/
public int getCurrentConnect(Random random){
currentConnect = random.nextInt(maxConnect);
return currentConnect;
}
}
2、随机方式
/**
* @author : wangchaodee
* @Description: 随机方式的负载均衡
*/
public class RandomSelector implements Selector {
private Random random;
private int count ;
public RandomSelector() {
this.random = new Random();
this.count =1 ;
}
@Override
public int generateIdx() {
return random.nextInt(this.count);
}
@Override
public void registerServerList(List<Server> serverList) {
count= serverList.size() ;
}
}
3、加权随机方式
先写个通用的加权处理方式
/**
* @author : wangchaodee
* @Description: 增加加权过滤
*/
public abstract class WeightSelector implements Selector {
// 前缀和 数组
private int[] pre;
// 总数
private int total;
protected int cur;
/**
* 选择器如果直接选择个Server 返回也是可以的
*
* 如果后续不用到server , 那么直接给个array 是否更合适,
*
* @param serverList
*/
public void registerServerList(List<Server> serverList) {
int N = serverList.size();
int[] array = new int[N];
for (int i = 1; i < N; i++) {
array[i] = serverList.get(i).getMaxConnect();
}
int gcd = getGcd(array);
array = processing(array, gcd);
pre = new int[N];
pre[0] = array[0];
for (int i = 1; i < N; i++) {
pre[i] = array[i] + pre[i - 1];
}
total = pre[N - 1] +1 ;
cur = 0 ;
}
private int[] processing(int[] array, int gcd) {
if (gcd == 1) return array;
for (int i = 0; i < array.length; i++) {
array[i] /= gcd;
}
return array;
}
private int getGcd(int[] nums) {
// 找公约数
}
private int binarySearch(int x) {
// 二分查找
}
public int generateIdx() {
int idx = binarySearch(generateCur());
return idx;
}
/**
* 子类复写这个方法就可以
* @return
*/
public abstract int generateCur();
}
再实现随机的加权处理
/**
* @author : wangchaodee
* @Description: 加权随机方式的负载均衡 力扣 528 加权随机
*/
public class WeightRandomSelector extends WeightSelector {
private Random random;
public WeightRandomSelector() {
this.random = new Random();
}
/**
* 用随机方式生成cur 的值
* @return
*/
public int generateCur() {
cur = random.nextInt(getTotal());
return cur;
}
}
4、轮询方式
/**
* @author : wangchaodee
* @Description: 轮询方式的负载均衡
*/
public class RoundRobinSelector implements Selector {
private int idx ;
private int count ;
@Override
public int generateIdx() {
idx =( idx +1 ) % this.count ;
return idx;
}
@Override
public void registerServerList(List<Server> serverList) {
count= serverList.size() ;
idx =0 ;
}
}
5、加权轮询方式
/**
* @author : wangchaodee
* @Description: 加权轮询方式的负载均衡
*/
public class WeightRoundRobinSelector extends WeightSelector {
/**
* 用轮询方式生成 当前cur 即可
* @return
*/
public int generateCur(){
cur =( cur +1 ) % this.getTotal() ;
return cur;
}
}
6、最小连接数方式
/**
* @author : wangchaodee
* @Description: 最小连接方式的负载均衡
*/
public class MiniConnectSelector implements Selector {
private Random random;
// 用优先队列维护服务器链接数量的排序
private PriorityQueue<Server> queue ;
public MiniConnectSelector() {
this.random = new Random();
this.queue = new PriorityQueue<>((a,b)-> a.getCurrentConnect()- b.getCurrentConnect());
}
public Random getRandom() {
return random;
}
@Override
public int generateIdx() {
Server server = queue.peek();
// 模拟间隔的刷新 服务器的当前连接数量
if(server.getCurrentConnect()==server.getMaxConnect()){
mockRefreshConnect();
}
// 取得最少连接 ,加1后 放回排序
server = queue.poll();
server.increaseCurrentConnect();
queue.offer(server);
// 这里用的是id , lb执行用的是list中的位置序号, 按理应该用map 映射
// 为了简化 在实际生成测试时 将位序直接赋值给了id
return server.getId();
}
@Override
public void registerServerList(List<Server> serverList) {
refreshQueue(serverList);
}
// 模拟
private void refreshQueue(List<Server> serverList){
queue.clear();
for (Server server : serverList){
server.refreshCurrentConnect(random);
queue.offer(server);
}
}
// 模拟
private void mockRefreshConnect(){
List<Server> serverList = new ArrayList<>();
while (!queue.isEmpty()){
serverList.add(queue.poll());
}
refreshQueue(serverList);
}
7、一致性哈希方式
一致性哈希需要取得Request 中信息进行哈希映射到服务器
/**
* @author : wangchaodee
* @Description: 一致性哈希方式的负载均衡
*/
public class ConsistentHashSelector implements HashSelector {
private static final int DEFAULT_VSERVER_NUM =5 ; //150
private int vServerNum ;
private int count ;
private SortedMap<Integer ,Server> vServers = new TreeMap<>();
@Override
public Server getByRequest(Request request) {
int hash = hash(request.getArea());
SortedMap<Integer,Server> subMap = vServers.tailMap(hash);
if(!subMap.isEmpty()){
return subMap.get(subMap.firstKey());
}
return vServers.get(vServers.firstKey());
}
@Override
public void registerServerList(List<Server> serverList) {
count= serverList.size() ;
for (Server server : serverList) {
for (int j = 0; j < vServerNum; j++) {
String vNodeName =server.getId() +"_vnode_" + j;
vServers.put(hash(vNodeName) , server);
}
}
}
private int hash(String name){
return Math.abs(name.hashCode()) % 1023;
}
}
8、测试对比
/**
* @author : wangchaodee
* @Description: 负载均衡的模拟测试
*/
public class LoadBalanceTest {
int serverNum = 5 ;
int requestNum = 15000 ;
/**
* 服务器处理能力一样
* @return
*/
private List<Server> generateServerList(){
List<Server> serverList = new ArrayList<>();
for (int i = 0; i < serverNum; i++) {
Server server = new Server(i);
// 服务器处理能力不一样
// Server server = new Server(i,100 * (i+1) );
serverList.add(server);
}
return serverList ;
}
/**
* 产生通用的空白请求 ,负载分发时不依赖请求中的信息
*/
private List<Request> generateBlankRequestList(){
// 执行请求
List<Request> requestList = new ArrayList<>();
Request base = new Request();
for (int i = 0; i < requestNum; i++) {
Request request = base.clone();
requestList.add(request);
}
return requestList ;
}
private void execute(LoadBalance loadBalance , List<Request> requestList){
for (int i = 0; i < requestList.size(); i++) {
loadBalance.handleRequest(requestList.get(i));
}
}
@Test
public void testRandomLb(){
LoadBalance loadBalance = new LoadBalance(new RandomSelector());
System.out.println("SelectorName :" + loadBalance.getSelector().getClass().getSimpleName());
loadBalance.registerServerList(generateServerList());
// 执行请求
execute(loadBalance,generateBlankRequestList());
for(Server server : loadBalance.getServerList()){
System.out.printf(" Server :%s , processed Request : %d \n " , server.hashCode() , server.getProcessed());
}
}
@Test
public void testConsistentHashLb(){
LoadBalance loadBalance = new LoadBalance(new ConsistentHashSelector());
System.out.println("SelectorName :" + loadBalance.getSelector().getClass().getSimpleName());
loadBalance.registerServerList(generateServerList());
// 执行请求
executeHash(loadBalance,generateRequestList());
for(Server server : loadBalance.getServerList()){
System.out.printf(" Server :%s , processed Request : %d \n " , server.hashCode() , server.getProcessed());
}
}
private void executeHash(LoadBalance loadBalance , List<Request> requestList){
for (int i = 0; i < requestList.size(); i++) {
loadBalance.handleRequestByHash(requestList.get(i));
}
}
// 其他测试用例 自由的组合替换 即可 代码略
模拟的测试结果仅供参考
SelectorName :RoundRobinSelector
Server :1330106945 , processed Request : 3000
Server :859417998 , processed Request : 3000
Server :5592464 , processed Request : 3000
Server :1830712962 , processed Request : 3000
Server :1112280004 , processed Request : 3000
SelectorName :WeightRoundRobinSelector
Server :104739310 maxConnect : 100, processed Request : 1000
Server :1761291320 maxConnect : 200, processed Request : 2000
Server :1451043227 maxConnect : 300, processed Request : 3000
Server :783286238 maxConnect : 400, processed Request : 4000
Server :1500056228 maxConnect : 500, processed Request : 5000
SelectorName :RandomSelector
Server :105704967 , processed Request : 2903
Server :392292416 , processed Request : 3013
Server :1818402158 , processed Request : 3127
Server :1590550415 , processed Request : 3000
Server :1058025095 , processed Request : 2957
SelectorName :WeightRandomSelector
Server :1876631416 maxConnect : 100, processed Request : 1021
Server :1359044626 maxConnect : 200, processed Request : 1941
Server :692342133 maxConnect : 300, processed Request : 2952
Server :578866604 maxConnect : 400, processed Request : 3972
Server :353842779 maxConnect : 500, processed Request : 5114
SelectorName :MiniConnectSelector
Server :1830712962 , processed Request : 2845
Server :104739310 , processed Request : 3204
Server :1761291320 , processed Request : 2820
Server :1451043227 , processed Request : 3003
Server :783286238 , processed Request : 3128
SelectorName :ConsistentHashSelector
Server :1595212853 , processed Request : 2130
Server :1911728085 , processed Request : 4004
Server :754666084 , processed Request : 6643
Server :88558700 , processed Request : 464
Server :1265210847 , processed Request : 1758