之前给大家讲了很多服务之前的关系,今天主要给大家介绍,当服务出现问题时,我们该如何解决。
首先我们先进行一些场景分析:
- 场景一
服务提供端提供了A、B、C、D 4个服务,服务调用方调用服务时,D服务出现了问题,导致调用服务D的请求都出现了超时或者错误,从而进一步影响了整个请求队列的性能。
- 场景二
我们根据client1和client2 的请求压力确定了我们serverA的负载情况,但是当client2的请求从1500 增加到15000 时,会远远大于ServerA 的性能瓶颈,这个时候也会导致Client1的请求出现超时或者失败的情况。
总结一下,会有哪些情形导致我们服务出现不可以,需要我们做容错呢?
- 单个节点故障,可能被无限向上放大
- 多租户相互影响
- 瞬时流量激增,系统扛不住
针对上面总结的场景,我们有哪些解决方案呢?
- 资源隔离
- 熔断
- 降级
1、资源隔离
资源隔离主要是对线程资源进行隔离。这里有两种方法进行隔离:使用线程池和信号量
(1)线程池隔离
使用线程池进行资源隔离,我们有两种途径,一种是在服务端,根据不同的请求类型划分不同的队列进行处理,比如我们可以根据业务的优先级,创建高中低、默认等优先级队列来处理业务请求。
还有一种方式是通过在服务调用端进行队列划分,比如我们有多个服务进行调研,每个调用内部都有一个队列,通过连接管理的方式进行服务调用
(2)信号量
还有一种方式是通过信号量的方式来实现。我们可以通过给每个服务提供方设置可用的信号量来实现资源隔离。
(3)对比
信号量与线程池对比
对比项 | 线程切换 | 异步 | 超时 | 熔断 | 限流 | 开销 |
---|---|---|---|---|---|---|
信号量 | 否 | 否 | 否 | 是 | 是 | 小 |
线程池 | 是 | 是 | 是 | 是 | 是 | 大 |
下面我们来具体分析下服务熔断和降级的设计应用。
2、服务熔断
什么叫服务熔断?
服务熔断就是临时关闭对某些功能的调用,个别的业务不可用,但是系统整体可用
有哪些服务熔断的具体例子呢?
比如一些大的电商平台在双11那天关闭退款入口,关闭换头像功能等等,这些功能的关闭对于用户来说是可感知的。
在做服务熔断设计中,我们需要关注哪些点呢?
- 可熔断服务
我们需要判断在我们的整体架构中,哪些服务是可用进行熔断的,哪些是核心业务,不能出现不可用的情况。
- 熔断触发:触发熔断的方式有哪些
- 主动熔断:系统管理员根据将要出现的场景,提前关闭某些服务
- 被动熔断:系统根据服务的状态触发熔断
- 恢复时机
3、服务降级
什么叫服务降级?
服务降级就是有损的提供服务,保证服务柔性可用。
业务中有哪些服务降级的场景呢?
比如我们在请求高峰期的时候,对于用户的个人推荐,我们返回兜底数据;计数服务返回假数据等。这些服务对于用户来说他是不易感知的。
我们在进行服务降级设计时,需要考虑到哪些方面呢?
- 可降级服务
首先我们要确认在我们的整体架构中,哪些服务是可以采用降级的方案的,哪些场景不适合降级的方法
- 降级方法
我们一般的降级方法有哪些,比如常见的默认返回等等
- 降级触发
降级触发的条件是什么,现在有2种常见的情况:主动和被动
主动:管理员发现某个服务出现异常,通过手动的方式触发某个服务的熔断,这是我们需要有一个降级的方法
被动:当某个服务调用超时或者异常,采用降级方案
- 恢复时机
触发降级服务后,什么时候恢复到原来的服务?
4、熔断降级
基于上面的描述,我们可以发现,降级熔断都是系统可用性可靠性的保障手段,为了保障服务的柔性可用。
- 目标一致:都是从可用性和可靠性出现,为了防止系统崩溃
- 用户体验:用户感受到某些功能的暂不可用
很多时候,降级和熔断是配置使用的
5、断路器设计
断路器
服务熔断的开关,当对下游服务调用异常量达到设定阈值后,打开断路器,触发熔断
(1)断路器的状态流转
- 断路器的状态分为3个状态,closed(关闭),open(打开),half open(半打开),服务正常使用时,断路器的状态为关闭的;
- 当服务调用在指定时间内达到一定的阈值,就会打开断路器,服务熔断;
- 当达到关闭时间后,会将断路器的状态更新为halfopen,将少部分流量打到之前熔断的服务;
- 如何服务调用正常,则将断路器状态更新为closed,否则更新为open
(2)阈值与统计数据
- 阈值的数据类型
- 我们一般采用百分比的方式,比如失败率达到多少多少我们出发熔断
- 颗粒度
- 我们一般是针对某个节点中的某个实例的某个方法而言,当某个方法的调用失败率达到一定的程度,我们就触发对该方法调用的熔断
- 统计
数据结构:如上图,我们需要在规定的时间范围内进行统计,只需要统计最近10个slot范围内的数据,根据场景,我们的数据结构可以是链表,或者循环数组,滑动窗口伪代码实现如下:
// 整个循环数组的结构
public class BucketCircularArray {
private volatile int size =10;
private static int maxSize = 60;
private volatile int dataLength;
private Bucket[] data;
private int head;
private int tail;
}
// 每个slot的数据结构
public class Bucket {
private final long windowStart;
private AtomicInteger successNum;
private AtomicInteger failNum;
private AtomicInteger timeoutNum;
}
public void addBucket(Bucket bucket) {
data[tail] = bucket;
incrementTail();
}
private void incrementTail() {
if (dataLength == size) {
// the size change
head = (head + 1) % size;
tail = (tail + 1) % size;
} else