相关文章:
- 自己动手写分布式任务调度框架
- 用 Java 代码实现负载均衡的五种常见算法
- 手写实现RPC框架(带注册中心)
- 本文完整代码地址:https://gitee.com/dongguabai/blog/tree/master/loadbalance
前段时间,我们有一台定时任务执行机器(总共有 3 台机器负责处理定时任务)出现内存使用率超过 92% 的告警。在分布式任务调度中心,通常会有一套负载均衡策略来选择执行当前任务的机器。
这里就有一个思考点:如果一台机器已经超负载或即将超负载,是否可以通过减少其任务分配来降低其在负载均衡中的权重,以实现更好的负载均衡。即基于机器负载情况的动态权重负载均衡,即负载最低优先。
前面几种算法主要是站在负载均衡服务的角度,保证每个机器节点获得的调用次数均衡或者相对均衡,但是实际生产环境调用次数相同并不一定真的能够让每台机器真的实现均衡,于是就有了负载最低优先的策略,负载最低优先可以使用如最小连接数、CPU/IO 负载,这里引入《从零开始学架构》中的介绍:
负载最低优先算法基本上能够比较完美地解决轮询算法的缺点,因为采用这种算法后,负载均衡系统需要感知服务器当前的运行状态。当然,其代价是复杂度大幅上升。通俗来讲,轮询可能是 5 行代码就能实现的算法,而负载最低优先算法可能要 1000 行才能实现,甚至需要负载均衡系统和服务器都要开发代码。负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。
用 Java 代码实现负载均衡的五种常见算法
要实现这一套逻辑,有一个关键点,即如何获取机器负载情况,在 Java 层面可以基于 JMX 来做。JMX 全称为 Java Management Extensions(Java管理扩展),是一种 Java 平台上用于监控和管理应用程序、系统和网络服务的技术和标准。它提供了一种标准化的方式来管理 Java 应用程序的运行时状态、性能以及配置信息。在 java.lang.management
下可以看到很多标准化的接口,如:
java.lang.management.OperatingSystemMXBean
java.lang.management.MemoryPoolMXBean
随机
这里基于很早之前的一篇博客手写实现RPC框架(带注册中心)来实现负载最低优先的负载均衡,之前是基于随机负载来做的,可以验证一下,使用 12345、12346、12347 三个端口号分别启动三个 Server:
/**
* @author Dongguabai
* @date 2018/11/1 18:07
*/
public class ServerDemo {
public static void main(String[] args) {
//之前发布服务
/*
RpcServer rpcServer = new RpcServer();
rpcServer.publisher(new HelloServiceImpl(),12345);
*/
//改造后
IRegistryCenter registryCenter = new RegistryCenterImpl();
//这里为了方便,获取ip地址就直接写了
RpcServer rpcServer = new RpcServer(registryCenter,"127.0.0.1:12345");
//绑定服务
rpcServer.bind(new HelloServiceImpl(12345));
rpcServer.publisher();
}
}
相关日志:
09:59:32.378 [main] INFO blog.dongguabai.lb.core.registry.RegistryCenterImpl - 服务注册成功:/rpcNode/blog.dongguabai.lb.core.demo.IHelloService/127.0.0.1:12347
09:59:32.380 [main] INFO blog.dongguabai.lb.core.RpcServer - 注册服务成功:【serviceName:blog.dongguabai.lb.core.demo.IHelloService,address:127.0.0.1:12347】
可以在 ZK 上看一下:
[zk: 127.0.0.1(CONNECTED) 1] ls /rpcNode/blog.dongguabai.lb.core.demo.IHelloService
[127.0.0.1:12346, 127.0.0.1:12345, 127.0.0.1:12347]
[zk: 127.0.0.1(CONNECTED) 0] get /rpcNode/blog.dongguabai.lb.core.demo.IHelloService/127.0.0.1:12345
0
cZxid = 0x600000005
ctime = Thu Sep 21 18:58:39 PDT 2023
mZxid = 0x600000005
mtime = Thu Sep 21 18:58:39 PDT 2023
pZxid = 0x600000005
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100151f82810000
dataLength = 1
numChildren = 0
能够看到三个服务都已经注册上去了。
客户端调用,看一下随机负载的效果:
/**
* @author Dongguabai
* @date 2018/11/1 18:10
*/
public class ClientDemo {
public static void main(String[] args) {
/*RpcClientProxy proxy = new RpcClientProxy();
IHelloService helloService = proxy.clientProxy(IHelloService.class, "127.0.0.1", 12345);
String name = helloService.sayHello("张三");
System.out.println(name);*/
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
IServiceDiscovery serviceDiscovery = new ServiceDiscoveryImpl(RegistryCenterConfig.CONNECTING_STR);
RpcClientProxy proxy = new RpcClientProxy(serviceDiscovery);
IHelloService service = proxy.clientProxy(IHelloService.class);
System.out.println(service.sayHello("张三"));
}
}
}
输出:
[12347]-你好,张三
[12347]-你好,张三
[12346]-你好,张三
[12346]-你好,张三
[12345]-你好,张三
[12345]-你好,张三
[12346]-你好,张三
[12347]-你好,张三
[12347]-你好,张三
[12345]-你好,张三
[12346]-你好,张三
[12346]-你好,张三
[12346]-你好,张三
[12347]-你好,张三
[12345]-你好,张三
[12347]-你好,张三
[12346]-你好,张三
[12347]-你好,张三
[12345]-你好,张三
[12347]-你好,张三
[12346]-你好,张三
[12345]-你好,张三
[12346]-你好,张三
[12346]-你好,张三
[12345]-你好,张三
[12347]-你好,张三
[12345]-你好,张三
[12346]-你好,张三
[12347]-你好,张三
[12347]-你好,张三
可以看到每个服务接收到的请求比较均匀。
服务发现的代码:
@Override
public String discover(String serviceName) {
//获取/rpcNode/dgb.nospring.myrpc.demo.IHelloService下所有协议地址
String nodePath = RegistryCenterConfig.NAMESPACE+"/"+serviceName;
try {
repos = curatorFramework.getChildren().forPath(nodePath);
} catch (Exception e) {
throw new RuntimeException("服务发现获取子节点异常!",e);
}
//动态发现服务节点变化,需要注册监听
registerWatcher(nodePath);
//这里为了方便,直接使用随机负载
LoadBalance loadBalance = new RandomLoadBanalce();
return loadBalance.selectHost(repos);
}
随机代码:
/**
* 随机负载算法
* @author Dongguabai
* @date 2018/11/2 10:17
*/
public class RandomLoadBanalce extends AbstractLoadBanance{
@Override
protected String doSelect(List<String> repos) {
return repos.get(new Random().nextInt(repos.size()));
}
}
随机改造负载最低优先
先梳理一下整个调用流程:
- Server 将自身信息(调用地址)注册到 ZK
- Client 从 ZK 中获取 Servers 信息
- 随机请求一个 Server
如果要改造成负载最低优先的负载均衡,那么 Server 需要定时将自身服务的情况上报给 ZK,Client 基于 ZK 中各个 Server 的服务情况选择相应的 Sever 进行调用。
这里简单点处理,Server 元数据直接放在 ZK 中。
首先定义 Invoker
,即每个服务执行器:
/**
* @author dongguabai
* @date 2023-09-22 13:59
*/
@NoArgsConstructor
@Getter
@Setter
@ToString
@EqualsAndHashCode(of = "address")
@AllArgsConstructor
public class Invoker {
private String address;
/**
* 权重
*/
private int weight = 10;
}
这里权重默认是 10,权重会根据服务的 CPU 情况进行调整:
@Override
public Invoker discover(String serviceName) {
//获取/rpcNode/dgb.nospring.myrpc.demo.IHelloService下所有协议地址
String nodePath = RegistryCenterConfig.NAMESPACE + "/" + serviceName;
List<Invoker> invokers = new ArrayList<>();
try {
repos = curatorFramework.getChildren().forPath(nodePath);
for (String child : repos) {
String childPath = nodePath + "/" + child;
byte[] data = curatorFramework.getData().forPath(childPath);
String dataStr = new String(data, StandardCharsets.UTF_8);
//CPU > 10,权重降低为1,否则为10
invokers.add(new Invoker(child, Integer.valueOf(dataStr) > 10 ? 1 : 10));
}
} catch (Exception e) {
throw new RuntimeException("服务发现获取子节点异常!", e);
}
//动态发现服务节点变化,需要注册监听
registerWatcher(nodePath);
//这里为了方便,直接使用随机负载
LoadBalance loadBalance = new SectionWeightRandomLoadBalance();
return loadBalance.selectHost(invokers);
}
每个 Server 会定时将自身的 CPU 负载情况进行上报:
private static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private void refreshMetadata(String addressPath) {
int port = Integer.parseInt(addressPath.split(":")[1]);
SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
try {
int processCpuLoad = getProcessCpuLoad();
curatorFramework.setData().forPath(addressPath, String.valueOf(processCpuLoad).getBytes());
log.info("[{}] refresh cpu : {}", port, processCpuLoad);
} catch (Exception e) {
log.error("refreshMetadata error.", e);
}
}, 5, 2, TimeUnit.SECONDS);
//模拟
highCpuUsage(port);
}
private static final OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private static int getProcessCpuLoad() {
//获取CPU Load
double cpuLoad = OPERATING_SYSTEM_MX_BEAN.getProcessCpuLoad();
return (int) (cpuLoad * 100);
}
基于负载情况进行加权随机处理:
/**
* @author dongguabai
* @date 2023-09-22 14:20
*/
public class SectionWeightRandomLoadBalance extends RandomLoadBanalce {
@Override
protected Invoker doSelect(List<Invoker> invokers) {
boolean averageWeight = true;
int totalWeight = 0;
for (int i = 0; i < invokers.size(); i++) {
Invoker invoker = invokers.get(i);
if (averageWeight && i > 0 && invoker.getWeight() != invokers.get(i - 1).getWeight()) {
averageWeight = false;
}
totalWeight += invoker.getWeight();
}
if (averageWeight || totalWeight < 1) {
return super.doSelect(invokers);
}
int index = new Random().nextInt(totalWeight);
for (Invoker invoker : invokers) {
if (index < invoker.getWeight()) {
return invoker;
}
index -= invoker.getWeight();
}
return super.doSelect(invokers);
}
}
端口号为 12345 的 Server 会有一个负载的变化,启动 3s 后 CPU 会逐步升高,持续 30s,然后逐步下降:
public void highCpuUsage(int port) {
//端口为12345的服务才进行模拟
if (port != 12345) {
return;
}
try {
//延迟3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException ignored) {
}
log.info("...........start highCpuUsage");
//执行20s
Thread cpuThread = new Thread(() -> call(), "highCpuUsage-thread");
cpuThread.start();
try {
cpuThread.join();
log.info("...........end highCpuUsage");
} catch (InterruptedException ignored) {
}
}
private void call() {
long startTime = System.currentTimeMillis();
long duration = 30000;
while (System.currentTimeMillis() - startTime < duration) {
// 空的计算任务,消耗CPU资源
double result = Math.random() * Math.random();
}
}
代码修改完成后使用 12345、12346、12347 三个端口号分别启动三个 Server,这是端口号为 12345 Server 的日志输出情况:
17:15:16.974 [main] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - 服务注册成功:/rpcNode/blog.dongguabai.lb.example.server.IHelloService/127.0.0.1:12345
17:15:19.987 [main] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - ...........start highCpuUsage
17:15:21.993 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 0
17:15:24.001 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:26.007 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:28.015 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:30.024 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:32.031 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:34.039 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:36.046 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:38.055 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:40.063 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:42.067 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:44.074 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:46.081 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 12
17:15:48.088 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 10
17:15:49.989 [main] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - ...........end highCpuUsage
17:15:50.096 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 10
17:15:52.105 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 0
17:15:54.113 [pool-1-thread-1] INFO b.dongguabai.lb.core.registry.RegistryCenterImpl - [12345] refresh cpu : 0
可以看到在 17:15:24.001~17:15:46.081 期间 12345 Server 的 CPU Load 是大于 10 的。
看一下执行效果:
//开始执行
2023-9-22 17:15:19 12347
2023-9-22 17:15:20 12346
2023-9-22 17:15:21 12345
2023-9-22 17:15:22 12347
2023-9-22 17:15:23 12346
//12345 CPU 激增
2023-9-22 17:15:24 12347
2023-9-22 17:15:25 12346
2023-9-22 17:15:26 12347
2023-9-22 17:15:27 12347
2023-9-22 17:15:28 12346
2023-9-22 17:15:29 12347
2023-9-22 17:15:30 12347
2023-9-22 17:15:31 12346
2023-9-22 17:15:32 12347
2023-9-22 17:15:33 12347
2023-9-22 17:15:34 12346
2023-9-22 17:15:35 12346
2023-9-22 17:15:36 12346
2023-9-22 17:15:37 12347
2023-9-22 17:15:38 12346
2023-9-22 17:15:39 12346
2023-9-22 17:15:40 12346
2023-9-22 17:15:41 12347
2023-9-22 17:15:42 12346
2023-9-22 17:15:43 12346
2023-9-22 17:15:44 12346
2023-9-22 17:15:45 12347
2023-9-22 17:15:46 12347
2023-9-22 17:15:47 12347
2023-9-22 17:15:48 12345
2023-9-22 17:15:49 12347
2023-9-22 17:15:50 12345
//12345 CPU 恢复
2023-9-22 17:15:51 12347
2023-9-22 17:15:52 12345
2023-9-22 17:15:53 12346
2023-9-22 17:15:54 12347
2023-9-22 17:15:55 12347
2023-9-22 17:15:56 12347
2023-9-22 17:15:57 12347
2023-9-22 17:15:58 12345
2023-9-22 17:15:59 12346
2023-9-22 17:16:00 12347
2023-9-22 17:16:01 12345
2023-9-22 17:16:02 12345
2023-9-22 17:16:04 12347
2023-9-22 17:16:05 12347
2023-9-22 17:16:06 12346
2023-9-22 17:16:07 12345
2023-9-22 17:16:08 12345
2023-9-22 17:16:09 12346
2023-9-22 17:16:10 12345
2023-9-22 17:16:11 12346
2023-9-22 17:16:12 12347
2023-9-22 17:16:13 12346
2023-9-22 17:16:14 12346
2023-9-22 17:16:15 12346
2023-9-22 17:16:16 12346
2023-9-22 17:16:17 12345
2023-9-22 17:16:18 12347
2023-9-22 17:16:19 12346
2023-9-22 17:16:20 12346
2023-9-22 17:16:21 12346
2023-9-22 17:16:22 12345
2023-9-22 17:16:23 12346
2023-9-22 17:16:24 12345
2023-9-22 17:16:25 12346
2023-9-22 17:16:26 12345
2023-9-22 17:16:27 12346
2023-9-22 17:16:28 12347
2023-9-22 17:16:29 12345
2023-9-22 17:16:30 12345
2023-9-22 17:16:31 12347
2023-9-22 17:16:32 12347
2023-9-22 17:16:33 12347
2023-9-22 17:16:34 12347
2023-9-22 17:16:35 12345
2023-9-22 17:16:36 12346
2023-9-22 17:16:37 12347
2023-9-22 17:16:38 12346
2023-9-22 17:16:39 12346
2023-9-22 17:16:40 12345
2023-9-22 17:16:41 12345
2023-9-22 17:16:42 12345
2023-9-22 17:16:43 12347
2023-9-22 17:16:44 12347
2023-9-22 17:16:45 12345
2023-9-22 17:16:46 12347
2023-9-22 17:16:47 12347
2023-9-22 17:16:48 12347
2023-9-22 17:16:49 12345
2023-9-22 17:16:50 12346
2023-9-22 17:16:51 12346
2023-9-22 17:16:52 12346
2023-9-22 17:16:53 12345
2023-9-22 17:16:54 12346
2023-9-22 17:16:55 12345
2023-9-22 17:16:56 12345
2023-9-22 17:16:57 12345
2023-9-22 17:16:58 12346
2023-9-22 17:17:00 12345
2023-9-22 17:17:01 12347
2023-9-22 17:17:02 12347
2023-9-22 17:17:03 12345
2023-9-22 17:17:04 12345
2023-9-22 17:17:05 12347
2023-9-22 17:17:06 12347
2023-9-22 17:17:08 12345
2023-9-22 17:17:09 12346
2023-9-22 17:17:10 12347
2023-9-22 17:17:12 12347
2023-9-22 17:17:13 12346
2023-9-22 17:17:15 12346
2023-9-22 17:17:16 12347
2023-9-22 17:17:17 12346
2023-9-22 17:17:18 12346
2023-9-22 17:17:19 12347
2023-9-22 17:17:20 12346
这里将执行比例做成一个表格,便于查看:
Server Port | 12345高CPU Load期间执行次数 | 1234CPU Load恢复后执行次数 |
---|---|---|
12345 | 2 | 28 |
12346 | 12 | 31 |
12347 | 13 | 31 |
总调用次数 | 27 | 90 |
当 Server 12345 的CPU负载高时,我们可以明显观察到其他服务的调用量明显减少。而当 Server 12345 的CPU负载逐渐恢复正常时,各个服务的调用量也逐步趋于均衡。至此,成功实现了负载最低优先的负载均衡策略。
总结
“负载最低优先” 的负载均衡策略在分布式系统中非常常见,目的是将负载较低的服务器优先分配请求,以确保资源的最佳利用。负载最低优先可以使用如最小连接数、CPU/IO 负载等。
但也要注意的是这种策略会极大增加系统的复杂度,这里引入《从零开始学架构》中的介绍:
负载最低优先算法基本上能够比较完美地解决轮询算法的缺点,因为采用这种算法后,负载均衡系统需要感知服务器当前的运行状态。当然,其代价是复杂度大幅上升。通俗来讲,轮询可能是 5 行代码就能实现的算法,而负载最低优先算法可能要 1000 行才能实现,甚至需要负载均衡系统和服务器都要开发代码。负载最低优先算法如果本身没有设计好,或者不适合业务的运行特点,算法本身就可能成为性能的瓶颈,或者引发很多莫名其妙的问题。所以负载最低优先算法虽然效果看起来很美好,但实际上真正应用的场景反而没有轮询(包括加权轮询)那么多。
References
- 用 Java 代码实现负载均衡的五种常见算法
- 手写实现RPC框架(带注册中心)
- 《从零开始学架构》
欢迎关注公众号: