一、背景 现场某个服务在eureka上注册了三个节点,但运维人员反映大多数请求均落在固定一个节点上,其它两个节点几乎没什么压力,服务请求是通过zuul网关进行转发。 我的第一反应是不可能啊,zuul网关转发不应该是默认负载均衡,轮询转发吗,肯定是配置问题?(甩锅标准套路,哈哈
)等拿到现场配置,仔细一看,确实是默认配置,难道是我记错了吗?赶紧启动项目,跟踪代码看看。
补充:读了Spring Cloud官方reference,其中写明了默认配置,还是要多读读官方文档 才行啊。
二、构造测试环境 搭建一套最小的集成环境,包括服务如下:
注册中心eureka 网关服务zuul 后端服务,也就是服务提供者 service-provider
三个服务依赖的springboot
版本为2.3.9.RELEASE
,spring 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/
返回的结果都是如下图一样:
三、调试跟踪调用链 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
。
2.定位ZoneAvoidanceRule
初始化过程 我们在ZoneAvoidanceRule
类的构造函数上设置断点,重启zuul网关,观察断点触发时机。可以观察到,程序启动完成后,断点都没有被触发,因此可以推断是懒加载的情况,既在请求发生时,按需创建。在浏览器中发起请求,断点被触发,查看堆栈如下:
观察调用堆栈,可以看到其构造函数是在org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration
中被调用的,点进去查看代码如下:
在这段代码中可以看到,首先是根据第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
变量值如下:
其处理逻辑就是根据class找到对应的名称,此处为NFLoadBalancerRuleClassName
,再拼接name + "." + NAMESPACE + "." + classNameProperty
为属性名(service-provider.ribbon.NFLoadBalancerRuleClassName
),从系统配置environment
中进行查找。如果找到,就调用org.springframework.cloud.netflix.ribbon.PropertiesFactory#get
方法初始化对象,否则就使用默认的负载均衡策略。
四、总结 通过RibbonClientConfiguration
可以看到,ribbon可以分别对单个服务进行个性化配置;同时懒加载的处理方式,保证了程序能够快速启动。下一步需要跟踪判断,ribbon的懒加载的实现方式,为什么在第一次调用时才会调用到RibbonClientConfiguration
里的函数,而不是程序启动初始化时执行。