目录
2.2 LoadBalancerAutoConfiguration
2.3 LoadBalancerInterceptor(重点)
2.7 DynamicServerListLoadBalance
2.10 ZoneAwareLoadBalancer遗留的问题
2.11 RibbonClientConfiguration
前言
在上一篇的文中有提到,只要我们给RestTemplate这个Bean添加一个@LoadBlanced注解,就可以实现负载均衡。这里实际上就是用到了一个组件叫做Ribbon。那么,我们来了解一下,Ribbon到底是怎么来实现的。
1. 负载均衡原理
Ribbon是属于客户端的负载均衡器。服务发起请求之后,它会根据服务名称去eureka-server里面拉取服务列表,然后根据不同的负载均衡原则来选择具体访问服务的IP和端口,如下图:
虽然,现在我们了解了Ribbon运行的原理,但是具体怎么实现的,我们还是需要结合源码来看看。
2. 源码跟踪
2.1 LoadBalanced
我们找到@LoadBalanced注解的地方,按住 ctrl+鼠标左键 点击LoadBalanced,就可以找到LoadBalanced这个接口:
我们根据这个类的描述可以知道,这个注解主要是起到一个标记作用,最后使用的是LoadBlancerClient这个类。
PS:这个翻译软件你可以直接去 设置->插件 里面下载Translation就可以了。
下面找到这个类,结果发现是一个接口:
我们可以看到LoadBlancerClient这个接口就是负责负载均衡的。
下面,我们选定接口名,按住 alt+7 看一下这个接口有几个方法:
我们可以看到,这个接口一共只有3个方法,其中两个都是execute(执行)方法,只是传入的参数不一样。
2.2 LoadBalancerAutoConfiguration
在说ribbon原理的那张图里面,我们可以看到,我们实际上发出的请求地址是http://userservice/user/1,这根本不是一个真的http地址,由此我们可以猜想,在实现LoadBlancerClient的execute(执行)方法之前,应该有一个拦截器,能够把我们的请求拦截做一些处理。那么,我们现在去看看是不是跟我们的猜想一样,选中接口名称,按住 alt+F7 搜索一下有哪些地方实现了LoadBlancerClient这个接口:
结果还真的发现LoadBalancerAutoConfiguration这个自动装配类配置了拦截器,我们点进去看一下:
LoadBalancerAutoConfiguration定义了一个静态的内部类LoadBalancerInterceptorConfig
PS:看这一段代码有的地方不是很理解的话,这里推荐两个只能AI插件,通易灵码(阿里云)和CodeGeeX(清华大学+智谱AI)。这两个都可以解释代码,我这里用的是通义灵码:
2.3 LoadBalancerInterceptor(重点)
进入LoadBalancerInterceptor:
这里的方法是重写的父类(ClientHttpRequestInterceptor)的,从父类的命名来看,我们就没有找错,这里就是http请求的拦截器。 为了验证一下,我们来打个断点调试一下:
可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:
- request.getURI():获取请求uri,本例中就是http://userservice/user/1
- originalUri.getHost():获取uri路径的主机名,其实就是服务id,userservice
- this.loadBalancer.execute():处理服务id和用户请求
这里的this.loadBalancer是LoadBalancerClient类型,我们继续跟入。
2.4 RibbonLoadBalancerClient
继续跟入execute方法,我们走到的就是LoadBalancerClient的实现类RibbonLoadBalancerClient:
代码是这样的:
- getLoadBalancer(serviceId):根据id来获取ILoadBalancer,而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。
- getServer(loadBalancer):利用内置的负载均衡算法,从服务列表中选择一个。
在下面可以很清楚的看见这里的服务列表是两个,一个是8081,一个是8082;
第一次获取的是8081:
放行之后,再次访问并跟踪,发现获取的是8082:
的确是在上面的方法实现了负载均衡。然后,点进去第一个方法getLoadBalancer(serviceId)看看,发现是通过AnnotationConfigApplicationContext来维护的,然后从里面根据service类型获取对应的实例信息。
2.5 ZoneAwareLoadBalancer
在刚才的代码中,可以看到获取服务使通过一个getServer方法来做负载均衡。那么getLoadBalancer方法呢?我们先跟进去,可以看到,这里的实例默认是ZoneAwareLoadBalancer。在上面的图可以看到,idea显示的loadBalancer前缀是DynamicServerListLoadBalancer,那么后面又怎么变成了ZoneAwareLoadBalancer呢?这个我们等一会儿再来看看。
接下来,走到了getServer(loadBalancer,hint),从字面意思来看,拉取完userservice就应该选择其中一个服务实例来调用了,我们跟进去看看具体实现:
在这里,ILoadBalancer是一个接口,方法都是抽象的,我们要弄清楚chooseServer()方法的具体实现就只能选定方法,然后按住 ctrl+alt 看看它的实现类。因为在前面,我们能够明显的看到运行的实例是ZoneAwareLoadBalancer,所以我们只要再跟进去看看就行:
2.6 BaseLoadBalancer
继续跟踪源码,发现这里执行的是父类(BaseLoadBalancer)的方法:
看到这个类的时候,有没有想起来之前我们在进行负载均衡选择服务的时候,idea显示的loadBalancer前缀是DynamicServerListLoadBalancer,但是具体的实例却是ZoneAwareLoadBalancer,由此可以猜想,ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer或者是继承了DynamicServerListLoadBalancer的子类,而现在重写父类chooseServer(key)方法却走到了BaseLoadBalancer,下面我们来看看这三个类的关系,选中ZoneAwareLoadBalancer类,然后按ctrl+alt+u:
2.7 DynamicServerListLoadBalance
从上面的继承关系图来看,DynamicServerListLoadBalance是ZoneAwareLoadBalancer的父类,BaseLoadBalancer是DynamicServerListLoadBalance的父类,那么在ZoneAwareLoadBalancer执行super.chooseServer(key)方法的时候,走的应该是DynamicServerListLoadBalance的方法,但是实际的调试,却没有进来,我们来看看DynamicServerListLoadBalance这个类:
这里显示,他是一个具有使用动态获取服务器列表能力的LoadBalancer。同时,具有筛选过滤的作用。因为,在ZoneAwareLoadBalancer执行super.chooseServer(key)方法的时候,没有调用DynamicServerListLoadBalance的方法,我们来看看它是不是没有chooseServer(key)方法,选定类名按住 alt+7 查看它所有方法:
果然没有chooseServer(key)方法,那么调用父类的父类就没有什么问题。
2.8 IRule
解决掉疑问之后,我们继续回到源码BaseLoadBalancer的chooseServer方法,发现这么一段代码:
从字面来看,rule就是负载均衡的规则,进行服务选择的就是这个rule,我们看看这个rule是谁:
这里的rule默认值是一个RoundRobinRule,看类的介绍:
但是我们之前的断点显示,我们走的规则是ZoneAvoidanceRule,我们来看看调用的规则跟IRule接口的关系:
因为断点走的是ZoneAvoidanceRule类,我们继续跟进:
2.9 PredicatBasedRule
然后发现,ZoneAvoidanceRule没有实现choose这个方法,但是他的父类PredicatBasedRule有,于是,我们继续跟进去:
发现这个规则类,在过滤后,会以轮询的方式从过滤列表中返回。
2.10 ZoneAwareLoadBalancer遗留的问题
之前,我们在ZoneAwareLoadBalancer这个小节点的时候,就是idea显示的loadBalancer前缀是DynamicServerListLoadBalancer,那么后面又怎么变成了ZoneAwareLoadBalancer呢?在调用getLoadBalancer(serviceId)方法的时候,我们发现是通过AnnotationConfigApplicationContext来维护的,也就是说,ZoneAwareLoadBalancer是在Ribbon客户端启动的时候就创建好了的。接下来,我们来验证一下。按住 alt+F7 搜索一下有哪些地方实现了ZoneAwareLoadBalancer类:
2.11 RibbonClientConfiguration
我们点进去看一下,打个断点,又因为Ribbon客户端在不设置的时候默认的是懒加载,只有在重启服务后第一次发起请求才会创建,所以我们重新启动order-service服务,发起第一次请求,看看是不是这里:
发现的确是这样,也就是因为这样,我们最后调用的实例是ZoneAwareLoadBalancer。到这里,整个负载均衡的流程我们就都清楚了。
2.12 总结
SpringCloudRibbon的底层采用了一个拦截器,拦截RestTemplate发出的请求,对地址做了修改。用一幅图来总结一下:
基本流程如下:
- 拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求的url(代码里面获取的是uri)中获取服务名称,也就是userservice
- DynamicServerListLoadBalancer(实例则是ZoneAwareLoadBalancer)根据userservice到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userserver,得到http://localhost:8081/user/1,发起真实请求
3. 负载均衡策略
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
不同的规则含义如下:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
默认实现就是ZoneAvoidanceRule,是一种轮询方案。
默认的情况下,浏览器访问http://localhost:8080/order/101,http://localhost:8080/order/102,http://localhost:8080/order/103,http://localhost:8080/order/104,访问之前,将服务器8081和8082的日志都清空(控制台右侧有个垃圾桶,点击一下):
发现8081实例是调用两次(2,4),8082实例也是调用两次(1,3),走的是轮询规则无疑了。
3.1 自定义负载均衡策略
通过定义IRule实现可以修改负载均衡规则,有两种方式:
1.代码方式:在order-service的OrderApplication类中,定义一个新的IRule,这里以RandomRule(随机规则)为例:
@Bean
public IRule randomRule(){
return new RandomRule();
}
下面,我们重启order-service,然后访问101,102,103,104做一个测试:
第一次随机的只调用了8082一台实例,再来测试第二次:
由此可见,的确是随机的规则,我们的代码方式成功了。
2.配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则:
userservice: # 给某个微服务配置负载均衡规则,这里是userservice
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
把之前的代码方式注释掉,重启order服务,测试101,102,103,104,105,106,107,108,结果如下:
发现配置的方式的效果的确跟代码方式一致,也实现了随机规则。
注意:
一般使用默认的负载均衡规则,不做修改。
另外,代码的方式作用的是全局,不管调用哪个服务,都是走的自定义规则。而配置文件的方式,只作用于定义的某一个或几个服务,没有被定义的服务还是走的默认的负载均衡规则。
4. 饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才回去创建LoadBalancerClient,请求时间会很长。 我们来验证一下,启动之后第一次发起请求:
第二次发起请求:
速度对比很明显,第一次请求比第二次慢了1s左右。为了解决这个问题,就出现了Ribbon的饥饿加载。懒加载是第一次访问时创建Ribbon客户端,饥饿加载则在项目启动时创建,这种方式降低了第一次访问的耗时。两种加载方式在日志里面看的很清楚:
上面是没有启动饥饿加载,也就是懒加载时order-service服务的启动记录。下面开始第一次发起请求:
这里才开始加载LoadBalancer,他的实例是BaseLoadBalancer。我们来看看它的介绍以及它跟LoadBalancer之间的关系:
那么饥饿加载要怎么开启呢?只要在order-service的application里配置就可以:
ribbon:
eager-load:
enabled: true
clients: userservice
上面的方式是只针对userservice开启饥饿加载模式,如果需要对多个服务开启懒加载时变为:
我这里只有一个服务,后续加其他服务,换一行直接加上 - xxx(服务名) 即可。
那么在饥饿加载模式下,order-service服务的启动日志是什么样的。我们来看看,重启order服务:
5. 总结
Ribbon负载均衡规则:
- 规则的接口IRule
- 默认实现时ZonAvoidanceRule,根据zone选择服务列表,然后轮询
负载均衡自定义方式:
- 代码方式:配置灵活,用于全局,但修改时需要重新打包
- 配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
饥饿加载:
- Ribbon客户端的加载方式默认时懒加载,如果需要开启饥饿加载enabled-load.enabled需要配置为true
- 指定饥饿加载的微服务,单个服务在clients:后面直接加上名称即可,如果是多个服务,要改成clients:- xxx(服务名称)