前言
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 对象以及负载均衡器可以实现自定义策略。下面为具体的代码实现:
- EnableCustomLoadBalance.java 自定义开启负载均衡策略注解,在启动类 Application 类上添加即可开启。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({EnableCustomLoadBalanceImportSelector.class}) public @interface EnableCustomLoadBalance { String rule() default "local-first"; }
rule 即要实现的策略规则,实现多种策略后,可以通过 rule 指定策略。
- 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; } }
- LoadBalanceAutoConfiguration.java
用于自定义负载均衡自动配置生效。
@ConditionalOnClass(LoadBalancerClients.class) @LoadBalancerClients(defaultConfiguration = LoadBalanceConfig.class) @AutoConfigureBefore({ReactorLoadBalancerClientAutoConfiguration.class, LoadBalancerBeanPostProcessorAutoConfiguration.class}) public class LoadBalanceAutoConfiguration { }
自动加载 LoadBalanceConfig 配置类,并限制在 ReactorLoadBalancerClientAutoConfiguration 和 LoadBalancerBeanPostProcessorAutoConfiguration 生效后开始配置。
- 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); } }
- 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。
- 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 是如何提供上层抽象的服务注册发现编程模型,屏蔽不同底层框架的具体实现。