重看spring-cloud-gateway负载均衡

前言

之前有好几篇文章讲了 灰度, 网关, 负载均衡 等, 本篇从源码角度分析几种负载均衡组件的实现。

基于ribbon和静态ip实现负载均衡

由于之前讲过很多次基于 nacos 的服务发现, 这里的案例直接用基于 静态ip 的实例来演示。

搭建 provider 服务

provider 服务是普通的web项目。首先提供对外暴露的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package cn.idea360.provider.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author cuishiying
* @date 2021-01-22
*/
@RestController
@RequestMapping("/test")
public class TestController {

@Autowired
private Environment env;

@Value("${version:0}")
private String version;

/**
* 测试接口, 对外提供访问
* http://localhost:9001/test/port
*/
@GetMapping("/port")
public Object port() {
return String.format("port=%s, version=%s", env.getProperty("local.server.port"), version);
}
}

其次在实际的环境中, 可能服务进程还在, 但是服务已不能对外提供访问, 所以我们这里提供一个 巡检 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.idea360.provider.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author cuishiying
* @date 2021-01-22
*/
@RestController
public class HealthController {

/**
* 网关监控检查
* http://localhost:9001/heath
*/
@GetMapping("/heath")
public String heath() {
return "up";
}
}

打包后我们启动2个实例用来后续测试, 端口分别是 90019002

1
2
java -jar target/provider.jar --server.port=9001
java -jar target/provider.jar --server.port=9002

测试下接口是否正常

1
2
3
4
5
curl 127.0.0.1:9001/test/port
port=9001, version=0%

curl 127.0.0.1:9002/test/port
port=9002, version=0%

搭建 gateway 服务

这里我们的依赖包不需要服务发现相关的

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

网关的路由配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: static-ip
uri: lb://static-ip-load-balanced-service
predicates:
- Path=/provider/**
filters:
- StripPrefix=1 #过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。StripPrefix=1就代表截取路径的个数为1,比如前端过来请求http://localhost:8080/provider/test/port,匹配成功后,路由到后端的请求路径就会变成http://localhost:xx/test/port

static-ip-load-balanced-service:
ribbon:
# 负载地址
listOfServers: http://localhost:9001, http://localhost:9002
# 负载轮询策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
# 健康检查
NFLoadBalancerPingClassName: cn.idea360.gateway.actuator.HealthExamination

logging:
level:
root: INFO
cn.idea360.gateway.actuator.HealthExamination: DEBUG

我们在网关负载时用到了健康检查, 这里我们需要配置下 RestTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.idea360.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* @author cuishiying
* @date 2021-01-22
*/
@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

健康检查类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package cn.idea360.gateway.actuator;

import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
* 监控检查由ribbon自动巡检, 非调用接口触发
*
* @author cuishiying
* @date 2021-01-22
*/
@Slf4j
@Component
public class HealthExamination implements IPing {

@Resource
private RestTemplate restTemplate;

@Override
public boolean isAlive(Server server) {
String url = "http://" + server.getId() + "/heath";
try {
ResponseEntity<String> heath = restTemplate.getForEntity(url, String.class);
if (heath.getStatusCode() == HttpStatus.OK) {
log.debug("ping {} success and response is {}", url, heath.getBody());
return true;
}
log.warn("ping {} error and response is {}", url, heath.getBody());
return false;
} catch (Exception e) {
log.error("ping {} failed", url);
return false;
}
}
}

网关请求测试负载是否复合预期, 经过测试, 请求轮询负载到 provider 服务

1
curl 127.0.0.1:8080/provider/test/port

同时查看 gateway 的日志可以看到健康检查在定时巡检

1
2
ping http://localhost:9002/heath success and response is up
ping http://localhost:9001/heath success and response is up

负载源码分析

大部分的 http 请求都是通过 filter 进行增强和逻辑处理的。所以, 首先我们需要找到请求是通过哪个过滤器处理的。

  1. application.yml 配置文件中配置了负载均衡策略 com.netflix.loadbalancer.RoundRobinRule, 所以我们从这里作为突破口。在 choose(ILoadBalancer lb, Object key) 方法打断点, 然后 从网关请求, 发现请求经过断点, 说明判断正确。

  2. 查看方法栈, 可以看到请求来自于 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#choose。继续往前追踪, 可以看到请求来自于 org.springframework.cloud.gateway.filter.LoadBalancerClientFilter#choose

    • 负载均衡器的目的是为了在集群实例中按指定策略帮我挑选出一个实例。它本身并不实际发出 http 请求。

    • 实际的请求是通过后续的 filter 中完成的。具体可以查看 org.springframework.cloud.gateway.filter.NettyRoutingFilter#filter 中的实现逻辑。

    • 默认的 httpclientorg.springframework.cloud.gateway.config.GatewayAutoConfiguration.NettyConfiguration#gatewayHttpClient 注入, 为 netty 中的 HttpClient

完整的调用链如下(参考):

  1. 以上的断点分析没有 spring-cloud-commons 包的 org.springframework.cloud.client.loadbalancer.LoadBalancerClient#execute 啥事儿了。其实该方法和 @LoadBalanced 密切相关, 而该注解只和 RestTemplate 相关。更多应用于服务间调用。

修改下 RestTemplate 配置

1
2
3
4
5
6
7
8
9
@Configuration
public class RestTemplateConfig {

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

网关巡检 /heath 是通过 RestTemplatehttp 协议通信的。方法栈如下:

+ org.springframework.web.client.RestTemplate#doExecute
+ org.springframework.http.client.AbstractClientHttpRequest#execute
+ org.springframework.http.client.AbstractBufferingClientHttpRequest#executeInternal(org.springframework.http.HttpHeaders)
+ org.springframework.http.client.InterceptingClientHttpRequest#executeInternal
+ org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute
+ org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor#intercept

查看 LoadBalancerInterceptor 源码可知, 这样请求就经过了 LoadBalancerClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

private LoadBalancerClient loadBalancer;

@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));
}
}

基于Ribbon和nacos实现负载均衡

provider实现服务发现

  1. 引入nacos服务发现依赖
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 启动类添加注解
1
@EnableDiscoveryClient

gateway实现服务发现

  1. 引入nacos服务发现依赖
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

由于nacos中默认引入了 spring-cloud-starter-netflix-ribbon, 所以该包可以不引入。

  1. 由于基于服务发现来调用服务, 所以静态ip配置不再需要
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8080
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: static-ip
uri: lb://provider
predicates:
- Path=/provider/**
filters:
- StripPrefix=1 #过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。StripPrefix=1就代表截取路径的个数为1,比如前端过来请求http://localhost:8080/provider/test/port,匹配成功后,路由到后端的请求路径就会变成http://localhost:xx/test/port
logging:
level:
root: INFO
  1. 启动类添加服务发现注解
1
@EnableDiscoveryClient

测试

1
curl 127.0.0.1:8080/provider/test/port

结果是轮询输出 port=9001, version=0%port=9002, version=0%

源码分析

  1. 同样在 com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object) 打断点查看调用链。说明请求并不经过 ribbon 内置的轮询策略。

  2. 由于是nacos在做服务发现相关的逻辑。很可能是 nacos 接管了负载策略。查看 com.netflix.loadbalancer.IRule#choose 实现类,发现确实有nacos做的负载策略。 所以我们在 com.alibaba.cloud.nacos.ribbon.NacosRule#choose 打断点继续观察。然而请求依旧没有进入断点。

  3. 根据之前的经验, 我们在 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#choose(java.lang.String, java.lang.Object) 打断点, 这次进入了断点。调用链如下:

  • 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

其实ribbon是推荐 AvailabilityFilteringRule 策略的

  1. 修改负载均衡策略

org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration#ribbonRule

1
2
3
4
5
6
7
8
9
10
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}

在源码中, 可以看到默认的负载均衡策略是可以覆写的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.idea360.gateway.config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author cuishiying
* @date 2021-01-22
*/
@Configuration
public class RibbonLbConfig {

@Bean
public IRule ribbonRule(){
return new RoundRobinRule();
}
}

再次测试, 发现这次请求进入到了 com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)

  1. 再次通过断点验证下, 发现 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.ServiceInstance, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T>) 依然没有被执行。说明该方法确实只和 @LoadBalancedRestTemplate 相关。

基于spring-cloud-starter-loadbalancer实现负载均衡

ReactiveLoadBalancerClientFilter-ReactiveLoadBalancer

未完待续…