本章简单使用Spring Cloud Netflix Ribbon进行客户端的负载均衡,并分析其原理实现。
官网地址 spring-cloud-ribbon
一、 RESTful HTTP协议通信
REST,全称是Resource Representational State Transfer,即:资源在网络中以某种形式进行状态转移。规范了HTTP通信协议的标准:
- HTTP METHOD 约束资源操作类型 GET/POST/PUT/DELETE
Verd | 描述 |
---|---|
GET(SELECT) | 查询 |
POST(CREATE) | 提交 |
PUT(UPDATE) | 修改,客户端需要提供新建资源的所有属性 |
DELETE(DELETE) | 删除 |
- REST是面向资源的:
以订单接口为例
根据id获取订单信息:对应类型 GET METHOD
/order(GET) /order/${id}
保存订单: 对应类型 POST METHOD
/order(POST)
修改订单:对应类型 PUT METHOD
/order(PUT)
删除订单:对应类型 DELETE METHOD
/order(DELETE) /order/${id}
查询订单列表: 使用
/orders
- 名词
以根据id查询订单明细为例,普通接口命名可能是queryOrderById
,而RESTful接口则是(GET) /order/${id}
- HTTP返回码
状态码 | 描述 |
---|---|
2XX | 请求正常处理并返回 |
3XX | 重定向,请求的资源位置发生变化 |
4XX | 客户端发送的请求有误 |
5XX | 服务器端的错误 |
这里有一篇RESTful详细的介绍,转载下 老_张 RESTful API浅谈
二、RestTemplate
当服务由单体架构一步步演变为集群->垂直拆分->SOA->微服务后,不可避免的涉及到服务间的相互通信。
此时我们可以使用 HttpClient 、 RestTemplate 、 OkHttp 、JDK HttpUrlConnection
这几种方式来达到通信数据交互的目的。
以电商的用户、订单服务为例,当用户服务内需要根据用户找到所有订单时,则用户服务会与订单服务建立HTTP通信:
下面基于SpringBoot简单演示接口实现:
1. OrderService
提供订单查询接口http://localhost:8081/orders
供UserService调用:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@GetMapping("/orders")
public String getAllOrder() {
return "Shen all orders";
}
}
2. UserService
UserServer使用RestTemplate进行服务调用
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserController {
@Autowired
RestTemplate restTemplate;
// @Bean
// public RestTemplate restTemplate() {
// return new RestTemplate();
// }
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
@GetMapping("/user/{id}")
public String findById(@PathVariable("id") int id) {
// 调用远程订单服务
// HttpClient 、 RestTemplate 、 OkHttp 、JDK HttpUrlConnection
String rst = restTemplate.getForObject("http://localhost:8081/orders", String.class);
return rst;
}
}
三、Spring Cloud Netflix Ribbon 集群负载均衡调用
上例子是以单服务节点的调用为例,真实场景中为了避免单点故障、提升并发效率,都会部署多台服务,此时就涉及客户端在同时调用多台服务时的负载均衡问题。
此时服务提供者OrderService的地址,可以先维护在客户端UserService的配置文件内,先看一下以Ribbon方法来实现客户端的负载均衡。
1. 声明LoadBalancerClient实现负载均衡
OrderService
服务提供方,订单服务,此时将服务部署在8081和8082两个不同端口,Intellij IDEA中支持同一个SpringBoot项目同时配置多个端口:
UserService
服务调用方、客户端 用户服务:
- 首先引入 netflix ribbot依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.3.RELEASE</version>
</dependency>
- 之后,
application.properties
配置文件内根据Ribbon规则配置服务地址
# 配置服务提供者的地址
spring-cloud-order-service.ribbon-listOfServers=
localhost:8081,localhost:8082
- 最后,客户端实现负载均衡改造
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserController {
@Autowired
RestTemplate restTemplate;
@Autowired
LoadBalancerClient loadBalancerClient;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
@GetMapping("/user/{id}")
public String findById(@PathVariable("id") int id) {
// 调用远程订单服务
// HttpClient 、 RestTemplate 、 OkHttp 、JDK HttpUrlConnection
// String rst = restTemplate.getForObject("http://localhost:8081/orders", String.class);
ServiceInstance serviceInstance = loadBalancerClient.choose("spring-cloud-order-service");
String url = String.format("http://%s:%s" + "/orders", serviceInstance.getHost(), serviceInstance.getPort());
System.out.println(url);
String rst = restTemplate.getForObject(url, String.class);
return rst;
}
}
当我们多次调用客户端/user/{id}
服务后,控制台输出如下,可见用户服务在调用集群订单服务时,已经实现了负载均衡,其实内部算法是默认的轮询机制。
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
http://localhost:8081/orders
http://localhost:8082/orders
2. 通过注解@LoadBalanced注解实现自动负载均衡
除了上面直接声明 @Autowired LoadBalancerClient loadBalancerClient;
负载均衡客户端外,也可以通过@LoadBalanced
注解来声明一个RestTemplate来达到负载均衡的目的
UserService
其余配置不变,将声明RestTemplate的Bean加上@LoadBalanced注解,服务调用地址也改为http://spring-cloud-order-service
,与配置文件内的key相对应。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class UserController2 {
@Autowired
RestTemplate restTemplate;
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@GetMapping("/user2/{id}")
public String getBuId(@PathVariable("id") int id) {
String rst = restTemplate.getForObject("http://spring-cloud-order-service" + "/orders", String.class);
return rst;
}
}
OrderService
订单服务其实可以不变,不过为了方便看到客户端负载均衡调用的结果,可以在订单服务内输出服务端口,来验证:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Value("${server.port}")
private int port;
@GetMapping("/orders")
public String getAllOrder() {
System.out.println(port);
return "Shen all orders";
}
}
这样就更方便的实现了负载均衡的调用。
本地案例demo地址:Spring-Cloud-Netflix-Ribbon Demo
四、Ribbon 源码分析
Ribbon其实是基于客户端的负载均衡的一个组件,从上面【二、1】例子的使用其实可以推测,Ribbon实现负载均衡很可能做了两个事情:
- 解析配置拿到服务方地址列表
- 基于负载均衡算法实现请求的分发
从【二、2】的例子,为RestTemplate添加了@LoadBalanced
注解可以自动负载均衡,可以尝试推测:
- 上面两个步骤需要做
- 需要在某个地方扫描加了@LoadBalanced注解的RestTemplate,并对RestTemplate做改造,来实现地址的动态解析
A、对RestTemplate添加@LoadBalanced
注解进行修饰
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
我们只是为 RestTemplate 添加了 LoadBalanced注解,为什么可以实现负载均衡,肯定是某些地方会扫描 加了@LoadBalanced
注解的 RestTemplate,并做相应改造
B、LoadBalancerAutoConfiguration获取到添加了@LoadBalanced
的RestTemplate,进行包装
- 我们可以先看下@LoadBalanced源码,可以看到除了配置了一些支持的特性外,本质是一个
@Qualifier
,相当于是一个继承了@Qualifier
的@LoadBalanced
标记。
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
- 而直接使用@LoadBalanced注解,可以看做是使用了Spring的Bean的自动装配的特性,那么应该是在spring.factories内配置了对应声明Bean的Configuration,
果然,我们可以在jar包内的META-INF/spring.factories
内找到维护了EnableAutoConfiguration
的LoadBalancerAutoConfiguration
配置:
- @LoadBalanced注解是在
spring-cloud-commons-2.2.3.RELEASE.jar
内的
- 在该jar包的
META-INF
的spring.factories内可以找到对应Configuration: LoadBalancerAutoConfiguration
- 现在我们就可以定位,LoadBalancerAutoConfiguration 内是如何对RestTemplate进行改造,来达到加了
LoadBalanced
注解就可以自动负载均衡
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class) // 自动装配条件 onClass
@ConditionalOnBean(LoadBalancerClient.class) // 自动装配条件 onBean
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
// Spring 的依赖注入
// 拿到加了@LoadBalanced标记的RestTemplate放入list集合中去,也就是我们之前在UserController2 里声明的`@LoadBalanced RestTemplate`
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
C、LoadBalancerAutoConfiguration为这些RestTemplate添加拦截器
在Configuration类做了一系列的Bean的初始化,且Bean的初始化其实是有先后顺序的依赖关系的:
- LoadBalancerAutoConfiguration 源码
LoadBalancerAutoConfiguration 类里声明了一系列Bean,并且Bean的初始化由先后依赖关系
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList(); // 获取到所有加了 @LoadBalanced 声明的 RestTemplate 集合
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
// `restTemplates`集合是在这里使用的,所有可以以这里的@Bean,Bean的初始化为出发点
// 因为是@Bean进行方法参数的依赖注入,存在Bean的初始化的依赖关系;大致有 C01 -> C02 -> C03 -> C04 -> C05 几个步骤的Bean初始化
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { // C01. 初始化SmartInitializingSingleton Bean,依赖注入 RestTemplateCustomizer -> C02
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { // C06. 获取到所有restTemplates集合,
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate); // C07. 对这些RestTemplate进行包装,其实调用的是 RestTemplateCustomizer.customize 至C08
}
}
});
}
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory( // C05.初始化 LoadBalancerRequestFactory Bean,也依赖于 LoadBalancerClient Bean的初始化 -> C04
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) { // C03. 初始化LoadBalancerInterceptor,依赖于 LoadBalancerClient -> C04 和 LoadBalancerRequestFactory -> C05
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) { // C02. 初始化RestTemplateCustomizer Bean,依赖于 LoadBalancerInterceptor -> C03
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors()); // C08. 取得默认的拦截器列表
list.add(loadBalancerInterceptor); // C09. 添加 loadBalancerInterceptor 拦截器,类型为 LoadBalancerInterceptor
restTemplate.setInterceptors(list); // C10. 为restTemplate添加 LoadBalancerInterceptor 拦截器
};
}
}
}
- RibbonAutoConfiguration 源码,
对应 C04. 初始化 LoadBalancerClient Bean
前面C01-C03几步可以看到,在对RestTemplate进行封装时,都需要 LoadBalancerClient Bean的实例,那么这个Bean是在什么时候进行初始化的呢?
因为我们使用的是Ribbon,所以推测 LoadBalancerClient 的自动装配是在 Ribbon 的 jar包内实现的。
定位到 META-INF/spring.factories
文件的AutoConfiguration配置
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(
name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
AsyncLoadBalancerAutoConfiguration.class }) // AutoConfigureBefore,定义了RibbonAutoConfiguration 的初始化是在LoadBalancerAutoConfiguration之前,也和我们上面几步分析的Bean初始化顺序一致
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
// LoadBalancerClient Bean的初始化,使用的是 RibbonLoadBalancerClient
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() { // C04. 初始化 LoadBalancerClient Bean
return new RibbonLoadBalancerClient(springClientFactory()); // 其实就是自动装配了 RibbonLoadBalancerClient
}
至此,我们已经看到了一条Bean初始化的先后顺序:
首先:在 spring-cloud-netflix-ribbon-2.2.3.RELEASE.jar 内 根据Bena的自动装配规则初始化: LoadBalancerClient
RibbonAutoConfiguration : [ LoadBalancerClient(RibbonLoadBalancerClient) ]
->
之后: 在 spring-cloud-commons-2.2.3.RELEASE.jar 内,通过一系列Bean的先后初始化达到对RestTemplate进行包装的目的
LoadBalancerAutoConfiguration :[ LoadBalancerRequestFactory -> LoadBalancerInterceptor -> RestTemplateCustomizer -> SmartInitializingSingleton ]
也正是这条链路来实现了对RestTemplate的重新包装
而这些Bean的初始化,其实是为了对加了@LoadBalanced
注解的RestTemplate进行包装,包装内部逻辑是为RestTemplate添加拦截器LoadBalancerInterceptor restTemplate.setInterceptors(list);
D、LoadBalancerInterceptor对请求进行拦截
因为之前是为 RestTemplate 添加了 LoadBalancerInterceptor 拦截器,所以当我们进行restTemplate.getForObject("http://spring-cloud-order-service" + "/orders", String.class);
调用时,一定会进入拦截器LoadBalancerInterceptor#intercept
内:
- LoadBalancerInterceptor#intercept 对 请求进行拦截
LoadBalancerInterceptor#intercept
拦截器 intercept() 方法对请求 request 进行拦截
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer; // D01. 此时的LoadBalancerClient,其实就是我们之前分析过得,在 RibbonAutoConfiguration 内声明的 RibbonLoadBalancerClient
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException { // 对请求进行拦截
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,this.requestFactory.createRequest(request, body, execution)); // D02. 取得负载均衡器,执行相应逻辑
}
}
- 负载均衡器来获取其中一个srever
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T>)
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
return execute(serviceId, request, null); // D03. execute
}
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T>, java.lang.Object)
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId); // D04. 获得负载均衡器
Server server = getServer(loadBalancer, hint); // D05. 根据负载均衡器获得一个 server,就是我们维护在配置文件中的服务地址之一
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
- 获取server逻辑
源码过多,在此列出方法流转的流程,主要逻辑是
先解析到配置文件内的地址列表,然后根据负载均衡算法得到一个服务地址
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#getServer(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)
com.netflix.loadbalancer.ZoneAwareLoadBalancer#chooseServer
com.netflix.loadbalancer.BaseLoadBalancer#chooseServer
com.netflix.loadbalancer.PredicateBasedRule#choose
com.netflix.loadbalancer.AbstractServerPredicate#chooseRoundRobinAfterFiltering(java.util.List<com.netflix.loadbalancer.Server>, java.lang.Object)
com.netflix.loadbalancer.AbstractServerPredicate#incrementAndGetModulo
E、负载均衡算法
所支持的负载均衡算法可以从com.netflix.loadbalancer.IRule
类图中看出:
RoundRobinRule(轮询算法)
RandomRule(随机算法)
AvailabilityFilteringRule():会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问
WeightedResponseTimeRule():根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换到WeightedResponseTimeRule
RetryRule():先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。
BestAviableRule():会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
ZoneAvoidanceRule():默认规则,符合判断server所在区域的性能和server的可用性选择服务器
其中Ribbon默认的负载均衡算法为RoundRobinRule
轮询算法,实现也比较简单
com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers(); // 1. 获取到所有服务地址
int upCount = reachableServers.size();
int serverCount = allServers.size(); // 2. 服务地址数量
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount); // 3. 递增拿到当前轮训数量
server = allServers.get(nextServerIndex); // 8. 根据下标从服务列表List中获取到对应地址
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server); // 9. 返回服务地址
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
private AtomicInteger nextServerCyclicCounter;
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get(); // 4. 获取当前游标
int next = (current + 1) % modulo; // 5. +1,和服务总数量取模
if (nextServerCyclicCounter.compareAndSet(current, next)) // 6. cas保证线程安全
return next; // 7. 返回递增后的游标
}
}
总结:
使用Ribbon的两种方式:
- 为RestTemplate添加
@LoadBalanced
- 直接使用
@Autowired LoadBalancerClient loadBalancerClient;
客户端
Ribbon原理的主要几种类型:
- ILoadBalancer 负载均衡器
- IRule 负载均衡算法规则(权重机制(区间算法))
- IPing 存活状态监测 定时任务在不断地发起请求,ping,每10s去访问一次目标服务的地址,如果不可用则剔除无效服务
- ServerList 定时任务每30s执行一次更新服务列表
- 自定义负载均衡算法、自定义Ping
官网介绍可从:https://spring.io/ -> 头部导航 Project/SpringCloud -> 左侧导航 Spring Cloud Netflix -> Learn/Reference Doc. -> 左侧导航 7. Client Side Load Balancer: Ribbon 内看到
本地案例demo地址:Spring-Cloud-Netflix-Ribbon Demo