0%

ribbon读取配置跟踪

一、背景

现场某个服务在eureka上注册了三个节点,但运维人员反映大多数请求均落在固定一个节点上,其它两个节点几乎没什么压力,服务请求是通过zuul网关进行转发。
我的第一反应是不可能啊,zuul网关转发不应该是默认负载均衡,轮询转发吗,肯定是配置问题?(甩锅标准套路,哈哈)等拿到现场配置,仔细一看,确实是默认配置,难道是我记错了吗?赶紧启动项目,跟踪代码看看。

补充:读了Spring Cloud官方reference,其中写明了默认配置,还是要多读读官方文档才行啊。

image-20210419153944949

二、构造测试环境

搭建一套最小的集成环境,包括服务如下:

注册中心eureka
网关服务zuul
后端服务,也就是服务提供者 service-provider

三个服务依赖的springboot版本为2.3.9.RELEASEspring cloud版本为Hoxton.SR10。注册中心、网关启动一个实例,后端服务启动两个实例,网关和后端服务均注册到注册中心当中。

1. 网关服务配置路由信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8080
eureka:
instance:
prefer-ip-address: true
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: zuul-gateway
zuul:
debug:
request: true
routes:
provider:
path: /service-provider/**
stripPrefix: false
serviceId: service-provider

2. 注册中心配置

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8761
eureka:
instance:
hostname: localhost
prefer-ip-address: false
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/

3. 后端服务配置

后端服务启动了两个实例,端口分别为8081、8082。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8081
servlet:
context-path: /service-provider
eureka:
client:
enabled: true
service-url:
default: http://localhost:8761/eureka/
spring:
application:
name: service-provider
1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8082
servlet:
context-path: /service-provider
eureka:
client:
enabled: true
service-url:
default: http://localhost:8761/eureka/
spring:
application:
name: service-provider

后端服务提供了一个get请求的接口,方便直接在浏览器中调用(也省了再运行一个服务去调用该接口)。服务实现就是打印helloworld

等待服务运行成功,我们可以通过访问以下三个url完成调用:

http://127.0.0.1:8081/service-provider/

http://127.0.0.1:8082/service-provider/

http://127.0.0.1:8080/service-provider/

返回的结果都是如下图一样:

image-20210417214819811

三、调试跟踪调用链

1.确定默认负载均衡策略

根据现场描述,问题出在负载均衡策略的选择上,因此找到负载均衡策略,查看接口IRule的定义:

1
2
3
4
5
6
7
8
9
10
package com.netflix.loadbalancer;

public interface IRule {
//选择服务节点
Server choose(Object var1);
//设置负载均衡器
void setLoadBalancer(ILoadBalancer var1);
//获取负载均衡器
ILoadBalancer getLoadBalancer();
}

可以看到,这3个接口,第一个就是选择服务节点,在idea中通过快捷键option+command+B找到所有所有继承该接口的类,可以定位到都集中在com.netflix.ribbon:ribbon-loadbalancer:2.3.0这个jar包中,排除抽象类,最终可用的类都放在com.netflix.loadbalancer主要有如下几个:

1
2
3
4
5
6
7
com.netflix.loadbalancer.AvailabilityFilteringRule
com.netflix.loadbalancer.BestAvailableRule
com.netflix.loadbalancer.RandomRule
com.netflix.loadbalancer.RetryRule
com.netflix.loadbalancer.RoundRobinRule
com.netflix.loadbalancer.WeightedResponseTimeRule
com.netflix.loadbalancer.ZoneAvoidanceRule

先将所有的策略的choose方法都设置断点,然后在浏览器中发起请求http://127.0.0.1:8080/service-provider/,观察调用堆栈如下,可以看到,调用的是PredicateBasedRule里的choose方法,定位具体使用的策略类为ZoneAvoidanceRule

image-20210417222814660

2.定位ZoneAvoidanceRule初始化过程

我们在ZoneAvoidanceRule类的构造函数上设置断点,重启zuul网关,观察断点触发时机。可以观察到,程序启动完成后,断点都没有被触发,因此可以推断是懒加载的情况,既在请求发生时,按需创建。在浏览器中发起请求,断点被触发,查看堆栈如下:

image-20210417225700302

观察调用堆栈,可以看到其构造函数是在org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration中被调用的,点进去查看代码如下:

image-20210417225917962

在这段代码中可以看到,首先是根据第112行判断当前服务标识下是否设置了IRule.class,如果设置了,就使用设置的策略,否则就使用ZoneAvoidanceRule作为默认策略。下面定位第112行的内部处理逻辑。

3.定位当前服务标识配置处理

在上图中,org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration第112设置断点,按照步骤2中的方法重新执行,触发断点,进入到isSet内部,查看到类org.springframework.cloud.netflix.ribbon.PropertiesFactory中处理逻辑如下:

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
public boolean isSet(Class clazz, String name) {
return StringUtils.hasText(getClassName(clazz, name));
}

public String getClassName(Class clazz, String name) {
if (this.classToProperty.containsKey(clazz)) {
String classNameProperty = this.classToProperty.get(clazz);
String className = environment
.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
return className;
}
return null;
}

@SuppressWarnings("unchecked")
public <C> C get(Class<C> clazz, IClientConfig config, String name) {
String className = getClassName(clazz, name);
if (StringUtils.hasText(className)) {
try {
Class<?> toInstantiate = Class.forName(className);
return (C) SpringClientFactory.instantiateWithConfig(toInstantiate,
config);
}
catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Unknown class to load " + className
+ " for class " + clazz + " named " + name);
}
}
return null;
}

查看此时this.classToProperty变量值如下:

image-20210417231628221

其处理逻辑就是根据class找到对应的名称,此处为NFLoadBalancerRuleClassName,再拼接name + "." + NAMESPACE + "." + classNameProperty为属性名(service-provider.ribbon.NFLoadBalancerRuleClassName),从系统配置environment中进行查找。如果找到,就调用org.springframework.cloud.netflix.ribbon.PropertiesFactory#get方法初始化对象,否则就使用默认的负载均衡策略。

四、总结

通过RibbonClientConfiguration可以看到,ribbon可以分别对单个服务进行个性化配置;同时懒加载的处理方式,保证了程序能够快速启动。下一步需要跟踪判断,ribbon的懒加载的实现方式,为什么在第一次调用时才会调用到RibbonClientConfiguration里的函数,而不是程序启动初始化时执行。