So easy! 教你实现自定义负载均衡策略!

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
简介: So easy! 教你实现自定义负载均衡策略!

前言

Feign 是⼀个 HTTP 请求的轻量级客户端框架。通过接口和注解的方式发起 HTTP 请求调用,面向接口编程,而不是像 Java 中通过封装 HTTP 请求报文的方式直接调用。服务消费方拿到服务提供方的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。

负载均衡是微服务架构中必须使用的技术,通过负载均衡来实现系统的高可用、集群扩容等功能。负载均衡可通过硬件设备及软件来实现,硬件比如:F5、Array 等,软件比如:LVS、Nginx 等。常用的负载均衡算法有:轮训、随机、加权轮训、加权随机、地址哈希等方法,负载均衡器维护一份服务列表,根据负载均衡算法将请求转发到相应的微服务上,所以负载均衡可以为微服务集群分担请求,降低系统的压力。

Feign 在调用 Http 接口时,需要先通过负载均衡策略获取一个服务实例后才能去调用微服务。在实践中,经常会遇到需要自定义负载均衡策略的场景,比如在本机开发调试时,需要优先调用本机或局域网内的微服务。

实现原理

Feign 通过动态代理,实现调用接口的方式发起 HTTP 请求调用,其中的核心方法见下图所示:

RetryableFeignBlockingLoadBalancerClient 内 execute 方法里,调用 BlockingLoadBalancerClient 类的 choose 方法获取服务实例,

该方逻辑是先根据 serviceId,即服务名获取对应的 ReactiveLoadBalancer 反应式负载均衡器,反应式负载均衡器根据请求信息 request, 阻塞获取最终的服务实例。

以 RoundRobinLoadBalancer 轮询负载均衡器为例,该类实现了 ReactiveLoadBalancer 接口,在 choose 方法,先获取 ServiceInstanceListSupplier 对象,该对象是服务列表的提供者,通过该对象获取过滤后的服务列表,然后对列表轮询计算选取最终的服务实例。

详细实现

根据上述原理,通过自定义实现 ServiceInstanceListSupplier 对象以及负载均衡器可以实现自定义策略。下面为具体的代码实现:

  1. EnableCustomLoadBalance.java 自定义开启负载均衡策略注解,在启动类 Application 类上添加即可开启。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableCustomLoadBalanceImportSelector.class})
public @interface EnableCustomLoadBalance {
    String rule() default "local-first";
}

rule 即要实现的策略规则,实现多种策略后,可以通过 rule 指定策略。

  1. EnableCustomLoadBalanceImportSelector.java

EnableCustomLoadBalanceImportSelector 可以根据当前运行环境,判断是否加载自定义负载均衡相关配置。比如本机优先策略只在生成环境生效,其它环境不会加载和生效相关配置。

@Order
public class EnableCustomLoadBalanceImportSelector extends SpringFactoryImportSelector<EnableCustomLoadBalance> {
    public EnableCustomLoadBalanceImportSelector() {
    }
    public String[] selectImports(AnnotationMetadata metadata) {
        Environment env = this.getEnvironment();
        // 从配置项com.xx.loadbalancer.ignoreNamespaces 获取忽略生效的nacos命名空间 例如:test, prod
        String namespaces = env.getProperty("com.xx.loadbalancer.ignoreNamespaces", "test,staging,prod");
        List<String> namespaceList = Arrays.asList(StringUtils.trimAllWhitespace(namespaces).split("[,]+"));
        List<String> imports = new ArrayList<>();
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(this.getAnnotationClass().getName(), true));
        // 测试、正式等非开发环境不支持自定义策略
        if (env instanceof ConfigurableEnvironment) {
            ConfigurableEnvironment configEnv = (ConfigurableEnvironment) env;
            String property = configEnv.getProperty("spring.cloud.nacos.discovery.namespace");
            if (!StringUtils.hasText(property) || namespaceList.contains(property)) {
                return new String[0];
            }
            if (Objects.nonNull(attributes)) {
                String rule = attributes.getString("rule");
                if (StringUtils.hasText(rule)) {
                    imports.add("com.xx.basic.springboot.starter.component.loadbalancer.LoadBalanceAutoConfiguration");
                    LinkedHashMap<String, Object> map = new LinkedHashMap();
                    map.put("spring.cloud.loadbalancer.rule", rule);
                    MapPropertySource propertySource = new MapPropertySource("springCloudLoadbalancer", map);
                    configEnv.getPropertySources().addLast(propertySource);
                }
            }
        }
        return imports.toArray(new String[0]);
    }
    @Override
    protected boolean isEnabled() {
        return false;
    }
    @Override
    protected boolean hasDefaultFactory() {
        return true;
    }
}
  1. LoadBalanceAutoConfiguration.java

用于自定义负载均衡自动配置生效。

@ConditionalOnClass(LoadBalancerClients.class)
@LoadBalancerClients(defaultConfiguration = LoadBalanceConfig.class)
@AutoConfigureBefore({ReactorLoadBalancerClientAutoConfiguration.class,
        LoadBalancerBeanPostProcessorAutoConfiguration.class})
public class LoadBalanceAutoConfiguration {
}

自动加载 LoadBalanceConfig 配置类,并限制在 ReactorLoadBalancerClientAutoConfiguration 和 LoadBalancerBeanPostProcessorAutoConfiguration 生效后开始配置。

  1. LoadBalanceConfig.java

实例化自定义提供服务列表对象 ServiceInstanceListSupplier,通过@ConditionalOnProperty 注解现在只在 rule 为 local-first 时实例化。自定义 RoundRobinLoadBalancer 负载均衡器,指定为默认负载均衡器。

public class LoadBalanceConfig {
    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.rule", havingValue = "local-first")
    public ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) {
        ServiceInstanceListSupplier supplier = ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withCaching().build(context);
        return new LocalServiceInstanceListSupplier(supplier);
    }
    @Bean
    @ConditionalOnProperty(
            value = {"spring.cloud.loadbalancer.rule"}, matchIfMissing = false)
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, ServiceInstanceListSupplier serviceInstanceListSupplier) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RoundRobinLoadBalancer(new SimpleObjectProvider<>(serviceInstanceListSupplier), name);
    }
}
  1. LocalServiceInstanceListSupplier.java

自定义 ServiceInstanceListSupplier 接口实现,

@Slf4j
public class LocalServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    private InetUtils inetUtils;
    public LocalServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
        super(delegate);
        inetUtils = new InetUtils(new InetUtilsProperties());
    }
    @Override
    public Flux<List<ServiceInstance>> get() {
        return getDelegate().get().map(this::filterByLocalIp);
    }
    private List<ServiceInstance> filterByLocalIp(List<ServiceInstance> instances) {
        InetAddress host = inetUtils.findFirstNonLoopbackAddress();
        if (host == null) {
            return instances;
        }
        String resourceIp = host.getHostAddress();
        List<ServiceInstance> targetList = Lists.newArrayList();
        for (ServiceInstance instance : instances) {
            if (resourceIp.equals(instance.getHost())) {
                targetList.add(instance);
            }
        }
        if (CollectionUtils.isEmpty(targetList)) {
            return instances;
        }
        return targetList;
    }
}

通过判断服务实例的 ip 是否与本机一致,筛选服务实例列表,提供给轮询负载均衡器 RoundRobinLoadBalancer。

  1. NacosAutoConfiguration

为了实现更灵活更负载的负载均衡策略,可以将服务实例的更多信息注册到 metadata 元数据中,这样 DelegatingServiceInstanceListSupplier 可以通过获取服务实例的 metadata 信息制定策略。本文以 nacos 为例,在服务启动时将启动时间和本机 hostname 注册到 metadata。

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@Slf4j
@AutoConfigureBefore({NacosDiscoveryAutoConfiguration.class})
public class NacosAutoConfiguration {
    public NacosAutoConfiguration() {
    }
    @Bean
    public NacosDiscoveryProperties myNacosProperties() {
        NacosDiscoveryProperties nacosDiscoveryProperties = new NacosDiscoveryProperties();
        nacosDiscoveryProperties.getMetadata().put("startup.time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        try (InetUtils inetUtils = new InetUtils(new InetUtilsProperties())) {
            InetUtils.HostInfo host = inetUtils.findFirstNonLoopbackHostInfo();
            nacosDiscoveryProperties.getMetadata().put("hostname", Optional.ofNullable(host).map(InetUtils.HostInfo::getHostname).orElse(""));
        }
        return nacosDiscoveryProperties;
    }
}

小结

通过上面原理讲解和实现代码,可以实现灵活的自定义负载均衡策略。

关键在于理解 Feign 的工作原理,以及 SpringCloud 是如何提供上层抽象的服务注册发现编程模型,屏蔽不同底层框架的具体实现。

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
2月前
|
负载均衡
slb自定义健康检查路径
slb自定义健康检查路径
28 3
|
29天前
|
负载均衡 Java Nacos
常见的Ribbon/Spring LoadBalancer的负载均衡策略
自SpringCloud 2020版起,Ribbon被弃用,转而使用Spring Cloud LoadBalancer。Ribbon支持轮询、随机、加权响应时间和重试等负载均衡策略;而Spring Cloud LoadBalancer则提供轮询、随机及Nacos负载均衡策略,基于Reactor实现,更高效灵活。
75 0
|
3月前
|
负载均衡 应用服务中间件 nginx
Nginx的6大负载均衡策略及权重轮询手写配置
【10月更文挑战第9天】 Nginx是一款高性能的HTTP服务器和反向代理服务器,它在处理大量并发请求时表现出色。Nginx的负载均衡功能可以将请求分发到多个服务器,提高网站的吞吐量和可靠性。以下是Nginx支持的6大负载均衡策略:
303 7
|
3月前
|
负载均衡 算法 Java
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
尼恩,一位资深架构师,分享了关于负载均衡及其策略的深入解析,特别是基于权重的负载均衡策略。文章不仅介绍了Nginx的五大负载均衡策略,如轮询、加权轮询、IP哈希、最少连接数等,还提供了手写加权轮询算法的Java实现示例。通过这些内容,尼恩帮助读者系统化理解负载均衡技术,提升面试竞争力,实现技术上的“肌肉展示”。此外,他还提供了丰富的技术资料和面试指导,助力求职者在大厂面试中脱颖而出。
腾讯面试:说说6大Nginx负载均衡?手写一下权重轮询策略?
|
4月前
|
负载均衡 Java 对象存储
负载均衡策略:Spring Cloud与Netflix OSS的最佳实践
负载均衡策略:Spring Cloud与Netflix OSS的最佳实践
62 2
|
6月前
|
负载均衡 算法 应用服务中间件
nginx自定义负载均衡及根据cpu运行自定义负载均衡
nginx自定义负载均衡及根据cpu运行自定义负载均衡
113 1
|
5月前
|
负载均衡 应用服务中间件 Linux
在Linux中,Nginx如何实现负载均衡分发策略?
在Linux中,Nginx如何实现负载均衡分发策略?
|
5月前
|
缓存 负载均衡 算法
在Linux中, LVS负载均衡有哪些策略?
在Linux中, LVS负载均衡有哪些策略?
|
6月前
|
消息中间件 负载均衡 算法
【RocketMQ系列十二】RocketMQ集群核心概念之主从复制&生产者负载均衡策略&消费者负载均衡策略
【RocketMQ系列十二】RocketMQ集群核心概念之主从复制&生产者负载均衡策略&消费者负载均衡策略
166 2
|
7月前
|
负载均衡 算法 Nacos
SpringCloud之LoadBalancer自定义负载均衡算法,基于nacos权重
ReactorLoadBalancer接口,实现自定义负载算法需要实现该接口,并实现choose逻辑,选取对应的节点。
535 0