Spring Cloud Hystrix 服务容错保护

在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身间题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。

举个例子,在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、评论等一系列服务单元。用户创建一个订单的时候,客户端将调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构更加不稳定。

为了解决这样的问题, 产生了断路器等一系列的服务保护机制。“断路器”本身是一种开关装置,用于在电路上保护线路过载,当线路中有电器发生短路时,"断路器”能够及时切断故障电路,防止发生过载、发热甚至起火等严重后果。

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

针对上述问题,Spring Cloud Hystrix [hɪst’rɪks] 实现了断路器线程隔离等一系列服务保护功能。它也是基于 Netflix 的开源框架 Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级服务熔断线程和信号隔离请求缓存请求合并以及服务监控等强大功能。

依赖隔离

Docker 通过“舱壁模式”实现进程的隔离,使得容器与容器之间不会互相影响。而 Hystrix 则使用该模式实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务。

通过实现对依赖服务的线程池隔离,可以带来如下优势:

  • 应用自身得到完全保护,不会受不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的其余部分。

  • 可以有效降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响应用其他的请求。

  • 当依赖的服务从失效恢复正常后,它的线程池会被清理并且能够马上恢复健康的服务,相比之下,容器级别的清理恢复速度要慢得多。

  • 当依赖的服务出现配置错误的时候,线程池会快速反映出此问题(通过失败次数、延迟、超时、拒绝等指标的增加情况)。同时,我们可以在不影响应用功能的情况下通过实时的动态属性刷新(通过Spring Cloud Config 与 Spring Cloud Bus的 联合使用) 来处理它。

  • 当依赖的服务因实现机制调整等原因造成其性能出现很大变化的时候,线程池的监控指标信息会反映出这样的变化。同时,我们也可以通过实时动态刷新自身应用对依赖服务的阙值进行调整以适应依赖方的改变。

  • 除了上面通过线程池隔离服务发挥的优点之外,每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问

总之, 通过对依赖服务实现线程池隔离,可让应用更加健壮,不会因为个别依赖服务出现问题而引起非相关服务的异常。 同时,也使得我们的应用变得更加灵活,可以在不停止服务的情况下,配合动态配置刷新实现性能配置上的调整。

虽然线程池隔离的方案带来如此多的好处,但是很多使用者可能会担心为每一个依赖服务都分配一个线程池是否会过多地增加系统的负载和开销。 对于这一点,Netflix 在设计 Hystrix 的时候,认为线程池上的开销相对于隔离所带来的好处是无法比拟的。

对于大多数需求来说带来的性能消耗是微乎其微的,更何况可为系统在稳定性和灵活性上带来巨大的提升。虽然对于大部分的请求可以忽略线程池的额外开销,而对于小部分延迟本身就非常小的请求(可能只需要 1ms), 那么带来的延迟开销还是非常昂贵的。

Hystrix 也为此设计了另外的解决方案:信号量。在 Hystrix 中除了可使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销远比线程池的开销小但是它不能设置超时和实现异步访问。所以,只有在依赖服务足够可靠的情况下才使用信号量。

请求缓存

当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大。在分布式环境下,通常压力来自于对依赖服务的调用,因为请求依赖服务的资源需要通过通信来实现,这样的依赖方式比进程内的调用方式会引起一部分的性能损失,同时 HTTP 相比其他高性能的通信协议在速度上没有任何优势,所以它有些类似于对数据库这样的外部资源进行读写操作,在高并发的情况下可能会成为系统的瓶颈。

既然如此,很容易联想到类似数据访问的缓存保护是否也可以应用到依赖服务的调用上呢?答案显而易见,在高并发的场景之下,Hystrix 中提供了请求缓存的功能,可以方便地开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗降低请求响应时间的效果。

通过开启请求缓存具备下面几项好处:

  • 减少重复的请求数,降低依赖服务的并发度
  • 在同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致
  • 请求缓存在 run() 和 construct() 执行之前生效,所以可以有效减少不必要的线程开销

请求合并

微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗连接数(线程数)占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将出现排队等待响应延迟的清况。为了优化这两个问题, Hystrix 提供了 HystrixCollapser 来实现请求的合并,以减少通信消耗和线程数的占用。

在使用了 HystrixCollapser 请求合并器之后,同一时间发生的多个请求处于请求合并器的一个时间窗内,这些请求被请求合并器拦截下来,并在合并器中进行组合,然后将这些请求合并成一个请求发向批量处理接口。在获取到批量请求结果之后,通过请求合并器再将批量结果拆分并分配给每个被合并的请求。

通过使用请求合并器有效减少了对线程池中资源的占用。所以在资源有效并且短时间内会产生高并发请求的时候,为避免连接不够用而引起的延迟可以考虑使用请求合并器的方式来处理和优化。

请求合并的额外开销

虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源,但是在使用的时候也需要注意它所带来的额外开销: 用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高

例如,某个请求不通过请求合并器访问的平均耗时为 5 ms,请求合并的延迟时间窗为 10 ms (默认值),那么当该请求设置了请求合并器之后,最坏情况下(在延迟时间窗结束时才发起请求)该请求需要 15 ms 才能完成。由于请求合并器的延迟时间窗会带来额外开销,所以是否使用请求合并器需要根据依赖服务调用的实际情况来选择,主要考虑下面两个方面:

  • 请求命令本身的延迟。如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并器,因为延迟时间窗的时间消耗显得微不足道了。
  • 延迟时间窗内的并发量。如果一个时间窗内只有 1-2 个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才响应。相反,如果一个时间窗内具有很高的并发量,并且服务提供方也实现了批量处理接口,那么使用请求合并器可以有效 减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。

参考:

《Spring Cloud 微服务实战》翟永超 著

发布了66 篇原创文章 · 获赞 151 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/siriusol/article/details/105578262
今日推荐