之前给大家讲了很多服务之前的关系,今天主要给大家介绍,当服务出现问题时,我们该如何解决。
首先我们先进行一些场景分析:
- 场景一
服务提供端提供了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 changehead = (head + 1) % size;tail = (tail + 1) % size;} else {tail = (tail + 1) % size;}if (dataLength < size) {dateLength++;}
}public Bucket tail() {if (dateLength == 0) {return null;}int index = (head + dataLength -1) % dataLength;return data[index];
}public Bucket[] toArray() {ArrayList<Bucket> list = new ArrayList<Bucket>();for (int i = 0; i < dataLength; i++) {int index = (head + i) % dataLength;Bucket tmp = data[index];if (tmp != null) {list.add(tmp);}}return list.toArray(new Bucket[list.size()]);
}
// 断路器CircuitBreaker
// open -> half_open
public boolean attemptExecution() {CircuitBreakConfigMeta configData = this.srvMgrClient.getCircuitBreakConfig(serviceName, method);............} else {if (isAfterSleepWindow(configData)) {if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {halfOpenPassNum.set(0);return true;} else {if (halfOpenPassNum.incrementAndGet() > maxHalfOpenPassNum) return false;elsereturn true;}} else {return false;}}
}private boolean isAfterSleepWindow(CircuitBreakConfigMeta configData) {final long circuitOpenTime = circuitOpened.get();final long currentTime = System.currentTimeMillis();final long sleepWindowTime = configData.getSleepwindowInMilliseconds();return currentTime > circuitOpenTime + sleepWindowTime;
}// hald_open -> closed
public void markSuccess() {boolean flag = false;if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {circuitOpened.set(-1L);flag = true;}return flag;
}// closed -> open
public boolean tryMarkOpen(int errorPercentage, Long requestNum) {circuitBreakConfigMeta configData = this.srvMgrClient.getCircuitBreakConfig(serviceName, method);if (configData != null && !configData.isForceClosed() && requestNum >= configData.getRequestVolumThreshold() && errorPecentage >= configData.getErrorThresholdPercentage()) {if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {circuitOpened.set(System.currentTimeMillis());return true;}}return false;
}// half_open -> open
public void markNonSuccess() {if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {logger.debug("circuitBreakerName {} reset to open", circuitBreakerName);circuitOpened.set(System.currentTimeMillis());}
}
// 控制器对象:MetricTimeWindow
public MetricTimeWindow(String serviceName, String method, ScfClientService srvMgrClient, int windowLength, EventBus eventBus) {this.metricName = serviceName + "@" + method;this.serviceName = serviceName;this.method = method;this.bucketArray = new BucketCircularArray(windowLength);this.circuitBreaker = new CircuitBreaker(serviceName, method, srvMgrClient);
}// 处理事件,成功、失败、超时
public void addEvent(MetricEventType event) {Bucket bucket = getCurrentBucket();switch(event) {case success:bucket.getSuccessNum().incrementAndGet();boolean makeClosed = circuitBreaker.markSuccess();break;case fail:bucket.getFailNum().incrementAndGet();this.circuitBreaker.markNonSuccess();break;case timeout:bucket.getTimeoutNum().incrementAndGet();this.circuitBreaker.markNonSuccess();break;}
}public boolean markSuccess() {boolean flag = false;if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {circuitOpened.set(-1L);flag = true;}return flag;
}public void markNonSuccess() {if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {logger.debug("circuitBreakerName {} reset to open", circuitBreakerName);circuitOpened.set(System.currentTimeMillis());}
}// 获取当前的bucket
public Bucket getCurrentBucket() {long currentTime = System.currentTimeMillis();lock.readLock().lock();Bucket bucket = null;try {bucket = bucketArray.tail();if (bucket != null && currentTime <= (bucket.getWindowStart() + bucketTimeSpan))return bucket;} finally {lock.readLock().unlock();}boolean createNewBucket = false;lock.writeLock().lock();try {Bucket check = bucketArray.tail();if (check != null && currentTime <= (check.getWindowStart() + bucketTimeSpan)) {bucket = check;} else {bucket = new Bucket(currentTime);bucketArray.addBucket(bucket);createNewBucket = true;}} finally {lock.writeLock().unlock();}if (createNewBucket) {dealCreateBucketEvent();}return bucket;
}private void dealCreateBucketEvent() {Bucket[] data = null;lock.readLock().lock();data = bucketArray.toArray();if (data == null || data.length == 0) {return;}long successNum = 0L;long failNum = 0L;for (int i = 0; i < data.length; i++) {Bucket tmp = data[i];successNum += tmp.getSuccessNum().get;failNum += tmp.getFailNum().get;failNum += tmp.getTimeoutNum().get;}Long reqNum = successNum + failNum;int errorPercentage = (int) ((double) failNum / (successNum + failNum) * 100);circuitBreaker.tryMarkOpen(errorPercentage, reqNum);
}
6、Hystrix应用
Hystrix 语义为“豪猪”,是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟、异常,引起资源耗尽导致系统不可用的解决方案。
(1)解决问题
- 阻止某个有问题调用耗尽系统的所有线程
- 阻止错误在分布式系统之间的传播
- 快速识别代替请求排队
- 错误回退、优雅的服务降级
(2)问题出现原因
- 服务提供者不可用:某依赖不可用,请求无法正常返回
- 重试加大流量:代码重试逻辑或客户端重试,导致流量加大
- 服务调用者不可用:线程资源耗尽,导致服务调用者不可用
解决方案:对依赖做隔离
(3)依赖隔离
- 包装依赖调用逻辑,每个命令在单独线程中执行
- 可配置依赖调用超时时间,当调用超时时,执行fallback逻辑
- 线程池已满调用将被立即拒绝,快速失败判断
- 依赖调用请求失败(异常、拒绝、超时、断路)时执行fallback逻辑
- 提供熔断器组件,可以自动运行或手动调用
(4)主要特性
- 资源隔离:限制调用服务使用的资源,当某一下游服务出现问题时,不会影响整个调用链
- 熔断机制:当失败率达到阈值自动触发熔断,熔断器触发不在进行调用
- 降级机制:超时、资源或触发熔断后,调用预设的降级接口返回托底数据