起源
承接熔断模式与延迟错误容忍系统 本文以一具体场景的业务目标作为切入点, 从设计到代码层面聊聊对熔断模式的理解
什么是熔断模式
老生常谈 就跟我们建房子要考虑保险丝一样, 在分布式环境下的系统难免会出现依赖(Hystrix的定义)或资源(Sentinel的定义)不可用的场景, 通过引入熔断组件能一定程度上增加整个系统弹性
不同的熔断组件对熔断及其相关的概念在细节上有一定的区别
最近趋势化的Sentinel:
- 最初设计目的放在流量控制上(与熔断降级是不同维度的概念)
- 提供熔断降级与并发控制功能
- 增加系统负载保护功能
- …更多烦请参考官方文档
在Sentinel中, 熔断与降级(失败返回) 被理解为同一个概念, 或者说 熔断是"因" 降级是"果". 的确, 从场景上来看某个资源/依赖绑定的熔断器打开后, 往往是会进入对应的fallback(失败返回/降级)函数 -> 流量控制为主,熔断降级为辅
老牌熔断组件Hystrix:
- 设计目的在于方便打造延迟 & 错误容忍系统
- 通过线程池级隔离远程资源来避免联级崩溃
- 侧重点在Resilience(服务的弹性)上
- …更多烦请参考官方文档
Hystrix的侧重点放在Resilience这个词上, 组件的能力支持围绕如何打造一个更加"弹性"的服务: 隔离远程访问点… 快速释放堆积的请求…调用失败返回方法
如果从功能目的层面上来看, 本人感觉它们非常接近, 就像是同一个接口的不同实现一样
关于失败返回更多的分析烦请参考熔断模式与延迟错误容忍系统 . 本文接下来将以一个业务目标作为切入点从应用层面讲解熔断模式的实践.
目标 -> 远程调用失败后的降级
起因: 搜索引擎在生产环境上可能会出现一定时间内"不可用"状态(网络…同步数据…请求量过大等),需要自动检测到该服务的不可用 并且在该场景下通过向数据库拉取数据来进行"补偿".
目标
- A: 对搜索引擎上拉取数据出错的函数进行降级(返回数据库拉取的数据)
- B: 定义统一抽象类,抽象类的实现上只需要定义远程函数与失败返回函数,无需感知到熔断组件(Sentinel/Hystrix/…)的api -> 通过修改抽象类的函数(调用实现类的外围函数),可以实现熔断组件的切换
- C: 熔断组件的规则从百度的Disconf拉取 & Disconfig上可以对保护的资源对应的规则进行热更新,并且即时同步到对应环境的所有节点上
- D: Disconf的配置热更新同步Sentinel的规则 -> 利用Disconfig的回调+Spring的SPI能力
- E: 通过打开熔断器开关的方式, 使单个查询接口强制走db或者其他降级函数
熔断降级模块设计(抽象层 已删除业务相关模块 )
设计分析(Sentinel为例,后文会有Hystrix的例子)
目标 C&D 在于如何衔接Disconfig提供的统一配置能力与Sentinel的规则配置
需要注意的是 -> Sentinel自带的Dashboard也支持节点为单位的资源规则配置, 本次使用Disconfig作为规则配置选型的原因是:
- 多环境下的不同规则配置支持
- 方便规则的变更统一到该环境的全部节点
- 以后熔断组件的变更 -> 虽然规则无法直接复用,但是可以复用 这些规则背后的含义
代码层面浅析*
定义Sentinel降级规则对应的接口
public interface SentinelDegradeConfig {
String getSourceKey();
Double getRatioCount();
int getTimeWindow();
int getMinRequestAmount();
int getRule();
}
对应实现(对应一个被保护的资源) 字段值从disconfig拉取
@Component
public class BizConfig implements SentinelDegradeConfig {
..........................
/**
* 通过异常百分比降级
* degrade via exception ratio
*/
public final int RULE = RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO;
private final String PREFIX = "规则对应配置项中的关键字";
@Override
@DisconfItem(key = PREFIX + "key")
public String getSourceKey() {
return sourceKey;
}
//disconfig的注解
@Override
@DisconfItem(key = PREFIX + "ratio")
public Double getRatioCount() {
return ratioCount;
}
@Override
@DisconfItem(key = PREFIX + "timeWindow")
public int getTimeWindow() {
return timeWindow;
}
@Override
@DisconfItem(key = PREFIX + "requestAmount")
public int getMinRequestAmount() {
return minRequestAmount;
}
@Override
public int getRule() {
return RULE;
}
.........................
}
利用Spring提供的SPI便利性加载全部规则的实现(SpringContextUtil是一个ApplicationContextAware的实现)
@Configuration
public class SentinelRulesConfig implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
reloadAllSentinelDegradeRules();
}
public static void reloadAllSentinelDegradeRules() {
List<SentinelDegradeConfig> configs = SpringContextUtil.getBeanList(SentinelDegradeConfig.class);
List<DegradeRule> degradeRules = configs
.stream().map(SentinelRulesConfig::convert)
.collect(Collectors.toList());
DegradeRuleManager.loadRules(degradeRules);
}
public static DegradeRule convert(SentinelDegradeConfig config) {
DegradeRule rule = new DegradeRule();
rule.setResource(config.getSourceKey());
rule.setCount(config.getRatioCount());
rule.setGrade(config.getRule());
rule.setTimeWindow(config.getTimeWindow());
rule.setMinRequestAmount(config.getMinRequestAmount());
return rule;
}
}
在Disconfig推送相关字段变更事件时全量刷新Sentinel规则(利用Disconfig的回调功能)
@Component
public class ConfigUpdatePipelineCallback implements IDisconfUpdatePipeline {
.....................
public static final String SENTINEL_DEGRADE_CONFIG_KEY = "规则对应配置项中的关键字";
@Override
public void reloadDisconfItem(String key, Object content) throws Exception {
if (key.contains(SENTINEL_DEGRADE_CONFIG_KEY)){
SentinelRulesConfig.reloadAllSentinelDegradeRules();
}
}
..............
}
到此,需求 C & D 已完成, 需要注意的是Disconfig上的配置项字段与SentinelDegradeConfig接口需要一一对应(这个默认的约束是个隐晦&难解的side-effect)
PS: 与Hystrix不同Sentinel的规则与对应的资源埋点代码(定义资源边界) 没有显式的静态依赖 -> Hystrix的使用 默认要求用户懂命令模式, 或者说,在实现他们提供的抽象类上使用组合/聚合的关系,委派成员对象(命令模式下的接收者)执行远程调用 -> 使用Hystrix时必须继承对应的抽象类并且代码层面上绑定规则 -> 这一点会在本文下面讲解CircuitBreakerCommand的实现类时进一步描述.
现在…聊一聊目标B -> CircuitBreakerCommand的定义与实现
所有熔断降级相关的行为抽象成统一函数,分别定义:
- 原先的调用:_execute();
- 降级(失败返回函数):_fallback();
- 上文规则的绑定函数_source();
原则上这三个函数由实现类实现
public interface CircuitBreakerCommand<T> {
T _execute();
T _fallback();
String _source();
}
对于Sentinel作为熔断组件的场景, 对应一个抽象类封装Sentinel有关的全部API(抽象类虽然与CircuitBreakerCommand是实现的关系,不实现具体任何一个方法):
- 以后可以修改这个抽象类来替换熔断组件(不新增的原因是一个工程不建议多个熔断组件并存)
- 子类的关注的实现点是CircuitBreakerCommand中定义的远程调用函数 & 失败返回函数 & 资源的捆绑, 不需要关注这个抽象类中的模版方法的行为(模版方法显式调用某一具体熔断组件的API)
**
* @Author: Yukai
* Description: 熔断模式 -> 将远程调用的对象使用熔断器对象包起来,使用熔断器对象进行监控,降级保护...etc
* 大道至简 -> 此类使用Sentinel作为技术选型 -> 建立的熔断器对象
* 经过这一层后 -> 实现类不该感知Sentinel Api的存在
* 1 申明规则
* 2 定义被保护的远程函数
* 3 定义失败回调函数
* 4 用得愉快!
**/
public abstract class AbstractSentinelCommand<T> implements CircuitBreakerCommand<T> {
/**
* 统一实现方法
* 熔断器关闭 : 调用原先的远程服务
* 熔断器打开: 调用失败返回方法
*/
public T call() {
Entry entry = null;
try {
entry = SphU.entry(_source());
// protected remote call 被保护的资源
return _execute();
} catch (BlockException e) {
// degraded method 降级方法
return _fallback();
} catch (Throwable t) {
//for reducing complexity -> do not call fallback method
//为了降低复杂度 -> 这里不调用失败返回方法
Tracer.trace(t);
throw t;
} finally {
entry.exit();
}
}
}
将这个抽象类改为Hystrix实现也很方便,CircuitBreakerCommand方法名_开头多原因是为了避免多继承/实现下的方法重写覆盖(请注意UML中CircuitBreakerCommand没有_开头, 需要变成_开头)
public abstract class AbstractHystrixCommand<T> extends HystrixCommand<T> implements CircuitBreakerCommand<T> {
public AbstractHystrixCommand(HystrixCommandGroupKey group) {
super(group);
}
/**
* 统一实现方法
* 熔断器关闭 : 调用原先的远程服务
* 熔断器打开: 调用失败返回方法
*
* @return
*/
public T call() {
return execute();
}
/** Hystrix 规定的远程调用对应方法 */
@Override
protected T run() {
return _execute();
}
/**
* Hystrix 规定的失败返回方法 -> 降级后自动调用
* */
@Override
protected T getFallback() {
return _fallback();
}
}
到此,目标 B 已完成, 目标B与之前目标 C&D 的完成很自然地完成了需求E(熔断器的开关相关是熔断组件规则之一)
目标 A的实现在于对CircuitBreakerCommand的实现上
定义类继承AbstractSentinelCommand(如果选择Sentinel)或AbstractHystrixCommand(如果选择Hystrix), 实现CircuitBreakerCommand对应的三个函数即可
需要注意的是RPC调用绑定的方法对应的实现类调用的是AbstractSentinelCommand/AbstractHystrixCommand的call()方法, 通过这个模版方法对实现类隐藏的熔断组件(Sentinel/Hystrix)的相关API调用