命令使用
我们需要构建封装调用的类,然后new该类的实例来执行hystrix调用。类的构建需要继承HystrixCommand 或者 HystrixObservableCommand。通常,继承自HystrixCommand足以。HystrixObservableCommand使用的是rxJava的模式,本质一样,语法不同而已。
下面以HystrixObservableCommand为例。由于该类没有无参构造方法,所以我们需要在子类构造方法中显示调用HystrixCommand构造方法来指定一些属性。另外还要实现run方法来写具体的调用过程。
定义好类以后,就是new实例执行调用了。分三种情况。
1)同步调用,有两种形式:
2)非响应式异步调用,也就是future的get不直接调用,先queue在get:
3)响应式异步调用,响应式其实就是回调,调用结束后我们不需要主动获取结果,而是在调用时就传入一个回调函数,调用结束后触发回调处理结果。借助的是rxJava的观察者模式。hystrix重写了部分方法,大体就是command执行结束,调用observer的onNext后者onError处理结果。需要注意的是,command执行是在另外的线程池中,所以主线程要sleep才能看到回调:
public class HelloCommand extends HystrixCommand<String> {
String param;
protected HelloCommand(String param) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)));
this.param = param;
}
@Override
protected String run() throws Exception {
Thread.sleep(2000);
return "ly " + param ;
}
@Override
protected String getFallback(){
return "wrong";
}
public static void main(String args[]) throws ExecutionException, InterruptedException {
// 同步式
System.out.println(new HelloCommand("jaja").execute());
System.out.println(new HelloCommand("jaja").queue().get());
// 异步式
Future<String> s = new HelloCommand("jaja").queue();
System.out.println("rrr");
System.out.println(s.get());
// 响应式
new HelloCommand("jaja").observe().subscribe(new Observer<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onNext(String s) {
System.out.println(s);
}
});
// 保证回调可以看到
Thread.sleep(10000);
}
FailBack
要注意的是,如果failback的值也要network call,就需要再包一层hystrix command。
public class FailCommand extends HystrixCommand<String> {
String param;
protected FailCommand(String param) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)));
this.param = param;
}
@Override
protected String run() throws Exception {
//Thread.sleep(20000);
throw new NullPointerException();
//return "ly " + param;
}
@Override
protected String getFallback() {
return "wrong";
}
public static void main(String args[]){
System.out.println(new FailCommand("jaja").execute());
}
}
CommandName
默认是类名:getClass().getSimpleName(),当然可以在super中通过Setter指定,之前的代码里。CommandName其实没有太大作用,除了在后面动态配置中根据这个name配置属性。
CommandGroup
命令组名,逻辑分组,使用HystrixCommandGroupKey。指南上写着用于出报表,暂时没用过。
Thread-pool key
线程key,使用HystrixThreadPoolKey,这个比较重要,不同的thread key对应不同的线程,最终监控,短路等都是以这个为单位的。如果不指定默认使用的是CommandGroupKey。
Cache
hystrix提供了缓存功能,需要实现getCacheKey方法,如果key一样,就返回缓存的值。另外使用缓存必须让command运行在同一个context下,需要开启context,否则报错。
public class CacheCommand extends HystrixCommand<Integer> {
Integer param;
protected CacheCommand(Integer param) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("PrimaryCommand"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)));
this.param = param;
}
@Override
protected Integer run() throws Exception {
return new Random().nextInt(100);
}
@Override
protected Integer getFallback() {
return -1;
}
@Override
protected String getCacheKey() {
return "" + param % 2;
}
public static void main(String args[]){
HystrixRequestContext context = HystrixRequestContext.initializeContext();
System.out.println(new CacheCommand(100).execute());
System.out.println(new CacheCommand(102).execute());
}
断路器
原理是维持一个时间窗口,统计时间窗口内的超时,错误的比例,达到阈值就打开断路器,随后的请求全部都failback,然后再过一段时间变成半开半闭,即放一个请求进来,如果没有问题就关闭否则仍旧打开。
hystrix也不是每一个请求都计算统计信息,而是分桶,桶的数目可以根据配置指定。随着时间的流逝,新的请求所在的桶成型了再一起计算新桶内的统计值,这样可以降级cpu的消耗。同时丢弃最旧的桶。
关于断路器的逻辑更详细的在官网:https://github.com/Netflix/Hystrix/wiki/How-it-Works
- Assuming the volume across a circuit meets a certain threshold (
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)... - And assuming that the error percentage exceeds the threshold error percentage (
HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
)... - Then the circuit-breaker transitions from
CLOSED
toOPEN
. - While it is open, it short-circuits all requests made against that circuit-breaker.
- After some amount of time (
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
), the next single request is let through (this is theHALF-OPEN
state). If the request fails, the circuit-breaker returns to theOPEN
state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions toCLOSED
and the logic in 1. takes over again.
属性
hystrix有很多,下面分类介绍下主要的属性。
hystrix配置属性有4个level,优先级从低到高依次为:
hystrix代码写死的默认值;
从全局配置文件读到的默认值;
实例静态默认值,这个指的就是构建HystrixCommand时,通过注解或者给父类构造器传参的方式设置默认值,具体配置项的名字在官网有权威总结。
实例动态值,这个需要专门介绍下,感觉官网的解释很少。但是这个功能确是相当有用啊,因为我们非常有可能在程序运行时动态修改配置,比如超时时间或者线程池大小等等。需要借助ConfigurationManager类来重设置,下面有例子。另外配置项的key要稍作修改,把上面的实例静态默认值中的“default”修改为{hystrixCommandKey},也就是命令的名字。
比如:静态配置名为“hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds”,我们的commnd名字为“HelloCommand”,那么动态的配置名就是“hystrix.command.HelloCommand.execution.isolation.thread.timeoutInMilliseconds”
public class ConfigCommand extends HystrixCommand<String> {
String param;
protected ConfigCommand(String param) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("TestCommand"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)));
this.param = param;
}
@Override
protected String run() throws Exception {
Thread.sleep(2000);
return "ly " + param;
}
@Override
protected String getFallback() {
return "wrong";
}
public static void main(String args[]){
System.out.println(new ConfigCommand("jaja").execute());
//hystrix.command.TestCommand.execution.isolation.thread.timeoutInMilliseconds
ConfigurationManager.getConfigInstance().setProperty("hystrix.command.TestCommand.execution.isolation.thread.timeoutInMilliseconds", 1000);
System.out.println(new ConfigCommand("jaja").execute());
}
}
第一个调用没有超时,第二个超时了。
ly jaja
2019-02-28 15:59:44 DEBUG HystrixPropertiesChainedProperty:236 - Property changed: 'hystrix.command.TestCommand.execution.isolation.thread.timeoutInMilliseconds = 1000'
2019-02-28 15:59:44 DEBUG HystrixPropertiesChainedProperty:90 - Flipping property: hystrix.command.TestCommand.execution.isolation.thread.timeoutInMilliseconds to use its current value: 1000
wrong
可以从日志看到超时属性变了。
下面分组过一下关键的配置项,这里只是介绍含义,具体的名字和默认值在官网:https://github.com/Netflix/Hystrix/wiki/Configuration
Execution
execution.isolation.strategy:隔离策略,有两种:
Thread:线程隔离,推荐的方式,如果下游服务挂了,调用方可以被安全隔离。但是中间加了一个线程池,对服务器的资源有一定消耗。官网说,只要qps不是太大,消耗的线程资源可以接受,就用这种方式。
Semaphore:信号量,中间没有线程池,调用方直接在当前线程下调用下游服务,通过限制并发请求的数目来隔离,一旦下游挂了,调用方资源不能及时释放,比如web服务线程将被一直消耗着。
execution.isolation.thread.timeoutInMilliseconds:服务超时时间,单位毫秒,默认1000.
execution.timeout.enabled:是否打开超时检测。
execution.isolation.semaphore.maxConcurrentRequests:使用信号量隔离时qps阈值,后续的请求会被拒。
Circuit Breaker
circuitBreaker.enabled:是否打开断路器
circuitBreaker.requestVolumeThreshold:断路器检测的基础请求值,只有时间窗口内的请求数达到这个阈值时,才会判定错误率,否则比如只有一两个请求,即便都失败了,也不会打开断路器,因为基数太少了。
circuitBreaker.sleepWindowInMilliseconds:指的是从断路器打开状态到半开半闭状态需要的时间,即断路后,需要等多久才能放一个请求进来。
circuitBreaker.errorThresholdPercentage:错误百分比,超过就会短路。
Metrics
metrics.rollingStats.timeInMilliseconds:时间窗口大小
metrics.rollingStats.numBuckets:时间窗口的桶的数目,必须能被时间窗口大小整除,否则报错。
metrics.healthSnapshot.intervalInMilliseconds:每一次检测的间隙。因为就算分窗口统计错误率,也会很占cpu,所以每一次统计都会等一个时间间隔再开始。
metrics.rollingPercentile.enabled:带rollingPercentile的都表示调用时延的统计,该选项表示是否打开时延统计,比如说95分位99分位等,如果关闭都返回-1.
metrics.rollingPercentile.timeInMilliseconds:时延统计的时间窗口。
metrics.rollingPercentile.numBuckets:时延统计的桶数目。
metrics.rollingPercentile.bucketSize:时延统计的桶大小,时延统计每一个桶只维持最新的该数值的请求的数据,早一些的将会被覆盖。
Thread pool
coreSize:核心线程数,不会变
maximumSize:最大线程数,如果不够了就进入等待队列。该属性是最大可并发的请求数。
maxQueueSize:等待队列,还超就会被拒。这个数值无法动态修改。
queueSizeRejectionThreshold:进入queue时被拒的概率值,即便是没有达到maxQueueSize。这个为了弥补上面无法动态修改的不足。可以通过这个概率值来控制队列大小。
这些参数好像和线程池的参数含义略有不同。