1 缘起
补课吧。
之前一直着急往前赶进度,
只梳理了微服务架构以及如何使用这些架构中的组件,
然而,并不了解运作原理,
我依旧还是太弱了,经不起大风大浪,
所以,想使自己强壮一些,继续研究一下源码。
还有另外一个原因,最近看了K8S,并实践了K8S部署SpringBoot服务,
发现,可以直接使用K8S进行负载均衡,
于是,又想到,Spring自己也有负载均衡,是如何实现的?
所以,有了这篇文章。
2 源码分析
2.1 如何找到入口
我们知道,
当开发同学使用SpringBoot进行开发时,
没有配置某些Bean,但是,可以直接使用这些Bean,
就说明,SpringBoot在启动时自动装配了这些Bean,
OpenFeign的负载均衡是同样的的道理,
开发同学没有配置负载均衡策略,但是,使用OpenFeign可以自动进行均衡,
因此,OpenFeign的负载均衡是通过自动装配自动完成的,
所以, 找负载均衡的自动装配类,
进入OpenFeign的源码,找到的LoadBalancer自动装配类,源码如下图所示。
位置:org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
该类即一种自动装配,实例化基于LoadBalancerClient的Client实现,
LoadBalancerClient是接口,即实现LoadBalancerClient。
2.2 寻找负载均衡策略
通过负载均衡入口类FeignLoadBalancerAutoConfiguration,我们看到标识了很多个注解,
需要关注的是LoadBalancerClientFactory负载均衡工厂,
我是怎么知道的?
挨个点进去看看呗。
2.2.1 负载均衡客户端工厂
这个工厂是干啥的?
从厂名猜测一下,应该是装配负载均衡客户端的。
下面,穿上工作服,进入负载均衡工厂吧!
首先看看工厂里都有啥?
源码如下图所示,由注释可知,该工厂就是生产客户端、负载均衡和客户端配置实例的。
他为每个客户端名称创建一个Spring应用上下文,并且根据需要提取。
位置:org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory
2.2.2 负载均衡客户端配置
通过上图,仔细阅读,不难发现,
工厂的构造器LoadBalancerClientFactory()
通过继承NamedContextFactory<LoadBalancerClientSpecification>
进行实例化,
而LoadBalancerClientSpecification就是指定负载均衡客户端,
所以,可以看下使用的类:LoadBalancerClientConfiguration.class,
猜想,这个类就是指定负载均衡策略的。(其实,是先点进去才知道,马后炮)
位置:org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration
该类源码如下图所示,由源码可知,
创建的Bean:reactorServiceInstanceLoadBalancer使用的负载均衡策略是“轮询”方式,
RoundRobinLoadBalancer。
2.3 “轮询式”负载均衡策略
老规矩,进入源码查看实现,
看看这个“轮询”的负载均衡策略是如何实现的?
位置:org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#RoundRobinLoadBalancer(org.springframework.beans.factory.ObjectProvider<org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier>, java.lang.String)
第一层的方法如下图所示,
这里关注第三个参数:new Random().nextInt(1000)
随机生成值域为[1, 1000]的种子,用于均衡客户端,而不是按顺序轮询。
实例化的参数如下图所示,
position作为种子。
接下来,看看是如何使用这个种子进行均衡的,
方法调用路径:choose-》processInstanceResponse-》getInstanceResponse,
因此,最终的均衡逻辑在:getInstanceResponse
位置:org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#getInstanceResponse
均衡逻辑中,有一个TODO: enforce order?是否强制均衡顺序?
这里使用的均衡,并不是顺序均衡,
而是通过随机种子与服务数量取余(pos%servcie.size),来决定将请求均衡到哪个服务上。
到这里,完成OpenFeign负载均衡探索。
下面验证下OpenFeign的负载均衡。
3 实践
本次实验的服务架构如下图所示。
共有三个模块:注册中心(Eureka)、消费者(tutorial)和生产者(spring-boot-template,三个)。
序号 | 模块 | 描述 |
---|---|---|
1 | 注册中心 | 这里绘制的是Eureka集群,但是,实际实验过程,只用了一个Eureka服务 |
2 | 消费者 | 通过集成OpenFeign和LoadBalancer调用生产者,验证LoadBalancer的负载均衡功能 |
3 | 生产者 | 对外提供接口,注册到Eureka,其他服务可以通过OpenFeign调用 |
3.1 依赖
SpingCloud版本:2020.0.3
因此,集成Eureka后,需要将默认的Ribbon移除,
添加SpringCloud的负载均衡。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.2 配置消费者(Consumer)FeignClient:服务A
服务A需要调用同一注册中心(Eureka实验用)的服务B,
因此,需要在服务A(tutorial)中配置服务B的Feigin客户端,以调用服务B(spring-boot-template),
服务A(tutorial)中配置的FeiginClient如下所示,
通过@FeignClient(value = “spring-boot-template”)指定生产者(Provider)。
package com.monkey.tutorial.common.rpc;
import com.monkey.tutorial.common.constant.MicroServiceApiConstant;
import com.monkey.tutorial.common.constant.MicroServiceNameConstant;
import com.monkey.tutorial.common.response.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* FeignTemplate服务调用.
*
* @author xindaqi
* @date 2021-12-22 10:01
*/
@FeignClient(value = "spring-boot-template")
public interface IFeignTemplateService {
/**
* 测试接口.
*
* @return 测试结果
*/
@RequestMapping(MicroServiceApiConstant.API_GET_TEST)
Response<String> feign1Test(@RequestParam("msg") String msg);
@RequestMapping(MicroServiceApiConstant.API_FEIGN_TEST)
String feign2Test();
}
3.3 启用FeignClient
package com.monkey.tutorial;
import com.monkey.tutorial.common.constant.MicroServiceNameConstant;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableCaching
@EnableFeignClients
@ServletComponentScan
public class TutorialApplication {
private static final Logger logger = LoggerFactory.getLogger(TutorialApplication.class);
public static void main(String[] args) {
SpringApplication.run(TutorialApplication.class, args);
logger.info("Tutorial 成功启动");
}
}
3.4 配置生产者(Provider):服务B
这里,在单台机器上开多个生产者的服务,
使用3个生产者:p1,p2和p3
3.4.1 配置文件
- application.yml
spring:
main:
allow-bean-definition-overriding: true
application:
name: spring-boot-template
profiles:
active: dev
web:
resources:
static-locations: classpath:/resources/
mybatis:
config-location: classpath:/config/mybatis-config.xml
mapper-locations: classpath:mapper/*.xml
logging:
config: classpath:config/logback.xml
每个生产者中这里精简了许多配置,
只给出了必要的配置:端口(用于区分服务)、Eureka(注册到Eureka)
- application-p1.yml
server:
port: 9321
servlet:
session:
timeout: PT10S
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8001/eureka/eureka
- application-p2.yml
server:
port: 9322
servlet:
session:
timeout: PT10S
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8001/eureka/eureka
- application-p3.yml
server:
port: 9323
servlet:
session:
timeout: PT10S
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8001/eureka/eureka
3.4.2 生产者接口
package com.monkey.springboottemplate.api;
import com.monkey.springboottemplate.common.enms.BizExceptionResponseCodeEnums;
import com.monkey.springboottemplate.common.exception.BizException;
import com.monkey.springboottemplate.common.response.Response;
import org.apache.ibatis.annotations.Param;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import static com.monkey.springboottemplate.common.constant.DigitalConstant.ONE;
/**
* 测试接口.
*
* @author xindaqi
* @date 2021-04-30 18:01
*/
@RestController
@RequestMapping("/api/v1")
public class TestApi {
@GetMapping("/get/test")
public Response<String> getTest(@RequestParam("msg") String msg, HttpServletRequest httpServletRequest) {
int localPort = httpServletRequest.getLocalPort();
StringBuilder sb = new StringBuilder();
sb.append(msg).append(",LocalPort:").append(localPort);
return Response.success(sb.toString());
}
@GetMapping("/feign")
public String feignTest() {
return "feign";
}
}
4 IDEA配置
IDEA指定配置文件,分别启动生产者。
在Configuration中配置需要启动的主函数,如下图所示。
这里使用三个生产者(Provider),因此新建三个Application,
分别为Provider-1、Provider-2和Provider-3,
添加运行参数,指定运行时激活的配置文件,如下图所示。
--spring.profiles.active
分别启动三个生产者,启动成功后,在IDEA中开启了三个Provider,
如下图所示。
5 注册中心
启动注册中心Eureka,
之后启动消费者和3个生产者,
总共有4个服务注册到Eureka,
登录Eureka,如下图所示,可以看到。
6 测试
通过消费者接口调用消费者,
每次调用都会均衡到不同的消费者。
下面测试了三次,结果如下图所示,
通过不同的接口判定请求的是哪个消费者。
7 小结
(1)SpringCloud:2020.0.3,已经弃用Netflix的Ribbon,
使用自有组件spring-cloud-loadbalancer做负载均衡,因此,集成Eureka时需要移除ribbon;
(2)spring-cloud-loadbalancer使用默认的负载均衡策略为:随机“轮询”方式,通过随机种子与服务数量取余,选择均衡的服务;
(3)消费者无需选择负载均衡策略,OpenFeign启动时,自动装配负载均衡器;
(4)spring-cloud-loadbalancer负载均衡有两种:纯随机和随机轮询。