软件架构场景之—— 熔断:如何预防一个服务故障崩掉整个系统?

业务场景

在一个新零售架构系统中,有一个通用用户服务(很多页面都会使用),它包含两个接口,第一个接口是用户状态接口,包含用户车辆所在位置,并且在用户信息展示页面都会使用到,比如客服系统中的用户信息页面。第二个接口是需要我们返回用户一个可操作的权限列表,它包含一个通用权限,也包含用户定制权限,而且每次用户打开 App 时都会使用它

第一个接口会遇到的问题:请求慢

用户状态的接口、服务间的调用关系如下图所示

在 Basic Data Service 中,有个接口 /currentCarLocation 需要调用第三方系统的数据,但第三方响应速度很慢且有时还会抽风,导致响应时间更长,接口经常出现超时报错

有一次,用户反馈 App 整体运行速度慢到无法接受的程度。通过后台监控,我们查看了几个 Thread Dump ,发现 User API 与 Basic Data Service 的线程请求数爆满,且所有的线程都在访问第三方接口。因为连接数满了,其他页面便不再受理 User API 的请求,最终导致 App 整体出现了卡顿,针对这个问题做过相关处理,考虑响应时间长,就把超时的时间设置很长,虽然超时报错概率小了,其他页面也保持正常,但是会导致客服后台查看用户信息的页面响应时间长

第二个接口会遇到的问题:流量洪峰缓存超时

用户权限的接口、服务间的调用关系与上面类似,如下图所示

关于服务间的关系调用具体流程分为以下三个步骤

  • App 访问 User API;
  • User API 访问基础数据服务的接口 /commonAccesses;
  • 基础数据服务提供一个通用权限列表。因为权限列表对所有用户都一样,所以我们把它放在了 Redis 中,如果通用权限在 Redis 中找不到,我们再去数据库中查找

在服务间的关系调用流程中,曾经遇到过的一些问题

因为历史代码的原因,在流量高峰时 Redis 中的通用权限列表超时了,那一瞬间所有的线程都需要去数据库中读取数据,导致 DB 中的 CPU 立马飙到了 100%,DB 挂后,紧接着 Basic Data Service 也挂了,因所有的线程堵塞了,获取不到数据库连接,导致 Basic Data Service 无法接受新的请求。而 User API 因调用了 Basic Data Service 的线程出现了堵塞,以至于 User API 服务的所有线程也出现堵塞,即 User API 也挂了,导致 App 上的所有操作都不能使用,所以出大事了

 

技术选型

为了解决以上两个问题,需要引入一个技术,且它还得满足以下两个条件

(1)线程隔离

针对第一个问题,希望的处理方式是这样,比如 User API 中每个服务配置的最大连接数是 1000,每次 API 调用 BasicDataService 的 /currentCarLocation 的速度就会很慢

因此,我们希望控制 /currentCarLocation 的调用请求数,保证不超过 50 条,以此保证至少还有 950 条的连接可用来处理常规请求。如果 /currentCarLocation 的调用请求数超过 50 条,我们就设计一些备用逻辑进行处理,比如在界面上给用户进行提示

(2)熔断

针对第二个问题,由于 DB 没有死锁,流量洪峰缓存超时单纯是因为压力太大,此时我们可以使用 Basic Data Service 暂缓一点儿时间,让它不接受新的请求,这样 Redis 的数据会被补上,数据库的连接也会降下来,我们的服务也就没事了

因此,希望这个技术能实现以下两点需求

  • 发现近期某个接口的请求老出异常,先别访问接口的服务;
  • 发现某个接口的请求老超时,先判断接口的服务是否不堪重负,如果不堪重负,先别访问它

 

Hystrix 的设计思路

Spring Cloud Hystrix 的设计思想是事前配置熔断机制,也就是说,要事先预见流量是什么情况?系统负载能力如何?然后预先配置好熔断的机制。但这种操作的缺点是,一旦实际流量或系统状况与预测的不一样,那么预先配置好的机制就达不到预期的效果,因此,开源 Hystrix 的公司 Netflix 想使用一个动态适应更灵活的熔断机制。不过 2018 年后官方已不再开发新功能,转向开发 Resilience4j 了,对于原有功能只做简单维护

Hystrix 为什么能满足我们的需求?

  • (1)线程隔离机制

在 Hystrix 机制中,当前服务与其他接口存在强依赖关系,且每个依赖都有一个隔离的线程池

比如下面这张架构图,当前服务调用接口 A 时,并发线程的最大个数是 10,调用接口 M 时,并发线程的最大个数是 5

一般来说,当前服务依赖的一个接口响应慢时,当前运行的线程会一直处于未释放状态,最终把所有的连接线程卷入慢接口中。为此,在隔离线程的过程中,Hystrix 的做法是每个依赖接口(也可以配置成几个接口共用)维护一个线程池,然后通过线程池的大小、排队数等隔离每个服务对依赖接口的调用,这样就不会出现前面的问题

当然,在 Hystrix 机制中,我们除了使用线程池来隔离线程,还可以使用信号量(计数器)

比如还是调用接口 A,因并发线程的最大个数是 10,在信号量隔离的机制中,Hystix 并不使用 1 个 size 为 10 的线程池来隔离,而是使用一个信号 semaphoresA,每当调用接口 A 时 semaphoresA++,A 调用完后 semaphoresA--,semaphoresA 一旦超过 10,不再调用

因为我们在使用线程池时经常需要切换线程,资源损耗较大,而信号量的优点恰巧就是切换快,大大解决了我们的烦恼。不过它也有一个缺点,即接口一旦开始调用就无法中断。因为调用依赖的线程是当前请求的主线程,不像线程隔离,调用依赖的是另外 1 个线程,当前请求的主线程可以根据超时时间把它中断

  • (2)熔断机制

1. 在哪种条件下会触发熔断?

熔断判断规则是某段时间内调用失败数超过特定的数量或比率时,就会触发熔断。那这个数据是如何统计出来的呢?

在 Hystrix 机制中,我们会配置一个不断滚动的统计时间窗口 metrics.rollingStats.timeInMilliseconds,在每个统计时间窗口中,当调用接口的总数量达到 circuitBreakerRequestVolumeThreshold,且接口调用超时或异常的调用次数与总调用次数的占比超过 circuitBreakerErrorThresholdPercentage,此时就会触发熔断

2. 熔断了会怎么样?

如果熔断被触发了,在 circuitBreakerSleepWindowInMilliseconds 的时间内,我们便不再对外调用接口,而是直接调用本地的一个降级方法,如下代码所示

@HystrixCommand(fallbackMethod = "getCurrentCarLocationFallback")

3. 熔断后怎么恢复?

circuitBreakerSleepWindowInMilliseconds 到时间后,Hystrix 首先会放开对接口的限制(断路器状态 HALF-OPEN),然后尝试使用 1 个请求去调用接口,如果调用成功,则恢复正常(断路器状态 CLOSED),如果调用失败或出现超时等待,就需要再重新等待circuitBreakerSleepWindowInMilliseconds 的时间,之后再重试

  • (3)滚动(滑动)时间窗口

比如我们把滑动事件的时间窗口设置为 10 秒,并不是说我们需要在 1 分 10 秒时统计一次,1 分 20 秒时再统计一次,而是我们需要统计每一个 10 秒的时间窗口

因此,我们还需要设置一个 metrics.rollingStats.numBuckets,假设我们设置 metrics.rollingStats.numBuckets 为 10,表示时间窗口划分为 10 小份,每 1 份是 1 秒。然后我们就会 1 分 0 秒 - 1 分 10 秒统计 1 次、1 分 1 秒 - 1 分 11 秒统计 1 次、1 分 2 秒 - 1 分 12 秒统计 1 次……(即每隔 1 秒都有 1 个时间窗口)

下图就是 1 个 10 秒时间窗口,我们把它分成了 10 个桶

每个桶中 Hystrix 首先会统计调用请求的成功数、失败数、超时数和拒绝数,再单独统计每 10 个桶的数据(到了第 11 个桶时就是统计第 2 个桶到第 11 个桶的合计数据)

  • (4)Hystrix 调用接口的请求处理流程

这是 1 次调用成功的流程,如下图所示

这是 1 次调用失败的流程,如下图所示

Hystrix 调用接口的请求处理流程结束后,我们就可以直接启用它了。在 Spring Cloud 中启用 Hystrix 的操作也比较简单,不过多赘述了。最后,关于 Hystrix,它还有包含 request caching(请求缓存) 和 request collapsing(请求合并)这两个功能,它们与熔断关系不大

 

注意事项

把 Hystrix 的设计思路搞清楚后,使用它之前还需要考虑几个注意事项

(1)数据一致性

假设服务 A 更新了数据库,在调用服务 B 时直接降级了,那服务 A 的数据库更新是否需要回滚?

再举一个复杂点的例子,比如服务 A 调用了服务 B,服务 B 调用了服务 C,我们在服务 A 中成功更新了数据库并成功调用了服务 B,而服务 B 调用服务 C 时降级了,直接调用了 Fallback 方法,此时就会出现两个问题:服务 B 向服务 A 返回成功还是失败?服务 A 的数据库更新需不需要回滚?

以上两个例子体现的就是数据一致性的问题。关于这个问题并没有一个固定的设计标准,只是在不同需求下使用熔断时,结合具体的情况设计即可

(2)超时降级

比如服务 A 调用服务 B 时,因为调用过程中 B 没有在设置的时间内返回结果,被判断超时了,所以服务 A 又调用了降级的方法,其实服务 B 在接收到服务 A 的请求后,已经在执行工作并且没有中断。等服务 B 处理成功后,还是会返回处理成功的结果给服务 A。可是服务 A 又已经走了降级的方法,而服务 B 又已经把工作做完了,此时就会导致服务 B 中的数据出现异常

(3)用户体验

请求触发熔断后,一般会出现以下三种情况

  • 用户读数据的请求时遇到有些接口降级了,导致部分数据获取不到,这时我们需要在界面上给用户提供一定的提示,或让用户发现不了这部分数据的缺失;
  • 用户写数据的请求时,熔断触发降级后,有些写操作就会改为异步,后续处理对用户没有任何影响,但我们要根据实际情况判断是否需要给用户提供一定的提示;
  • 用户写数据的请求时,熔断触发降级后,操作可能就回滚掉,此时我们必须提示让用户重新操作

因此,服务调用触发了熔断降级时,需要把这些情况都考虑到以此保证用户体验,而不是仅仅保证服务器不宕机

(4)熔断监控

熔断使用上线后,其实我们只是完成了熔断设计的第一步。因为 Hystrix 是一个事前配置的熔断框架,关于熔断配置到底对不对,效果好不好,只有实际使用后才知道

实际使用时,还需要盯着 Hystrix 的监控面板查看各个服务的熔断数据,然后根据实际情况再做调整。只有这样,我们才能在真正使用熔断时将服务器的异常损失降到最低

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值