一, 服务稳定性以及熔断降级的意义
需求的多样性, 业务的复杂程度也是不断上升的, 一个接口依赖的服务也会逐步增加, 各个服务的稳定性是需要着重考虑的; 例如一个电商订单场景, 不能因为评论服务异常超时, 而阻止用户浏览该商品的其他信息, 同样的也不能因为用户优惠券服务等这些非核心服务(弱依赖), 异常超时从而阻止用户下订单; 但是网络环境又是不可控的, 有可能在某一段时间内, 由于网络抖动, 请求被拒绝, 或者是高并发的大流量进来, 导致系统资源繁忙(线程池耗尽等), 请求被拒绝. 这时就需要使用熔断降级的方式提供有损服务, 确保核心业务不受影响.
二, 图示演示
一次用户请求过来, 需要后台多个服务A, B....提供支持, 但如果服务A一直失败, 而且服务A还是一个非核心服务(即是一个弱依赖的服务),可以采用熔断降级的方式, 使后续的请求直接走降级逻辑, 返回固定逻辑即可, 确保核心的业务不受到影响.
三, Hystrix框架
要想实现上面描述的过程, 那么hystrix就必须要实现下面这几样功能:
1 服务出现异常, 超时, 能够自动走降级逻辑;
2 当服务的失败率达到阈值时, 能够自动进行熔断;
3 在服务熔断期间, 所有对于该服务的请求, 全部走降级.
4 当服务恢复时, 后续请求能够正常访问该服务.
四, dubbo结合hystrix实现服务的熔断降级
1 通过在<dubbo:reference>下配置自定义<dubbo:parameters>参数, 决定是否启用hystrix
<!-- 配置打开hystrix -->
<dubbo:parameter key="isOpen" value="true"/>
/**
* @Description:
* @project dubbo-hystrix-filter
* @class DubboHystrixFilter.java
* @author xiongxianze
* @version 1.0
* @date 2019年2月10日 上午12:10:40
*/
@Activate(group = Constants.CONSUMER, order = 10002)
public class DubboHystrixFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String isOpen = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "isOpen");
// 是否打开hystrix
if (isOpen != null && Boolean.parseBoolean(isOpen)) {
DubboHystrixCommand dubboHystrixCommand = new DubboHystrixCommand(invoker, invocation);
Result result = dubboHystrixCommand.execute();
return result;
} else {
// 未打开的话, 直接走真正调用逻辑
return invoker.invoke(invocation);
}
}
}
2 DubboHystrixCommand继承HystrixCommand类, 并重写其run()和getFallback()方法, 当执行dubboHystrixCommand.execute()时, 就会执行其run()方法, 执行run()方法报异常时, 就会调用getFallback()方法走降级逻辑, 代码如下:
DubboHystrixCommand类构造方法如下: 调用HystrixCommand_Setter()方法配置hystrix熔断的条件属性.
public DubboHystrixCommand(Invoker<?> invoker, Invocation invocation){
// 构造HystrixCommand.Setter
super(HystrixCommand_Setter(invoker, invocation));
this.invoker = invoker;
this.invocation = invocation;
this.fallbackName = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "fallback");
;
}
①构造HystrixCommand.Setter
private static HystrixCommand.Setter HystrixCommand_Setter(Invoker<?> invoker, Invocation invocation) {
// interfaceName.methodName
String key = String.format("%s.%s", invoker.getInterface().getName(), invocation.getMethodName());
// 1 根据interfaceName+methodName从缓存获取Setter
if (setterHashMap.containsKey(key)) {
return setterHashMap.get(key);
} else {
setterHashMap.put(key,
HystrixCommand.Setter
// 组名使用服务接口模块名称
.withGroupKey(HystrixCommandGroupKey.Factory.asKey(invoker.getInterface().getName()))
// 隔离粒度为接口方法, 但是同一个接口中的所有方法公用一个线程池, 各个服务接口的线程池是隔离的
// 配置到这里, 就说明, 相同的接口服务, 相同的方法, 拥有相同的熔断配置策略
.andCommandKey(HystrixCommandKey.Factory.asKey(invocation.getMethodName()))
// 熔断配置
.andCommandPropertiesDefaults(hystrixCommandProperties_Setter(invoker.getUrl(),invocation.getMethodName()))
// 线程池配置
.andThreadPoolPropertiesDefaults(hystrixThreadPoolProperties_Setter(invoker.getUrl())));
return setterHashMap.get(key);
}
}
②配置hystrixCommandProperties.Setter熔断属性
public static HystrixCommandProperties.Setter hystrixCommandProperties_Setter(URL url, String method) {
// 从URL获取熔断配置
return HystrixCommandProperties.Setter()
// 熔断触发后多久恢复half-open状态,
// 熔断后sleepWindowInMilliseconds毫秒会放入一个请求,如果请求处理成功,熔断器关闭,否则熔断器打开,继续等待sleepWindowInMilliseconds
.withCircuitBreakerSleepWindowInMilliseconds(url.getMethodParameter(method,
"sleepWindowInMilliseconds",
5000))
// 熔断触发错误率阈值, 超过50%错误触发熔断
.withCircuitBreakerErrorThresholdPercentage(url.getMethodParameter(method,
"errorThresholdPercentage",
50))
// 熔断判断请求数阈值, 一个统计周期内(默认10秒)请求不少于requestVolumeThreshold才会进行熔断判断
.withCircuitBreakerRequestVolumeThreshold(url.getMethodParameter(method,
"requestVolumeThreshold",
20))
// 这里可以禁用超时, 而采用dubbo的超时时间, 默认为true
// .withExecutionTimeoutEnabled(false)
// 当隔离策略为THREAD时,当执行线程执行超时时,是否进行中断处理,默认为true。
.withExecutionIsolationThreadInterruptOnTimeout(true)
// 执行超时时间,默认为1000毫秒,如果命令是线程隔离,且配置了executionIsolationThreadInterruptOnTimeout=true,则执行线程将执行中断处理。
// 如果命令是信号量隔离,则进行终止操作,因为信号量隔离与主线程是在一个线程中执行,其不会中断线程处理,所以要根据实际情况来决定是否采用信号量隔离,尤其涉及网络访问的情况。
// 注意该时间和dubbo自己的超时时间不要冲突,以这个时间优先,比如consumer设置3秒,那么当执行时hystrix会提前超时, 因为这里设置的时间为1秒
.withExecutionTimeoutInMilliseconds(url.getMethodParameter(method,
"timeoutInMilliseconds",
1000))
// fallback方法的信号量配置,配置getFallback方法并发请求的信号量,如果请求超过了并发信号量限制,则不再尝试调用getFallback方法,而是快速失败,默认信号量为10
.withFallbackIsolationSemaphoreMaxConcurrentRequests(url.getMethodParameter(method,
"fallbackMaxConcurrentRequests",
50))
// 隔离策略, 默认thread线程池隔离
.withExecutionIsolationStrategy(getIsolationStrategy(url))
// 设置隔离策略为ExecutionIsolationStrategy.SEMAPHORE时,HystrixCommand.run()方法允许的最大请求数。如果达到最大并发数时,后续请求会被拒绝。
.withExecutionIsolationSemaphoreMaxConcurrentRequests(url.getMethodParameter(method,
"maxConcurrentRequests",
10));
}
③ 配置HystrixThreadPoolProperties.Setter线程池属性
public static HystrixThreadPoolProperties.Setter hystrixThreadPoolProperties_Setter(URL url) {
// 从url获取线程池配置
return HystrixThreadPoolProperties
.Setter()
.withCoreSize(url.getParameter("coreSize",10))
.withAllowMaximumSizeToDivergeFromCoreSize(true)
.withMaximumSize(url.getParameter("maximumSize",20))
.withMaxQueueSize(-1)
.withKeepAliveTimeMinutes(url.getParameter("keepAliveTimeMinutes",1));
}
④ 设置隔离策略
public static HystrixCommandProperties.ExecutionIsolationStrategy getIsolationStrategy(URL url) {
String isolation = url.getParameter("isolation", THREAD);
if (!isolation.equalsIgnoreCase(THREAD) && !isolation.equalsIgnoreCase(SEMAPHORE)) {
isolation = THREAD;
}
if (isolation.equalsIgnoreCase(THREAD)) {
return HystrixCommandProperties.ExecutionIsolationStrategy.THREAD;
} else {
return HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE;
}
}
⑥重写run()方法, 当远程调用异常时, 就会hystrix框架就会调用getFallback()方法执行降级逻辑.
protected Result run() throws Exception {
Result result = invoker.invoke(invocation);
// 如果远程调用异常,抛出异常就会调用getFallback()方法去执行降级逻辑
if (result.hasException()) {
throw new HystrixRuntimeException(HystrixRuntimeException.FailureType.COMMAND_EXCEPTION,
DubboHystrixCommand.class, result.getException().getMessage(),
result.getException(), null);
}
return result;
}
⑦getFallback()方法, 执行降级逻辑; 基于SPI扩展机制让业务方实现Fallback接口, 执行降级逻辑.
protected Result getFallback() {
// 如果没有fallback, 则抛出原本的异常:No fallback available.
if (StringUtils.isEmpty(fallbackName)) {
return super.getFallback();
}
try {
// 基于SPI扩展加载fallback实现
ExtensionLoader<Fallback> loader = ExtensionLoader.getExtensionLoader(Fallback.class);
Fallback fallback = loader.getExtension(fallbackName);
Object value = fallback.invoker();
return new RpcResult(value);
} catch (RuntimeException ex) {
logger.error("fallback failed", ex);
throw ex;
}
}
⑧业务方实现Fallback接口, 执行降级逻辑.
首先创建, META-INF/dubbo/com.xxz.dubbo.fallback.Fallback文件, 并添加其实现类fallbackImpl=com.xxz.dubbo.fallback.FallbackImpl
五, 使用
1 引入依赖
<!-- 引入自定义dubbo-hystrix-filter熔断降级 -->
<dependency>
<groupId>com.xxz</groupId>
<artifactId>dubbo-hystrix-filter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2 实现降级Fallback接口, 如上步骤⑧配置, 实现降级逻辑.
public class FallbackImpl implements Fallback {
/**
* @Description:
* @author xiongxianze
* @version 1.0
* @date 2019年2月10日 上午12:28:17
* @see com.xxz.dubbo.fallback.Fallback#invoker()
*/
@Override
public Object invoker() {
return "返回默认值null";
}
}
3 <dubbo:reference>配置自定义参数
<!-- 引用服务serviceA -->
<dubbo:reference id="serviceA" interface="com.xxz.dubbo.ServiceA"
protocol="dubbo" check="false" mock="false"
retries="0">
<dubbo:method name="serviceA" timeout="3000"/>
<dubbo:method name="serviceNum" timeout="2000"/>
<!-- 配置打开hystrix -->
<dubbo:parameter key="isOpen" value="true"/>
<!-- 降级配置 -->
<dubbo:parameter key="fallback" value="fallbackImpl"/>
<!--核心线程数大小 -->
<dubbo:parameter key="coreSize" value="10"/>
<!--最大线程数大小 -->
<dubbo:parameter key="maximumSize" value="20"/>
<!--空闲线程持有时间(分钟) -->
<dubbo:parameter key="keepAliveTimeMinutes" value="1"/>
<!-- 熔断判断请求数阈值, 一个统计周期内(默认10秒)请求不少于requestVolumeThreshold才会进行熔断判断 -->
<dubbo:parameter key="requestVolumeThreshold" value="5"/>
<!-- 熔断触发后多久恢复half-open状态, 熔断后sleepWindowInMilliseconds毫秒会放入一个请求,如果请求处理成功,熔断器关闭,否则熔断器打开,继续等待sleepWindowInMilliseconds -->
<dubbo:parameter key="sleepWindowInMilliseconds" value="20000"/>
<!-- 熔断触发错误率阈值, 超过50%错误触发熔断 -->
<dubbo:parameter key="errorThresholdPercentage" value="50"/>
<!-- 任务执行超时时间, 注意该时间和dubbo自己的超时时间不要冲突,以这个时间优先,
比如consumer设置3秒,这里的超时时间为200ms的话, 则本次调用就会超时 -->
<dubbo:parameter key="timeoutInMilliseconds" value="3000"/>
</dubbo:reference>
六, 结果(基于上面的参数配置)
1 首先在一个统计周期(默认10s), 配置requestVolumeThreshold=5次请求, 有errorThresholdPercentage=50的错误异常率的话, hystrix就会自动熔断, 后续请求直接走降级逻辑getFallback()方法返回默认值.
2 同时, Hystrix也会在sleepWindowInMilliseconds=20000ms(即20s后), 会放入一个请求, 这时hystrix处于一个半开状态, 若这个请求能正常被处理, Hystrix熔断器就会关闭, 否则熔断器就会继续打开, 确保其他请求还是走降级, 从而继续等待下一个sleepWindowInMilliseconds.
七, 综上: 使用Hystrix确实是可以实现刚刚我们在上面描述的这四个功能点.
1 服务出现异常, 超时, 能够自动走降级逻辑;
2 当服务的失败率达到阈值时, 能够自动进行熔断;
3 在服务熔断期间, 所有对该服务的请求, 全部走降级.
4 当服务恢复时, 后续的请求能够正常访问到该服务.