微服务之Ribbon组件

目录

前言

1. 负载均衡原理

2. 源码跟踪

2.1 LoadBalanced

2.2 LoadBalancerAutoConfiguration

 2.3 LoadBalancerInterceptor(重点)

 2.4 RibbonLoadBalancerClient

 2.5 ZoneAwareLoadBalancer

2.6 BaseLoadBalancer 

 2.7 DynamicServerListLoadBalance

2.8 IRule

 2.9 PredicatBasedRule

2.10 ZoneAwareLoadBalancer遗留的问题

 2.11 RibbonClientConfiguration

2.12 总结

 3. 负载均衡策略

3.1 自定义负载均衡策略

 4. 饥饿加载

5. 总结


前言

上一篇的文中有提到,只要我们给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地址,由此我们可以猜想,在实现LoadBlancerClientexecute(执行)方法之前,应该有一个拦截器,能够把我们的请求拦截做一些处理。那么,我们现在去看看是不是跟我们的猜想一样,选中接口名称,按住 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.loadBalancerLoadBalancerClient类型,我们继续跟入。

 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

从上面的继承关系图来看,DynamicServerListLoadBalanceZoneAwareLoadBalancer的父类,BaseLoadBalancerDynamicServerListLoadBalance的父类,那么在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/101http://localhost:8080/order/102http://localhost:8080/order/103http://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(服务名称)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值