nacos+LoadBalancer自定义负载均衡算法,实现基于cpu负载的动态权重

本博客采用的技术方案纯属学生突发奇想折腾出来玩,若用作参考请仔细斟酌

本博客的代码建立在spring cloud alibaba 2022.0.0.0-RC1架构之上

一、背景说明

spring cloud loadbalancer是spring cloud推荐的负载均衡器,但我们观察源码可以发现,它只实现了两种负载均衡算法,分别是轮询和随机。我在项目开发的过程中,对现有的轮询算法不太满意,于是希望自己编写一个算法满足项目的需要,比如根据cpu负载来动态更新权重,并根据权重来负载均衡。

所幸nacos注册中心使我们能够很方便的实现这一需求,我们只需要让服务器节点定时向nacos更新自己的cpu负载,就能在负载均衡算法中从nacos获取可用服务器节点的cpu负载。

二、获取cpu占用

com.sun.management包含了一些对java.lang.management包的扩展,利用这个包下的OperatingSystemMXBean对象可以获取当前进程所处的jvm的cpu占用。

java 复制代码
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;

public class CpuMonitor {
    public static double getCpuUsage() {
        OperatingSystemMXBean osBean =
                (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        return osBean.getCpuLoad() * 100; // 返回百分比
    }
}

getCpuLoad方法会返回一个0到1的浮点数,表示cpu负载。

三、定时向nacos更新cpu负载信息

我们需要让应用实例定时告诉nacos自己当前的cpu负载,这里通过juc包的ScheduledExecutorService类对象来开启一个定时任务。

ini 复制代码
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.personal.train.member.monitor.CpuMonitor;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

  @Autowired
	private NacosDiscoveryProperties discoveryProperties;
	@Autowired
	private NacosServiceManager nacosServiceManager;

	@PostConstruct
	public void init() {
		// 每5秒更新一次CPU数据到Nacos元数据
		ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
		scheduler.scheduleAtFixedRate(() -> {
			double cpuUsage = CpuMonitor.getCpuUsage();

			Instance instance = new Instance();
			instance.setIp(discoveryProperties.getIp());
			instance.setPort(discoveryProperties.getPort());
			instance.setServiceName(discoveryProperties.getService());
			instance.setClusterName(discoveryProperties.getClusterName());
			instance.setMetadata(discoveryProperties.getMetadata());
			instance.setEphemeral(discoveryProperties.isEphemeral());
			instance.setWeight(discoveryProperties.getWeight());
			instance.getMetadata().put("cpu_usage", String.format("%.2f", cpuUsage));
			try {
				nacosServiceManager.getNamingService().registerInstance(instance.getServiceName(), instance);
			} catch (NacosException e) {
				e.printStackTrace();
			}
		}, 0, 5, TimeUnit.SECONDS);
	}

这段代码编写在应用的启动类中,@PostConstruct注解将使得spring在依赖注入完毕之后,应用启动之前运行init方法。

开启两个应用测试一下,看看cpu负载元数据是否成功传到nacos上了,是否每5秒正常刷新cpu负载。

四、编写负载均衡算法

我们在gateway模块中新增一个CpuWeightLoadBalancer类实现ReactorServiceInstanceLoadBalancer接口,然后让loadbalancer使用我们这个负载均衡算法即可。仿照RoundRobinLoadBalancer类的实现,我们需要实现choose方法,这个方法需要返回一个Mono<Response < ServiceInstance > >对象。这里的ServiceInstance就是我们选择的应用实例。

我们首先编写选择逻辑:我们的最终目的是给予cpu占用率较低的实例更大的权重,并让其有更大的概率被选中。

那么,我们设置权重 = 100 - cpu占用率。然后我们要求实现每个实例被选中的概率 = 权重 / 所有可用实例的总权重。要实现这样的选择逻辑,想象每个实例的权重都是一条长度为权重的线段,然后我们把每个实例的线段都拼在一起,组成一条长为所有可用实例的总权重的线段,然后我们每次选择的时候就在线段上随机选一个点,这个点落在哪个实例的线段上,就选择哪个线段。这样,点落在某个实例的概率就是这个实例的线段长度 / 总长度,也就是我们所期望的概率(权重 / 所有可用实例的总权重)。

以上算法的实现如下。

scss 复制代码
private ServiceInstance selectInstance(List<ServiceInstance> instances) {
    // 计算权重总和
    double totalWeight = instances.stream()
            .mapToDouble(instance -> 100 - Double.parseDouble(instance.getMetadata().getOrDefault("cpu_usage", "50")))
            .sum();

    // 随机选择线段上的一个点
    double random = Math.random() * totalWeight;
    for (ServiceInstance instance : instances) {
        double weight = 100 - Double.parseDouble(instance.getMetadata().getOrDefault("cpu_usage", "50"));
        random -= weight;
        if (random <= 0) {
            return instance;
        }
    }
    return instances.get(0);
}

在上面这个实现中,判断点落在了哪个实例的线段上的逻辑被转化成了遍历可用实例并用random(也就是点的坐标)减去这个实例的权重,若random小于等于0则选择当前遍历到的实例,这个逻辑转化是可行的。

最终我们的自定义负载均衡算法类代码如下

ini 复制代码
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;

public class CpuWeightLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
    private final String serviceId;

    public CpuWeightLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> supplierProvider, String serviceId) {
        this.supplierProvider = supplierProvider;
        this.serviceId = serviceId;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = supplierProvider.getIfAvailable();
        return supplier.get().next().map(instances -> {
            if (instances.isEmpty()) return new EmptyResponse();
            ServiceInstance instance = selectInstance(instances);
            return new DefaultResponse(instance);
        });
    }

    private ServiceInstance selectInstance(List<ServiceInstance> instances) {
        // 计算权重总和
        double totalWeight = instances.stream()
                .mapToDouble(instance -> 100 - Double.parseDouble(instance.getMetadata().getOrDefault("cpu_usage", "50")))
                .sum();

        // 加权随机选择
        double random = Math.random() * totalWeight;
        for (ServiceInstance instance : instances) {
            double weight = 100 - Double.parseDouble(instance.getMetadata().getOrDefault("cpu_usage", "50"));
            random -= weight;
            if (random <= 0) {
                return instance;
            }
        }
        return instances.get(0);
    }
}

五、将自定义的负载均衡算法配置为要使用的算法

首先写一个配置类,用来返回自定义的负载均衡算法实例

kotlin 复制代码
package com.personal.train.gateway.config;

import com.personal.train.gateway.balancer.CpuWeightLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class LoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(
            Environment env, LoadBalancerClientFactory factory) {
        String serviceId = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new CpuWeightLoadBalancer(factory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
    }
}

然后在网关的启动类上增加一个注解

python 复制代码
@LoadBalancerClient(value = "",configuration = LoadBalancerConfig.class)

这个注解用来指定一个负载均衡客户端,作用是对于要转发至value的请求,使用configuration负载均衡配置类。

六、使用Jmeter测试负载均衡效果

为了便于测试,我把每5秒更新一次cpu负载改为1000秒,然后通过nacos控制台手动修改cpu负载元数据

然后通过Jmeter开启100个线程访问目标服务,看看每个实例处理的请求数。

可以看到cpu负载为50的实例接收到了38个请求,cpu负载为5的实例接收到了62个请求,这个结果与我们自定义的负载均衡算法一致。

七、总结

总的来说,我们成功通过自定义负载均衡算法避开了loadbalancer自带算法较少较简陋的缺点。不过这次尝试也比较简陋,还有不少需要考虑的地方。比如增加其他监控值(如内存占用)来作为动态权重的因子,使用滑动窗口算法来避免负载均衡的不平滑,以及在实际运行中自定义负载均衡算法是否能比默认的轮询算法更好地处理高并发,如果能,具体的效果或者说收益有多少,这些是这次尝试没有涉及的。

相关推荐
艾文伯特几秒前
Maven集成模块打包&使用
java·maven
碎梦归途22 分钟前
23种设计模式-创建型模式之原型模式(Java版本)
java·开发语言·jvm·设计模式·原型模式
自带五拨片35 分钟前
XHTMLConverter把docx转换html报java.lang.NullPointerException异常
java·html
doglc1 小时前
Java 动态代理教程(JDK 动态代理)(以RPC 过程为例)
java·rpc·动态代理·jdk动态代理
向哆哆1 小时前
Java 性能优化:如何利用 APM 工具提升系统性能?
java·python·性能优化
一一Null1 小时前
Android studio—socketIO库return与emit的使用
android·java·网络·ide·websocket·网络协议·android studio
魔道不误砍柴功2 小时前
《理解 Java 泛型中的通配符:extends 与 super 的使用场景》
java·windows·python
Joseit2 小时前
基于 Spring Boot实现的图书管理系统
java·spring boot·后端
{⌐■_■}2 小时前
【go】什么是Go语言的GPM模型?工作流程?为什么Go语言中的GMP模型需要有P?
java·开发语言·后端·golang