Hystrix组件提供了两种隔离的解决方案:线程池隔离和信号量隔离。两种隔离方式都是限制对共享资源的并发访问量,线程在就绪状态、运行状态、阻塞状态、终止状态间转变时需要由操作系统调度,占用很大的性能消耗;而信号量是在访问共享资源时,进行tryAcquire,tryAcquire成功才允许访问共享资源。
protected HelloCommandIsolateThreadPool(String name) {
super(HystrixCommand.Setter.
//设置GroupKey 用于dashboard 分组展示
withGroupKey(HystrixCommandGroupKey.Factory.asKey("hello"))
//设置commandKey 用户隔离线程池,不同的commandKey会使用不同的线程池
.andCommandKey(HystrixCommandKey.Factory.asKey("hello" + name))
//设置线程池名字的前缀,默认使用commandKey
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("hello$Pool" + name))
//设置线程池相关参数
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(15)
.withMaxQueueSize(10)
.withQueueSizeRejectionThreshold(2))
//设置command相关参数
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
//是否开启熔断器机制
.withCircuitBreakerEnabled(true)
//舱壁隔离策略
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
//circuitBreaker打开后多久关闭
.withCircuitBreakerSleepWindowInMilliseconds(5000)));
}
. withExecutionIsolationStrategy (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) 设置隔离策略是关键,默认是信号隔离,如果不设置为线程池隔离,上面设置的线程池相关的参数都无意义。开启熔断器机制,如果在10秒内50%以上的请求都失败,回路就会被断开,后面的请求都会直接返回失败,即 Fast Fail 策略。 withCircuitBreakerSleepWindowInMilliseconds 5秒后会尝试闭合电路 。
在系统内部,线程池存放在一个ConcurrentHashMap中,key是commandKey ,value就是线程池。线程池的名字是 ThreadPoolKey值。
为避免在系统运行过程中,频繁的创建新的线程,过段时间又销毁线程,在Hystrix系统内部,线程池的最大线程数和核心线程数是同样大小,所以设置时,只有一个CoreSize参数需要设置。
- 线程池名字andThreadPoolKey ,
- coreSize(核心线程池大小) ,
- KeepAliveTimeMinutes(线程存存活时间),
- MaxQueueSize(最大队列度),
- QueueSizeRejectionThreshold(拒绝执行的阀值)等等。
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(resourcesManager.getThreadPoolProperties(platformProtocol.getAppId()).getCoreSize())
.withKeepAliveTimeMinutes(resourcesManager.getThreadPoolProperties(platformProtocol.getAppId()).getKeepAliveSeconds())
.withMaxQueueSize(resourcesManager.getThreadPoolProperties(platformProtocol.getAppId()).getMaxQueueSize())
.withQueueSizeRejectionThreshold(resourcesManager.getThreadPoolProperties(platformProtocol.getAppId()).getQueueSizeRejectionThreshold()))
threadPoolKey 也是线程池的名字的前缀,默认前缀是 hystrix 。在Hystrix中,核心线程数和最大线程数是一致的,减少线程临时创
建和销毁带来的性能开销。
线程池的默认参数都在HystrixThreadPoolProperties中,重点讲解一下参数queueSizeRejectionThreshold 和maxQueueSize 。
- queueSizeRejectionThreshold默认值是5,允许在队列中的等待的任务数量。
- maxQueueSize默认值是-1,队列大小。如果是Fast Fail 应用,建议使用默认值。线程池饱满后直接拒绝后续的任务,不再进行等待。
@Override
public boolean isQueueSpaceAvailable() {
if (queueSize <= 0) {
// we don't have a queue so we won't look for space but instead
// let the thread-pool reject or not
return true;
} else {
return threadPool.getQueue().size() < properties.queueSizeRejectionThreshold().get();
}
}
线程池一旦创建完成,相关参数就不会更改,存放在静态的ConcurrentHashMap中,key是对应的commandKey 。而queueSizeRejectionThreshold是每个命令都是设置的。线程池的相关参数都保存在HystrixThreadPool这个类文件中,线程池的创建方法getThreadPool则在HystrixConcurrencyStrategy类文件中。从getThreadPool方法可以看出线程池的名字就是hystrix-threadPoolKey-threadNumber.
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "hystrix-" + threadPoolKey.name() + "-" + threadNumber.incrementAndGet());
thread.setDaemon(true);
return thread;
}
/* package */final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
String key = threadPoolKey.name();
// this should find it for all but the first time
HystrixThreadPool previouslyCached = threadPools.get(key);
if (previouslyCached != null) {
return previouslyCached;
}
// if we get here this is the first time so we need to initialize
synchronized (HystrixThreadPool.class) {
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}
}
return threadPools.get(key);
final protected Observable<R> getExecutionObservable() {
return Observable.create(new OnSubscribe<R>() {
@Override
public void call(Subscriber<? super R> s) {
try {
s.onNext(run());
s.onCompleted();
} catch (Throwable e) {
s.onError(e);
}
}
});
}
在这个Call方法中执行了具体的业务逻辑run() ;
线程隔离的优点:
- 使用线程可以完全隔离第三方代码,请求线程可以快速放回。
- 当一个失败的依赖再次变成可用时,线程池将清理,并立即恢复可用,而不是一个长时间的恢复。
- 可以完全模拟异步调用,方便异步编程。
线程隔离的缺点:
- 线程池的主要缺点是它增加了cpu,因为每个命令的执行涉及到排队(默认使用SynchronousQueue避免排队),调度和上下文切换。
- 对使用ThreadLocal等依赖线程状态的代码增加复杂性,需要手动传递和清理线程状态。
注: Netflix公司内部认为线程隔离开销足够小,不会造成重大的成本或性能的影响。Netflix 内部API 每天100亿的HystrixCommand依赖请求使用线程隔,每个应用大约40多个线程池,每个线程池大约5-20个线程。
Semaphore that only supports tryAcquire and never blocks and that supports a dynamic permit count.
Using AtomicInteger increment/decrement instead of java.util.concurrent.Semaphore since we don't need blocking and need a custom implementation to get the dynamic permit count and since AtomicInteger achieves the same behavior and performance without the more complex implementation of the actual Semaphore class using AbstractQueueSynchronizer.
在开发时,跟线程池隔离类似,同样是继承HystrixCommand类,在run方法中实现业务逻辑,通过getFallback 实现优雅降级。只是在设置隔离策略及相关参数数有较小的变化:
protected HelloCommandIsolateSemaphore(String key, int semaphoreCount) {
super(HystrixCommand.Setter
//设置GroupKey 用于dashboard 分组展示
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("hello"))
//设置CommandKey 用于Semaphore分组,相同的CommandKey属于同一组隔离资源
.andCommandKey(HystrixCommandKey.Factory.asKey("hello" + key))
//设置隔离级别:Semaphore
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
//是否开启熔断器机制
.withCircuitBreakerEnabled(true)
//舱壁隔离策略
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
//设置每组command可以申请的permit最大数
.withExecutionIsolationSemaphoreMaxConcurrentRequests(50)
//circuitBreaker打开后多久关闭
.withCircuitBreakerSleepWindowInMilliseconds(5000)));
}
TryableSemaphore 接口定义了信号隔离的行为,内部借助AtomicInteger类实现资源的分配。HystrixProperty<Integer> numberOfPermits 存储可分配的资源,AtomicInteger count存储已分配的资源。numberOfPermits 在类初始化时就需要赋值,所以定义成了final类型。
protected final HystrixProperty<Integer> numberOfPermits;
private final AtomicInteger count = new AtomicInteger(0);
@Override
public boolean tryAcquire() {
int currentCount = count.incrementAndGet();
if (currentCount > numberOfPermits.get()) {
count.decrementAndGet();
return false;
} else {
return true;
}
}