前言
之前有好几篇文章讲了 灰度
, 网关
, 负载均衡
等, 本篇从源码角度分析几种负载均衡组件的实现。
基于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;
@RestController @RequestMapping("/test") public class TestController {
@Autowired private Environment env;
@Value("${version:0}") private String version;
@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;
@RestController public class HealthController {
@GetMapping("/heath") public String heath() { return "up"; } }
|
打包后我们启动2个实例用来后续测试, 端口分别是 9001
和 9002
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
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;
@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;
@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
进行增强和逻辑处理的。所以, 首先我们需要找到请求是通过哪个过滤器处理的。
-
application.yml
配置文件中配置了负载均衡策略 com.netflix.loadbalancer.RoundRobinRule
, 所以我们从这里作为突破口。在 choose(ILoadBalancer lb, Object key)
方法打断点, 然后 从网关请求, 发现请求经过断点, 说明判断正确。
-
查看方法栈, 可以看到请求来自于 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#choose
。继续往前追踪, 可以看到请求来自于 org.springframework.cloud.gateway.filter.LoadBalancerClientFilter#choose
。
-
负载均衡器的目的是为了在集群实例中按指定策略帮我挑选出一个实例。它本身并不实际发出 http
请求。
-
实际的请求是通过后续的 filter
中完成的。具体可以查看 org.springframework.cloud.gateway.filter.NettyRoutingFilter#filter
中的实现逻辑。
-
默认的 httpclient
在 org.springframework.cloud.gateway.config.GatewayAutoConfiguration.NettyConfiguration#gatewayHttpClient
注入, 为 netty
中的 HttpClient
完整的调用链如下(参考):
- 以上的断点分析没有
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
是通过 RestTemplate
的 http
协议通信的。方法栈如下:
+ 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实现服务发现
- 引入nacos服务发现依赖
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
- 启动类添加注解
gateway实现服务发现
- 引入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
, 所以该包可以不引入。
- 由于基于服务发现来调用服务, 所以静态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 logging: level: root: INFO
|
- 启动类添加服务发现注解
测试
1
| curl 127.0.0.1:8080/provider/test/port
|
结果是轮询输出 port=9001, version=0%
和 port=9002, version=0%
源码分析
-
同样在 com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)
打断点查看调用链。说明请求并不经过 ribbon
内置的轮询策略。
-
由于是nacos在做服务发现相关的逻辑。很可能是 nacos
接管了负载策略。查看 com.netflix.loadbalancer.IRule#choose
实现类,发现确实有nacos做的负载策略。 所以我们在 com.alibaba.cloud.nacos.ribbon.NacosRule#choose
打断点继续观察。然而请求依旧没有进入断点。
-
根据之前的经验, 我们在 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
策略的
- 修改负载均衡策略
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;
@Configuration public class RibbonLbConfig {
@Bean public IRule ribbonRule(){ return new RoundRobinRule(); } }
|
再次测试, 发现这次请求进入到了 com.netflix.loadbalancer.RoundRobinRule#choose(com.netflix.loadbalancer.ILoadBalancer, java.lang.Object)
- 再次通过断点验证下, 发现
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.ServiceInstance, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T>)
依然没有被执行。说明该方法确实只和 @LoadBalanced
和 RestTemplate
相关。
基于spring-cloud-starter-loadbalancer实现负载均衡
ReactiveLoadBalancerClientFilter-ReactiveLoadBalancer
未完待续…