写在前面
思考的过程往往比直接得到结论更加重要
1、为什么需要熔断器
服务雪崩
在分布式环境下,不可避免的就是服务之间的调用。A 调 B,B 可能会失败,如果此时 B 服务挂掉,那么会导致服务 A 因为服务 B 的失败而失败。从而导致 客户端认为 A 也是失败的。 简单说就是,牵一发而动全身
这也是,我们需要熔断器的原因。我们需要有保护服务调用的组件。当服务 B 挂掉时,服务 B 需要能够快速失败。
2、如果自己写一个断路器,你会怎么做呢?
隔离策略
思考一下,什么是隔离?
我对他的理解,大概就是,服务 A 调用服务 B,服务 C。不能因为调用服务 B 出现问题,而导致调用服务 C 也出现问题。也就是,服务 B、服务 C 的调用应该放在不同的环境下。
常见的资源隔离,有线程池隔离,信号量隔离
线程池隔离 | 信号量隔离 | |
---|---|---|
线程 | 请求线程和调用 provider 不是同一个线程 | 请求线程和调用 provider 是同一个线程 |
开销 | 排队、调度、上下文开销等 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发支持 | 支持(线程池大小) | 支持(信号量上限) |
传递 Header | 无法传递 Http Header | 可以传递 Http Header |
快速失败
当 provider 提供的服务不可用,或出现异常时,应该有可供回调的降级方法。
- provider 调用失败抛出异常,可能针对某种异常,不想执行快速失败策略,而是需要直接抛出异常
- provider 调用失败,可能不想执行快速失败策略,也不想抛出任何异常。
限流
当 provider 被大量调用时,为了保护链路,需要做限流。那么其实核心的问题是,如何进行统计。
统计
每个 provider 的调用情况需要统计。这样可以更好的监控到 provider 提供的服务的情况。统计的算法,应该是基于滑动窗口进行统计。
熔断
因为有对每个 provider 调用情况统计,在调用之前,失败次数达到某个阈值时,可以认为该 provider 已经是有问题的,可以直接快速失败,以防止服务雪崩情况。
系统自适应保护
熔断机制是针对每个 provider 的。但是,有可能服务系统已经要达到极限了,不能再接收任何请求了,那么此时,出于系统的保护,也应该快速失败。
扩展
作为一个组件,支持扩展那是必须。
常见的扩展方式:(其实就是面向接口编程,在某些执行流程中,暴露出部分接口。剩下的就看你怎么封装了)
- 观察者设计模式 + 策略设计模式。例如
public interface SayListener {String say();
}
public class App {List<SayListener> sayListener = new ArrayList<>();
public void doSomething() {
// ...
sayAction();
// 触发了 say 的动作
if (null != sayListener && sayListener.size() > 0) {
sayListener.forEach(listener -> {listener.say();
});
}
// ...
}
public void sayAction() {}
public void registerSayListener(SayListener listener) {sayListener.add(listener);
}
}
3、现有类似功能组件对比
sentinel | hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离 (并发线程数限流) | 线程池隔离 / 信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件形式 | 接口形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS, 支持基于调用关系的限流 | 基于线程池个数有限支持 | Rate Limiter |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 丰富 | 简单 | 不提供控制台,可对接其他监控系统 |
hystrix wiki 介绍实在太全了,这里就没必要在介绍了,主要是从个人的角度去思考如果让自己也实现一个断路器中间件,你会怎么做?(鉴于 hystrix 停止维护,就没看源码了,以及 sentinel 的活跃,重心会放在 sentinel 上)
4、使用注意
在使用 hystrix 需要注意的几个地方。
hystrix 超时设置
设置 hystrix 超时时间时,需要大于等于 ribbon 的超时时间。例如 ribbon 的配置如下
ReadTimeout=1000
ConnectTimeout=1000
MaxAutoRetries=1
MaxAutoRetriesNextServer=1
那么 hystrix 的超时时间公式为:
execution.isolation.thread.timeoutInMilliseconds >= (MaxAutoRetriesNextServer + 1) * (MaxAutoRetries + 1) *(ReadTimeout + ConnectTimeout)
hystrix 使用线程池隔离时,无法传递绑定在 tomcat 线程上下文的值
因为使用线程池隔离时(execution.isolation.strategy=THREAD),会新创建一个线程,因此 tomcat 线程的变量则无法传递给新的线程。那么此时可以使用 信号量隔离(execution.isolation.strategy=SEMAPHORE)。因为使用信号量隔离时,会使用 tomcat 线程调用远程服务。
其实 hystrix 也想到了会有这样的需求, 因此在 wiki 中,推荐我们继承 HystrixConcurrencyStrategy,重写 wrapCallable() 方法。具体代码参考 线程池隔离传递 ThreadLocal 值
该作者是基于 spi 实现的(牛逼),其实 hystrix 提供了可配置的参数供我们配置
hystrix:
plugin:
HystrixConcurrencyStrategy:
implementation: com.csp.hystrix.MyHystrixConcurrencyStrategy