负载均衡 - Ribbon

负载均衡 - Ribbon1. Ribbon体系架构解析2. 懒加载和饥饿加载-一定是环境问题3. 负载均衡策略-七种策略3.1 RandomRule - 随性而为3.2 RoundRobinRule - 按部就班3.3 RetryRule - 卷土重来3.4 WeightedResponseTimeRule - 能者多劳3.5 BestAvailableRule - 让最闲的人来3.6 AvailabilityFilteringRule - 我是有底线的3.7 ZoneAvoidanceRule - 我的
摘要由CSDN通过智能技术生成

1. Ribbon体系架构解析

  在SpringCloud的世界观中,Ribbon 作为负载均衡器,具有如下特点:

  1. 丰富的组件库 整套负载均衡由 7个具体策略组成,不管你是什么特殊需求,都有合适的策略供你选择;
  2. 给谁都能用 适配性好,跟谁都能搭配,SpringCloud 里的五小强(eureka,feign,gateway,zuul,hystrix),谁拿都能用。

  更牛的是 Ribbon 可以脱离 SpringCloud 应用在一般项目中。
在这里插入图片描述

  Ribbon 的基本工作模式(IPing + IRule),一个 HttpRequest 发过来,先被转发到 Eureka 上。此时 Eureka 仍然通过服务发现获取了所有服务节点的物理地址,但问题是他不知道该调用哪一个,只好把请求转到了 Ribbon 手里:

  • IPing IPing 是 Ribbon 的一套 healthcheck 机制,故名思议,就是要 Ping 一下目标机器看是否还在线,一般情况下 IPing 并不会主动向服务节点发起 healthcheck 请求,Ribbon 后台通过静默处理返回 true 默认表示所有服务节点都处于存活状态(和 Eureka 集成的时候会检查服节点 UP 状态);
  • IRule 这就是 Ribbon 的组件库了,各种负载均衡策略都继承自 IRule 接口。所有经过 Ribbon 的请求都会先请示 IRule 一把,找到负载均衡策略选定的目标机器,然后再把请求转发过去。

  

2. 懒加载和饥饿加载-一定是环境问题

测试妹子:老师你写的接口怎么又timeout了?报个bug先
RD老师:一定是你机器问题,在我机器上就没有问题
测试妹子:可是我换了台机子,一启动还是超时啊
RD老师:这肯定是环境问题唉,肯定你配置不对
测试妹子:可是所有配置都一样啊?
RD老师:这种随机问题不可能是代码问题,本地又不能重现,以后碰到保留现场再来找我。
就这样,一个疑似的 bug 就这么被放过了,这到底是不是个问题呢?

  凭着以往的经验,本地无法重现,那就去服务器 log 里寻找蛛丝马迹。不巧还真被我找到这么一条疑似异常,在超时请求之前有这么一条 log:

INFO [main] com.netflix.loadbalancer.DynamicServerListLoadBalancer -
DynamicServerListLoadBalancer for client eureka-consumer initialized

  这行 log 意思是,Ribbon 客户端已经完成了 LoadBalancer 的初始化。初始化是没问题的,但问题是初始化发生的时间,为什么偏偏发生在首次超时之前呢?

  
  这就要从 Ribbon 的懒加载说起了,原来 Ribbon 是在第一次方法调用的时候才去初始化 LoadBalancer。这样看来,第一个方法请求不仅仅包含 HTTP 连接和方法的响应时间,还包括了 LoadBalancer 的创建耗时。假如你的方法本身就比较耗时的话,而且超时时间又设置的比较短,那么很大可能这第一次 http 调用就会失败。其实还有很多框架也实现了类似的懒加载功能,比如 Hibernate 的 lazy-fetch,懒加载在大部分情况下可以节省系统资源开销,但某些情况下反而导致服务响应时间被延长。

  
  既然 LoadBalancer 首次加载发生在第一次调用的时候,那么我们让 Ribbon 在调用以前就初始化好了。接下来我们就开启 Ribbon 的狂暴模式-饥饿加载

ribbon.eager-load.enabled=true 
ribbon.eager-load.clients=ribbon-consumer

  第一个参数开启了 Ribbon 的饥饿加载模式,第二个属性指定了需要应用饥饿加载的服务名称。完成上面配置并再次重启服务,就会发现 LoadBalancer 初始化日志在方法调用之前就打印出来了。

  

3. 负载均衡策略-七种策略

  我们学习了 LoadBalancer 的加载方式,那么大家知道 LoadBalancer 底层是怎么做具体的分发吗?这里我们就打开 Ribbon 的工具箱,来看一下 LoadBalancer 下面更底层的7种内置的负载均衡策略,他们是 Ribbon 实现负载均衡的核心类。

  

3.1 RandomRule - 随性而为

  从名字就能看出,这是个很随性的策略,随性到什么程度呢?它会从当前可用的服务节点中,随机挑选一个节点访问。

  
  这分明是乱拳打死老师傅的路子吗,哪有负载均衡的智慧?别急,这里为了打死老师傅还真用了两个小花招,使用了 yield + 自旋的方式做重试,还采用了严格的防御性编程。

  

3.2 RoundRobinRule - 按部就班

  这个 rule 是 RandomRule 的亲兄弟,RandomRule 是随性而为挑选节点,RobinRule 却按部就班从一个节点一步一步地向后选取节点,既不会跳过一个,也不会原地踏步,每一次只向后移动一步。
在这里插入图片描述

  也许会问了,假如在多线程环境下,两个请求同时访问这个 Rule 是否会读取到相同节点呢?不会,这靠的是 RandomRobinRule 底层的自旋锁 + CAS 的同步操作。CAS 的全称是 compare and swap,是一种借助操作系统函数来实现的同步操作。前面讲到过 Eureka 为了防止服务下线被重复调用,就使用 AtomicBoolean 的 CAS 方法做同步控制,CAS + 自旋锁这套组合技是高并发下最廉价的线程安全手段,因为这套操作不需要锁定系统资源。当然了,有优点必然也有缺点,自旋锁如果迟迟不能释放,将会带来 CPU 资源的浪费,因为自旋本身并不会执行任何业务逻辑,而是单纯的使 CPU “空转”。所以通常情况下会对自旋锁的旋转次数做一个限制,比如 JDK 中 synchronize 底层的锁升级策略,就对自旋次数做了动态调整。

// CAS+自旋锁获取系统资源的打开方式,真实应用中还要注意防止无休止自旋:

// 或者for (;;) 做自旋
while (true) { 
	// cas操作
	if (cas(expected, update)) {
		// 业务逻辑代码
		// break或退出return
	}
}

  

3.3 RetryRule - 卷土重来

  RetryRule 是一个类似装饰器模式的 Rule,我们前面学习服务注册的时候了解过,装饰器相当于一层套一层的俄罗斯娃娃,每一层都会加上一层独特 BUFF,我们这里复习一下装饰器的结构
在这里插入图片描述

  RetryRule 也是同样的道理,他的 BUFF 就是给其他负载均衡策略加上“重试”功能。而在 RetryRule 里还藏着一个 subRule,这才是隐藏在下面的真正被执行的负载均衡策略,RetryRule 正是要为它添加重试功能(如果初始化时没指定 subRule,将默认使用 RoundRibinRule)。

  

3.4 WeightedResponseTimeRule - 能者多劳

  这个 Rule 继承自 RoundRibbonRule,他会根据服务节点的响应时间计算权重,响应时间越长权重就越低,响应越快则权重越高,权重的高低决定了机器被选中概率的高低。也就是说,响应时间越小的机器,被选中的概率越大。
在这里插入图片描述

  由于服务器刚启动的时候,对各个服务节点采样不足,因此会采用轮询策略,当积累到一定的样本时候,会切换到 WeightedResponseTimeRule 模式。

  

3.5 BestAvailableRule - 让最闲的人来

在这里插入图片描述

  应该说这个 Rule 有点智能的味道了,在过滤掉故障服务以后,它会基于过去 30分钟的统计结果选取当前并发量最小的服务节点,也就是最“闲”的节点作为目标地址。如果统计结果尚未生成,则采用轮询的方式选定节点。

  • 过滤故障服务;
  • 选取并发量最小的节点。

  

3.6 AvailabilityFilteringRule - 我是有底线的

  这个规则底层依赖 RandomRobinRule 来选取节点,但并非来者不拒,它也是有一些底线的,必须要满足它的最低要求的节点才会被选中。如果节点满足了要求,无论其响应时间或者当前并发量是什么,都会被选中。
在这里插入图片描述

  每次 AvailabilityFilteringRule(简称AFR)都会请求 RobinRule 挑选一个节点,然后对这个节点做以下两步检查:

  • 是否处于熔断状态(熔断是 Hystrix 中的知识点,这里大家可以把熔断当做服务不可用);
  • 节点当前的 active 请求连接数超过阈值,超过了则表示节点目前太忙,不适合接客。

  如果被选中的 server 不幸挂掉了检查,那么 AFR 会自动重试(次数最多 10次),让 RobinRule 重新选择一个服务节点。

  

3.7 ZoneAvoidanceRule - 我的地盘我做主

  这个过滤器包含了组合过滤条件,分别是 Zone 级别和可用性级别。

在这里插入图片描述

  • Zone Filter: 在 Eureka 注册中一个服务节点有 Zone, Region 和 URL 三个身份信息,其中 Zone 可以理解为机房大区(未指定则由 Eureka 给定默认值),而这里会对这个 Zone 的健康情况过滤其下面所有服务节点;
  • 可用性过滤:这里和 AvailabilityFilteringRule 的验证非常像,会过滤掉当前并发量较大,或者处于熔断状态的服务节点。

  

4. 负载均衡器LoadBalancer原理解析

  上面我们学习了 Ribbon 的 7种内置负载均衡策略,那么大家知道是谁在这中间牵线搭桥,把配置的负载均衡策略“加塞”到一个 HTTP 请求的调用链路里的吗?这就要讲到 Ribbon 对 RestTemplate 的改造。

  

4.1 传送门 - @LoadBalanced

  在上学的时候很喜欢玩一款叫《英雄无敌》的游戏,这是一个魔法战争的游戏,充满着魔幻神秘色彩。这个游戏里有一个叫做“传送门”的机关,当你控制的英雄进入传送门的时候,他便会从另一个世界的另一扇门中出现。

  
  当上帝为你关上一扇门的时候,也会为你开启另一扇门。Ribbon 也有这么一个魔法门,就是 @LoadBalanced 注解,它会将 RestTemplate 传送到 Ribbon 的自动装配类里进行改造。我们来看下它是怎么做的:
在这里插入图片描述

  • @LoadBalanced 这个注解一头挂在 RestTemplate 上,另一头挂在 LoadBalancerAutoConfiguration 这个类上。它就像连接两个世界的传送门,将所有顶着「LoadBalanced」注解的 RestTemplate 类,都传入到 LoadBalancerAutoConfiguration 中。如果要深挖底层的作用机制,大家可以发现这个注解的定义上还有一个 @Qualifier 注解,可能对 Spring 比较熟悉的就恍然大悟了。@Qualifier 注解搭配 @Autowired 注解做自动装配,可以通过 name 属性,将指定的 Bean 装载到指定位置(即使有两个同样类型的 Bean,也可以通过Qualifier 定义时声明的 name 做区分)。这里「LoadBalanced」也是借助 Qualifier 实现了一个给 RestTemplate 打标签的功能,凡是被打标的 RestTemplate 都会被传送到 AutoConfig 中做进一步改造;
  • LBAutoConfig 从前一步中传送过来的 RestTemplate,会经过 LBAutoConfig 的装配,将一系列的 Interceptor(拦截器)添加到 RestTemplate 中。拦截器是类似职责链编程模型的结构,我们常见的 ServletFilter,权限控制器等,都是类似的模式。Ribbon 拦截器会拦截每个网络请求做一番处理,在这个过程中拦截器会找到对应的 LoadBalancer 对 HTTP 请求进行接管,接着 LoadBalancer 就会找到默认或指定的负载均衡策略来对 HTTP 请求进行转发。

  
  总结 Ribbon 的作用机制就是,由 LoadBalanced 在 RestTemplate 上打标,Ribbon 将带有负载均衡能力的拦截器注入标记好的 RestTemplate 中,以此实现了负载均衡。

  

4.2 没事看电视 - IPing机制

  在计划生育实施的初期,效果并不好,因为那时候没有如今各种丰富的娱乐活动,大家下了班都闲着没事干,闲着没事那就只好造人了。后来有个村支书想了个法子,他给每家配了一台电视机,这样一来每家每户晚上就有事情可干了,超生指标立马就降下来了。这说明了一个问题,闲着没事就得要找点事干。

  Ribbon 也正面临这么个问题,和其它 SpringCloud 组件相比,Ribbon 可谓称得上闲差,因为它的职责相对单一,只应用在负载均衡方面,说明它太闲了,所以这才要给 Ribbon 找点事儿干。

  
  IPing 机制就是 SpringCloud 发给 Ribbon 家的电视机,那它放哪些电视节目呢?现在请收看电视节目《撩妹指南》
在这里插入图片描述

  IPing 是一个主动出击撩妹子的机制,他主动判断服务节点的当前状态,决定是否可作为目标节点,只有当前可用的节点才会作为负载均衡器的目标节点。IPing 有以下几个撩妹手段:

  • 自娱自乐式:DummyPing,默认返回 true,即认为所有节点都可用,这也是单独使用Ribbon 时的默认模式;
  • 隔山打牛式:NIWSDiscoveryPing,借助 Eureka 服务发现机制获取节点状态,假如节点状态是 UP 则认为是可用状态;
  • 主动出击式:PingUrl,这才是真正走出家门去撩妹,它会主动向服务节点发起一次 http 调用,如果对方有响应则认为节点可用。

  大家可以看出第三种主动出击的模式比较生猛,IPing 就像一条脱缰的野狗一样,对每个服务节点撩个不停,节点们纷纷表示扛不住扛不住。大家可以想象,假如我们的服务节点搭载的是淘宝服务(负责计算商品、购物车与订单优),服务下随便一个微服务都有大几千台服务器,在服务本身就被超高访问量调用的情况下,那这种主动出击的 IPing 策略必然会大大增加服务节点的访问压力。

  
  既然 Eureka 已经有了服务发现机制,可以获取节点的当前状态,拿来就用岂不更好?因此,除非特殊指定,在和 Eureka 搭配使用的时候,采用的是第二种隔山打牛式,也就是过滤非 UP 状态的节点(其实这个功能直接放 Eureka 里也能做。。。看来真的是没事给 Ribbon 找点事)

  

5. 如何针对具体业务采用合适的负载均衡策略

  前面我们一直在学习负载均衡策略,话说条条大路通罗马,既然 Ribbon 提供了这么多的策略,我该用哪个策略才好呢?都说原配的才是最合适的,那针对具体业务我们都有哪些需要注意的呢?

  

5.1 Retry - 有多少爱可以重来

  正所谓有的爱一旦过去就过去了,有些人你永远不必等。比如说,我们开发了一个库存补货接口,每次调用后就自动补货上架 100个商品。假如这个接口发生了超时,调用方收到timeout异常,但实际上库存服务在后台还在执行,只不过最终结果无法通知到调用方。也就是说实际补货成功,但调用方那边却超时了。

  
  在上面这种情况下,如果一次次不停重试,可能你马上就要爆仓了。之所以类似的服务不能进行 retry 的原因,在于接口没有实现幂等性。

  幂等性往往针对的是执行“update”操作的接口,也就是常说的“写”操作。简单理解就是对一个具备幂等性的接口进行一次请求调用,和多次请求调用(每次调用的参数也相同),在执行结果上没有区别,接口并不会因为多调用了几次就产生不同结果。因此就需要从业务层面和代码层面做资源检查或锁定,来保证幂等性,这也是目前常用的分布式事务TCC方案的核心知识点。

  

5.2 用时间换空间

  在魔都买房子就得采取“用时间换空间”的策略,甭管老破小先上车再说,过个几年再换个大house。在 Ribbon 这里,时间和空间经常要被换来换去,时间代表着接口响应时间(下文简称RT:Response Time),空间表示服务器的可用连接数。

  
  在 Ribbon 里有两个和时间与空间密切相关的负载均衡策略,BestAvailableRule(简称BA)和WeightedResponseTimeRule(简称WRT)。他们都有同一个梦想,那就是希望世界和平,那就是选择压力较小的服务节点,但这两个策略努力的方向不同。BA 会根据服务节点过去一段时间的请求数,选择并发量最小的机器(选择空间);WRT 则是根据响应时间的统计结果,选择响应时间最快的服务(选择时间)。

  
  我们知道服务的 RT 受很多因素制约,服务本身响应时间,网络连接时间,容器状态甚至 JVM 的 full GC 等等都会影响最终的 RT。我们来设想这样一个场景,现在有一个非常轻量级的微服务,他的业务代码耗时大概在 2ms范围内,只占整个接口响应时间的 20%,而剩下 80%基本都用在了网络连接的开销上。

  
  在上面这个例子中,如果我们以 RT 作为指标,其实并不能客观获取服务节点当前的性能数据,因为接口本身的处理时间在 RT 分布中只占有很小的比例,甚至短时间的网络抖动都会对 RT 采样造成很大影响。而由于接口响应时间较短,因此性能瓶颈更容易被连接线程数卡住。线程数量达到上限会延长新请求的等待时间,从而增加 RT,但这种情况下 active 的线程数量有更灵敏的指示作用,因为等到 RT 显著增加的时候,线程池可能早已被吃满了。对待这类问题,我们的实践经验是:

  • 连接数敏感模型 对响应时间较短,或RT和业务复杂度是非线性相关关系的接口,采用基于可用连接数的负载均衡策略更加合适。

  
  同样的,假设某个接口比较重量级,接口的处理时间与接收到的参数强相关。打个比方,订单导出服务,如果发起 10个请求,每个请求都需要导出当前用户过去一整年的订单数据,那么这 10个请求都会耗费大量的系统资源(CPU,内存)参与业务,同时RT时间也会相应拉长。在另一台机子上,同样是 10个请求,但是只需要导出 1个月的数据,相比较第一台机器,连接数相等的情况下,系统资源的占用率却大大不同。在这样的场景下,基于RT的指标具有更高的敏感度,我们的实践经验是:

  • RT敏感模型 对重量级接口,尤其是根据参数不同会导致系统资源使用率浮动较大的接口(RT与业务复杂度线性相关),建议采用基于响应时间的负载均衡策略。

  

5.3 当断则断

  假如集成了 Hystrix 熔断器,而当前服务正处于熔断状态,你还想往火坑里跳吗?这时我们就需要根据熔断状态做过滤,使用 AvailabilityFilteringRule 便是极好的。

  

5.4 I Don’t Care

  那还说啥,默认负载均衡策略走起不就不行了。Who cares.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值