写在前面
在我们的系统中一般可以分为核心业务和非核心业务,比如电商系统中,订单模块就是核心模块,购物成功通知模块,评论模块等是非核心模块,在系统调用中我们不能因为非核心模块出现问题导致核心模块的正常运行,此时就可以考虑使用熔断,本文我们就一起看下如何使用hystrix来实现dubbo调用的熔断。
实现思路:利用dubbo的Filter,过滤请求,加入hystrix熔断逻辑。
源码:这里 。
dubbo基础知识以及环境搭建可以参考这篇文章 。
1:引入依赖
<dependecies>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.12</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-serialization</artifactId>
<version>1.5.12</version>
</dependency>
</dependencies>
2:熔断执行逻辑
先来定义如果是发生了熔断,要执行的逻辑。
2.1:接口
import com.alibaba.dubbo.common.extension.SPI;
@SPI
public interface Fallback {
Object invoker();
}
注意使用了@SPI注解,因为后续需要通过DUBBO SPI机制配置,而DUBBO SPI机制强制要求使用该注解,不然会异常。
dubbo SPI在jdk SPI基础上进行了优化,可以参考这篇文章 了解下jdk spi的用法,了解之后dubbo spi自然也就知道怎么使用了。
2.2:实现类
// 熔断后执行的类
public class FallbackImpl implements Fallback {
// 熔断后执行的方法
@Override
public Object invoker() {
return "dubbo调用被熔断";
}
}
3:定义开启hystrix执行逻辑
public class DubboHystrixCommand extends HystrixCommand<Result> {
private Logger logger = LoggerFactory.getLogger("command_DubboHystrixCommand");
private Invoker<?> invoker;
private Invocation invocation;
private String fallbackName;
protected Result run() {
String logHead = "DubboHystrixCommand_run";
logger.info(logHead + " begin");
Result result = invoker.invoke(invocation);
// 如果远程调用异常,抛出异常就会调用getFallback()方法去执行降级逻辑
if (result.hasException()) {
String url = invoker.getUrl().toFullString();
logger.info(logHead + " provider exception happened, url is: " + url);
throw new HystrixRuntimeException(
HystrixRuntimeException.FailureType.COMMAND_EXCEPTION,
DubboHystrixCommand.class,
result.getException().getMessage(),
result.getException(),
null
);
}
logger.info(logHead + " end");
return result;
}
// 如果是发生了熔断,则会自动调用该方法来获取
protected Result getFallback() {
String logHead = "DubboHystrixCommand_getFallback";
logger.info(logHead + " fallback in, fallback name is: " + fallbackName);
// 如果没有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);
logger.info(logHead + " query fallback impl by dubbo spi result is: " + fallback.getClass().getName());
Object value = fallback.invoker();
logger.info(logHead + " fallback result is: " + value);
return new RpcResult(value);
} catch (RuntimeException ex) {
logger.error(logHead + " fallback failed ", ex);
throw ex;
}
}
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");
}
static Map<String, HystrixCommand.Setter> setterHashMap = new HashMap<>();
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)) {
setterHashMap.put(key,
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);
}
public static HystrixCommandProperties.Setter hystrixCommandProperties_Setter(URL url, String method) {
// 从URL获取熔断配置
return HystrixCommandProperties.Setter()
// 熔断触发后多久恢复half-open状态,
// 熔断后sleepWindowInMilliseconds毫秒会放入一个请求,如果请求处理成功,熔断器关闭,否则熔断器打开,继续等待sleepWindowInMilliseconds
.withCircuitBreakerSleepWindowInMilliseconds(url.getMethodParameter(method,
"sleepWindowInMilliseconds",
30000))
// 熔断触发错误率阈值, 超过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",
3000))
// fallback最大并发数
.withFallbackIsolationSemaphoreMaxConcurrentRequests(url.getMethodParameter(method,
"fallbackMaxConcurrentRequests",
50))
// 隔离策略, 默认thread线程池隔离
.withExecutionIsolationStrategy(getIsolationStrategy(url))
// 正常请求最大并发数
.withExecutionIsolationSemaphoreMaxConcurrentRequests(url.getMethodParameter(method,
"maxConcurrentRequests",
100));
}
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;
}*/
return HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE;
}
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));
}
}
4:定义dubbo过滤器
@Activate(group = Constants.CONSUMER, order = 10002)
public class DubboHystrixFilter implements Filter {
private Logger logger = LoggerFactory.getLogger("filter_DubboHystrixFilter");
// 通过该方法完成调用服务提供者的服务
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
String logHead = "DubboHystrixFilter_invoke";
logger.info(logHead + " begin");
String isOpen = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "isOpen");
logger.info(logHead + " isOpen is: " + isOpen);
// 是否打开hystrix
if (isOpen != null && Boolean.parseBoolean(isOpen)) {
logger.info(logHead + " use hystrix");
DubboHystrixCommand dubboHystrixCommand = new DubboHystrixCommand(invoker, invocation);
Result result = dubboHystrixCommand.execute();
return result;
} else {
logger.info(logHead + " not use hystrix");
// 未打开的话, 直接走真正调用逻辑
return invoker.invoke(invocation);
}
}
}
注意加上@Activate
不然不生效,其中的group = Constants.CONSUMER
代表是在消费者使用,order = 10002
是配置当存在多个时的执行顺序,数字越小优先级越高。
5:配置dubbo SPI
首先在classpath下创建META-INF/dubbo
目录,然后分别以com.alibaba.dubbo.rpc.Filter
和Fallback
接口的类全限定名作为文件名称创建文件,如下是我的注意Fallback改成你自己的
:
com.alibaba.dubbo.rpc.Filter
内容如下:
hystrixFilter=dongshi.daddy.filter.DubboHystrixFilter
具体实现类注意改成给你自己的。
dongshi.daddy.filter.Fallback
内容如下:
fallbackImpl=dongshi.daddy.filter.FallbackImpl
具体实现类注意改成给你自己的。
6:xml配置
<fakeRoot>
<dubbo:reference id="myHystrixServiceInConsumerSide"
check="false"
interface="dongshi.daddy.service.MyHystrixService">
<dubbo:method name="sayHi" timeout="3000"/>
<!-- 配置打开hystrix -->
<dubbo:parameter key="isOpen" value="true"/>
<!-- 降级配置 -->
<dubbo:parameter key="fallback" value="fallbackImpl"/>
<!--核心线程数大小 -->
<dubbo:parameter key="coreSize" value="10"/>
<!--最大线程数大小 -->
<dubbo:parameter key="maximumSize" value="50"/>
<!--空闲线程持有时间(分钟) -->
<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>
<!-- 使Filter生效 hystrixFilter为在dubbo spi文件中配置的key-->
<dubbo:consumer filter="hystrixFilter"/>
</fakeRoot>
7:consumer主函数
public class ConsumerWithHystrixMain {
static Executor executor = Executors.newFixedThreadPool(100);
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("consumer-with-hystrix.xml");
context.start();
MyHystrixService myHystrixService = (MyHystrixService) context.getBean("myHystrixServiceInConsumerSide");
while (true) {
/*
new Thread(() -> {
System.out.println(myHystrixService.sayHi("熔断哈哈"));
}).start();
*/
executor.execute(() -> System.out.println(myHystrixService.sayHi("熔断哈哈")));
Thread.sleep(50);
}
// System.in.read();
}
}
我们可以在provider service增加一个比较长的休眠来触发consumer的超时,如下是我本地的模拟逻辑:
class FakeCls {
public String sayHi(String word) {
int j = 0;
while (j < 100 || (j > 200 && j < 300)) {
j = i++;
System.out.println("异常处理 【 " + word + " 】 中");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("正常处理 【 " + word + " 】 中");
return "real hystrix say hi: " + word;
}
}
从而consumer会因为大量超时而触发熔断,如下是我本地测试的结果:
real hystrix say hi: 熔断哈哈
real hystrix say hi: 熔断哈哈
real hystrix say hi: 熔断哈哈
返回默认值null
返回默认值null
real hystrix say hi: 熔断哈哈
real hystrix say hi: 熔断哈哈
返回默认值null
返回默认值null
返回默认值null
正常调用和熔断交叉出现,说明熔断和恢复正常调用都是正常的。