持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情
12、Hystrix的高级用法
6、断路器关闭
断路器打开后,在一段时间内,命令不会再执行(一直触发回退),这段时间称为休眠期,休眠期的默认值为5秒, 休眠期结束之后,Hystrix会尝试性执行一次命令,此时断路器状态不是开启,也不是关闭,而是一个半开的状态,**如果这一次命令执行成功,则会关闭断路器并且清空链路的健康信息,**如果执行失败,断路器会继续保持打开的状态。 断路器的打开与关闭测试,如下代码:
public class CloseTest {
public static void main(String[] args) throws Exception {
// 断路器配置(此处配置成:10s内大于 3 次请求,错误百分比为 50%)
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.metrics.rollingStats.timeInMilliseconds", 10000);
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.circuitBreaker.requestVolumeThreshold", 3);
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.circuitBreaker.errorThresholdPercentage", 50);
// 设置休眠期,断路器打开后,这段时间不会再执行命令,默认值为 5 秒,此处设置为 3 秒
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds", 3000);
// 该值决定是否执行超时
boolean isTimeout = true;
for (int i = 0; i < 10; i++) {// 发送10次请求,一开始均为失败,则会打开断路器
TestCommand c = new TestCommand(isTimeout);// 总会超时
c.execute();
// 输出健康状态等信息
HealthCounts hc = c.getMetrics().getHealthCounts();
System.out.println("断路器状态:" + c.isCircuitBreakerOpen() + ", 请求数量:"
+ hc.getTotalRequests());
if (c.isCircuitBreakerOpen()) {
// 如果断路器打开, 让下一次循环成功执行命令
isTimeout = false;//
System.out.println("============ 断路器打开了,等待休眠期结束 ===========");
Thread.sleep(6000);// 等待休眠期结束,6s之后尝试性发一次请求
}
}
}
/**
* 模拟超时的命令
*/
static class TestCommand extends HystrixCommand<String> {
private boolean isTimeout;
public TestCommand(boolean isTimeout) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(500)));// 500毫秒没有响应,则打开断路器
this.isTimeout = isTimeout;
}
@Override
protected String run() throws Exception {
// 让外部决定是否超时
if (isTimeout) {
// 模拟处理超时
Thread.sleep(800);// 方法执行时间为800毫秒
} else {
Thread.sleep(200);
}
return "";
}
@Override
protected String getFallback() {
return "fallback";
}
}
}
代码清单中配置了休眠期为 3秒,循环 10 次,创建 10 个命令并执行 在执行完第4 个命令后,断路器会被打开,此时我们等待休眠期结束,让下 次循环的命令执行成功。 代码清单中使用了 一个布尔值来决定是否执行成功,第5 次命令会执行成功,此时 路器将会被关闭,剩下的命令全部都可以正常执行。在循环体中,使用了 HealthCounts 象,该对象用于记录链路的健康信息。如果断路器关闭(链路恢复健康), Health Counts 面的健康信息将会被重置。
执行结果:
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:1
断路器状态:false, 请求数量:2
断路器状态:true, 请求数量:3
============ 断路器打开了,等待休眠期结束 ===========
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:3
断路器状态:false, 请求数量:3
断路器状态:false, 请求数量:5
7、隔离机制
为什么要做线程隔离 比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩。如下图
参考链接:www.jianshu.com/p/df1525d58…
命令的真正执行,除了断路器要关闭外,还需要再过一关:执行命令的线程池或者信号量是否满载。如果满载,命令就不会执行,而是直接触发回退,这样的机制,在控制命令的执行上,实现了错误的隔离。 Hystrix 提供了两种隔离策略。
- THREAD :默认值,由线程池来决定命令的执行,如线程池满载,则不会执行命令。 Hystrix 使用了 ThreadPoolExecutor 来控制线程池行为,线程池的默认大小为10。
- SEMAPHORE :由信号量来决定命令的执行,当请求的并发数高于阔值时,就不再执行命令。相对于线程策略,信号量策略开销更小,但是该策略不支持超时以及异步,除非对调用的服务有足够的信任,否则不建议使用该策略进行隔离。
使用代码测试线程隔离与信号隔离两个策略,编写共用的命令类:
/**
* 测试隔离策略的命令类
*/
public class MyCommand extends HystrixCommand<String> {
public int index;
public MyCommand(int index) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
.asKey("ExampleGroup")));
this.index = index;
}
@Override
protected String run() throws Exception {
Thread.sleep(500);
System.out.println("run(),当前索引:" + index);
return "success";
}
@Override
protected String getFallback() {
System.out.println("getFallback(),当前索引:" + index);
return "fallback";
}
}
测试线程隔离: 在使用线程隔离策略的运行类中 配置了线程池大小为 3,进行6次循环,意味着有3次命令将会触发回退,运行后可看到效果。
当用户请求服务A和服务I的时候,tomcat的线程(图中蓝色箭头标注)会将请求的任务交给服务A和服务I的内部线程池里面的线程(图中橘色箭头标注)来执行,tomcat的线程就可以去干别的事情去了,当服务A和服务I自己线程池里面的线程执行完任务之后,就会将调用的结果返回给tomcat的线程,从而实现资源的隔离,当有大量并发的时候,服务内部的线程池的数量就决定了整个服务的并发度,例如服务A的线程池大小为10个,当同时有12请求时,只会允许10个任务在执行,其他的任务被放在线程池队列中,或者是直接走降级服务,此时,如果服务A挂了,就不会造成大量的tomcat线程被服务A拖死,服务I依然能够提供服务。整个系统不会受太大的影响。
public class ThreadIso {
/**
* 测试线程隔离
*/
public static void main(String[] args) throws Exception {
// 设置线程池(超过3,认为是线程池满载)
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.threadpool.default.coreSize", 3);
for (int i = 0; i < 6; i++) {
MyCommand c = new MyCommand(i);
c.queue();//异步执行
}
Thread.sleep(5000);
}
}
运行结果:
getFallback(),当前索引:3
getFallback(),当前索引:4
getFallback(),当前索引:5
run(),当前索引:0
run(),当前索引:2
run(),当前索引:1
测试信号量隔离: 信号量的资源隔离只是起到一个开关的作用,例如,服务X的信号量大小为10,那么同时只允许10个tomcat的线程(此处是tomcat的线程,而不是服务X的独立线程池里面的线程)来访问服务X,其他的请求就会被拒绝,从而达到限流保护的作用。
注意代码中的 3个配置项,指定使用信号量作为隔离策略,分别设置了命令执行的最大并发数,以及执行回退的最大并发数。运行代码如下,最终只有两个命令正常执行,其余命令都会触发回退,可见信号量隔离的相关配置生效。
/**
* 测试信号量隔离
*/
public static void main1(String[] args) throws Exception {
// 配置使用信号量的策略进行隔离
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.strategy",
HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE);
// 设置最大并发数,默认为 10 本例配置为 2
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests", 2);
// 设置执行回退方法的最大并发数,默认为 10 本例配置为 20
ConfigurationManager.getConfigInstance().setProperty(
"hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests", 20);
//此处是tomcat的线程,而不是服务X的独立线程池里面的线程
for (int i = 0; i < 6; i++) {
final int index = i;
Thread t = new Thread() {
public void run() {
MyCommand c = new MyCommand(index);
c.execute();
}
};
t.start();
}
// 如果使用独立Hystrix的线程池里的线程,信号量控制不起作用都会 执行成功
// for (int i = 0; i < 6; i++) {
// MyCommand c = new MyCommand(i);
// c.queue();//异步执行
// }
Thread.sleep(5000);
}
执行结果:
getFallback(),当前索引:2
getFallback(),当前索引:0
getFallback(),当前索引:3
getFallback(),当前索引:5
run(),当前索引:4
run(),当前索引:1
| test | 线程池隔 | 信号量隔离 |
|---|---|---|
| 线程 | 与调用线程非相同线程 | 与调用线程相同(jetty线程) |
| 开销 | 排队、调度、上下文开销等 | 无线程切换,开销低 |
| 异步 | 支持 | 不支持 |
| 并发支持 | 支持(最大线程池大小) | 支持(最大信号量上限) |
总结 当请求的服务网络开销比较大的时候,或者是请求比较耗时的时候,我们最好是使用线程隔离策略,这样的话,可以保证大量的容器(tomcat)线程可用,不会由于服务原因,一直处于阻塞或等待状态,快速失败返回。而当我们请求缓存这些服务的时候,我们可以使用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了缓存服务的效率。
8、合并请求
合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文 根据前面小节的介绍可知,默认情况下,会为命令分配线程池来执行命令实例,线程 池会消耗一定的性能。对于一些同类型的请求 (URL 相同,参数不同) , Hystrix 提供了合 并请求的功能,在一次请求的过程中,可以将一个时间段内的相同请求(参数不同),收集 到同一个命令中执行,这样就节省了线程的开销,减少了网络连接,从而提升了执行的性能。 这个功能有点像数据库的批处理功能。 实现合并请求的功能,至少包含以下3个条件
- 需要有一个执行请求的命令,将全部参数进行整理,然后调用外部服务。
- 需要有一个合并处理器,用于收集请求,以及处理结果。
- 外部接口提供支持,例如外部的服务提供了/person/ {personName}的服务用于查找一 个Person ,如果合并请求,外部还需要提供一个/persons 的服务,用于查找多个Person。
接下来 实现一个简单的查找逻辑。假设有 以下场景: 客户端多次调用查找单个 Person的 Web 服务,而服务端提供了一个新的服务,可以传入多个名字,查找井返回多个 Person 实例 ,此时,可以考虑使用合并请求。编写一个命令类,用于收集请求参数以及调用服务, 请见代码清单如下:
/**
* 合并请求测试类
*/
public class CollapseTest {
/**
* 编写命令类,用于收集请求参数以及调用服务
*/
static class CollapserCommand extends HystrixCommand<Map<String, Person>> {
// 请求集合,第一个类型是单个请求返回的数据类型,第二是请求参数的类型
Collection<CollapsedRequest<Person, String>> requests;
private CollapserCommand(Collection<CollapsedRequest<Person, String>> requests) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")));
this.requests = requests;
}
@Override
protected Map<String, Person> run() throws Exception {
System.out.println("收集参数后执行命令,参数数量:" + requests.size());
// 处理参数
List<String> personNames = new ArrayList<>();
for (CollapsedRequest<Person, String> request : requests) {
personNames.add(request.getArgument());
}
// 调用服务(此处模拟调用),根据名称获取Person的Map
Map<String, Person> result = callService(personNames);
return result;
}
// 模拟服务返回
private Map<String, Person> callService(List<String> personNames) {
Map<String, Person> result = new HashMap<>();
for (String personName : personNames) {
Person p = new Person();
p.id = UUID.randomUUID().toString();
p.name = personName;
p.age = new Random().nextInt(30);
result.put(personName, p);
}
return result;
}
}
static class Person {
String id;
String name;
Integer age;
public String toString() {
return "id: " + id + ", name: " + name + ", age: " + age;
}
}
/**
* 合并处理器 -- 将请求进行合并
* 第一个类型为批处理返回的结果类型
* 第二个为单个请求返回的结果类型
* 第三个是请求参数类型
*/
static class MyHystrixCollapser extends HystrixCollapser<Map<String, Person>, Person, String> {
String personName;
public MyHystrixCollapser(String personName) {
this.personName = personName;
}
@Override
public String getRequestArgument() {
return personName;
}
@Override
protected HystrixCommand<Map<String, Person>> createCommand(Collection<CollapsedRequest<Person, String>> requests) {
return new CollapserCommand(requests);
}
@Override
protected void mapResponseToRequests(Map<String, Person> batchResponse, Collection<CollapsedRequest<Person, String>> requests) {
// 让结果与请求进行关联
for (CollapsedRequest<Person, String> request : requests) {
// 获取单个响应返回的结果
Person singleResult = batchResponse.get(request.getArgument());
// 关联到请求中
request.setResponse(singleResult);
}
}
}
public static void main(String[] args) throws Exception {
// 收集 1 秒内发生的请求,合并为一个命令执行
ConfigurationManager.getConfigInstance().setProperty("hystrix.collapser.default.timerDelayinMilliseconds",1000);
// 请求上下文
HystrixRequestContext context = HystrixRequestContext.initializeContext();
// 创建请求合并处理器
MyHystrixCollapser c1 = new MyHystrixCollapser("Angus");
MyHystrixCollapser c2 = new MyHystrixCollapser("Crazyit");
MyHystrixCollapser c3 = new MyHystrixCollapser("Sune");
MyHystrixCollapser c4 = new MyHystrixCollapser("Paris");
// 异步执行
Future<Person> f1 = c1.queue();
Future<Person> f2 = c2.queue();
Future<Person> f3 = c3.queue();
Future<Person> f4 = c4.queue();
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
context.shutdown();
}
}
在命令类 CollapserCommand 中, 维护着 CollapsedRequest 集合,一个CollapsedReuest实例表示一个请求,该类指定的第一个类型为“单请求返回的类型” 第二个类型为请求的参数类型。例如, CollapsedRequest<Person, String >,表示单次请求将会以 String 作为参数,返回一个 Person 实例。 代码清单中的粗体部分模拟调用查询多个 Person 的服务。
合并处理器中实现了父类 的3个方法, getRequestArgument 用于返回请求的参数 ,本例需要根据名称查询 Person,因此该参数为字符串:createCommand 返回实际执行的命令 (查找多个Person的批处理命令) ; mapResponseToRequests 方法将会在返回结果后执行, 可在该方法中设置结果与请求之间的关联。
运行类设置了“时间段”,在1秒内执行的请求将会被合并到一起执行,该“时间段” 的默认值为10毫秒。
执行结果:
收集参数后执行命令,参数数量:4
id: 79aa8267-7e9d-4285-be51-284469e2e4ab, name: Angus, age: 5
id: a0f9d22c-b03c-4aba-9b5b-e4c2389322bf, name: Crazyit, age: 9
id: f40490fa-fe5d-4dae-9510-d3c10c1676bc, name: Sune, age: 2
id: c6f2c140-f643-4978-946e-e8355335a69d, name: Paris, age: 27
虽然合并请求后只执行了一个命令,只启动了一个线程,只进行了一次网络请求,但是在收集请求、合并请求、处理结果的过程中仍然会耗费一定的时间。 总的来说,一般情况下,合并请求进行批处理,比发送多个请求快,对于一些服务的 URL 相同 、参数不同 的请求,笔者推荐使用合并请求的功能
9、请求缓存
Hystrix 支持缓存功能,如果在一次请求的过程中,多个地方调用同一个接口,可以考虑使用缓存。打开后,下一次的命令不会执行,直接到缓存中获取响应并返回,具体可参4 节介绍的运作流程 。开启缓存较为简单,在命令中重写父类的 getCacheKey即可。 代码清单:测试开启缓存和清空缓存。
/**
* 测试缓存的打开与关闭
*/
public class CacheMain {
public static void main(String[] args) {
// 初始化请求上下文,告知:这是一次请求
HystrixRequestContext context = HystrixRequestContext.initializeContext();
// 定义一个key
String cacheKey = "cache-key";
// 创建命令,开始执行
CacheCommand cacheCommand1 = new CacheCommand(cacheKey);
CacheCommand cacheCommand2 = new CacheCommand(cacheKey);
CacheCommand cacheCommand3 = new CacheCommand(cacheKey);
// 从结果可以发现,run只执行了一次
cacheCommand1.execute();
cacheCommand2.execute();
cacheCommand3.execute();
System.out.println("命令cacheCommand1是否读取缓存:" + cacheCommand1.isResponseFromCache());
System.out.println("命令cacheCommand2是否读取缓存:" + cacheCommand2.isResponseFromCache());
System.out.println("命令cacheCommand3是否读取缓存:" + cacheCommand3.isResponseFromCache());
// 清空缓存
HystrixRequestCache cache = HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey("MyCommoandKey"),
HystrixConcurrencyStrategyDefault.getInstance());
cache.clear(cacheKey);
CacheCommand cacheCommand4 = new CacheCommand(cacheKey);
cacheCommand4.execute();
System.out.println("命令cacheCommand4是否读取缓存:" + cacheCommand4.isResponseFromCache());
// 关闭
context.shutdown();
}
static class CacheCommand extends HystrixCommand<String> {
public String cacheKey;
public CacheCommand(String cacheKey) {
super(Setter.withGroupKey(
HystrixCommandGroupKey.Factory.asKey("GroupKey"))
.andCommandKey(HystrixCommandKey.Factory.asKey("MyCommoandKey")));
this.cacheKey = cacheKey;
}
@Override
protected String run() throws Exception {
System.out.println("run()");
return "success";
}
@Override
protected String getFallback() {
System.out.println("getFallback()");
return "fallback";
}
@Override
protected String getCacheKey() {
return this.cacheKey;
}
}
}
在代码清单的运行类中,使用了同一个缓存的 key 来创建命令实例。注意粗体代码,使用HystrixRequestCache 来清空缓存。获取 HystrixRequestCache 实例需要传入 CommandKey, 因此在命令的构造器中,也设置了 CommandKey 。
根据输出结果可知,命令实际上只执行了两次: c2与c3 这两个命令执行时都读取了缓存,而 c4 在执行前清空了缓存。 运行结果如下:
执行run()方法
命令cacheCommand1是否读取缓存:false
命令cacheCommand2是否读取缓存:true
命令cacheCommand3是否读取缓存:true
执行run()方法
命令cacheCommand4是否读取缓存:false
合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文