Hystrix快速⼊门
祝⼤家国庆快乐!
对⼤部分电商和快递公司来说,每年年底(Q4季度)由于双11等⼤促活动的存在,将⾯对⼤量的⽤户流量,尤其是属于⼤促的那⼏天,⽆论是⽤户的商品订单还是物流订单,都将是平时的3倍以上。对于技术⼈员来说,提前落地相应的服务保障体系,并进⾏相应的压测和演习,是题中应有之意。整个保障体系的实现涉及的环节很多,本⽂将选取奈飞Netflix公司的Hystrix"豪猪"框架(其基于Java语⾔和最近⽐较流⾏RxJava流式框架),针对分布式应⽤的服务保障问题进⾏探讨,之后将按照基本知识、应⽤实践、配置知识和源码分析的顺序进⾏介绍,不⾜之处望不吝赐教。
⾸先通过⼀张思维导图来展⽰本⽂的思路,有标记部分的推荐程度⾼,图可以拖到浏览器新窗⼝放⼤。经过⼤半年的调整,⼿还是⽐较⽣,我熊⼆哥⼜回来了!
为了便于理解本⽂的意图,⾸先提出并解答两个问题。
1.为什么需要在项⽬中引⼊Hystrix,其可以应⽤在什么场景中?
在分布式系统中,单个应⽤通常会有多个不同类型的外部依赖服务,内部通常依赖于各种RPC服务,外部则依赖于各种HTTP服务。这些依赖服务不可避免的会出现调⽤失败,⽐如超时、异常等情况,如何在外
部依赖出问题的情况,仍然保证⾃⾝应⽤的稳定,就是Hystrix这类服务保障框架的⼯作了。常见的服务依赖如下图所⽰,应⽤X依赖于服务A、B和C,A和B正常提供服务,C服务出错,这是如何避免C服务对A、B服务产⽣影响,也引出了⼀个隔离的概念。
举个例⼦来说,某个应⽤中依赖了30个外部服务,实际应⽤中通常⽐这还要多,假设每个服务的可⽤性为99.9%,3个9的可⽤性,算是不错了,但99.9%的30次幂≈ 97.0%,这个可⽤性已经是⽆法容忍的了。
2.Hytrix的⽬标是什么,其采⽤了什么⼿段来达到该⽬标?
Hystrix的⽬标就是能够在1个或多个依赖出现问题时,系统依然可以稳定的运⾏,其⼿段包括隔离、限流和降级有等,接下来详细介绍这些⼿段。补充⼀点,张开涛⽼师曾对系统⾼可⽤⼿段进⾏过总结,除了以上的限流、隔离和降级,还有负载均衡、超时与重试、回滚、压测与预案,共7种⼿段。
隔离
隔离说到底还是分治思想的体现,在当前场景中,就是将不同的外部依赖进⾏分类,确定其边界,然后隔离开来分开进⾏管理。Hystrix⽀持的隔离策略isolationStrategy包括信号量和线程池两种,具体内容将在之后限流知识中介绍。
限流
在基于服务化(包括SOA和微服务)的系统架构中,对服务请求进⾏限流是保护服务稳定性的⼀个常见⼿段。此外,关于限流有两个⽐较重要的概念:限流算法,包括计数限流、令牌桶和漏桶等;限流粒度,包括⽅法级别、接⼝级别、应⽤级别、集级别等。对于Hystrix来说,其采⽤了⾃⼰的⼀套限流⽅式,这⾥⾸先延续之前隔离知识中提到的信号量和线程池概念进⾏介绍。
信号量概念⽐较简单,常⽤于获取共享资源的场景中,⽐如计算机连接了两个打印机,那么初始的信号量就是2,被某个进程或线程获取后减1,信号量为0后,需要获取的线程或进程进⼊资源等待状态。Hystrix的处理有些不同,其不等待,直接返回失败。
线程池采⽤的就是jdk的线程池,其默认选⽤不使⽤阻塞队列的线程池,例如线程池⼤⼩为10,如果某时刻10个线程均被使⽤,那么新的请求将不会进⼊等待队列,⽽是直接返回失败,起到限流的作⽤。
此外,其还引⼊了⼀个断路器机制,当断路器处于打开状态时,直接返回失败或进⼊降级流程。断路器打开和关闭的触发流程为:当总的请求数达到可阈值HystrixCommandProperties.circuitBreakerRequestVolumeThreshold(),或总的请求失败百分⽐达到了阈
springboot其实就是spring
值HystrixCommandProperties.circuitBreakerErrorThresholdPercentage(),这时将断路器的状态由关闭设置为打开。当断路器打开时,所有的请求均被短路,在经过指定休眠时间窗⼝后,让下⼀个请求通过(断路器被认为是半开状态)。如果请求失败,断路器进⼊打开状态,并进⼊新的休眠窗⼝;否则进⼊关闭状态。
断路器依赖的统计信息如下图所⽰,默认情况下10s为⼀个统计周期,10个滚动窗⼝,每个负责统计1s内的数据,包括请求成功、失败、超时和拒绝次数。
降级
这⾥的降级具体来说就是服务质量的降级,需要注意的是,只有⽅法所属的业务场景适合降级时才采⽤,⼀般为查询场景。Hystrix通过配置fallbackMethod指定降级时的处理⽅法,触发降级动作的4种情况如下所⽰。
run()⽅法抛出⾮HystrixBadRequestException异常。
run()⽅法调⽤超时
熔断器开启拦截调⽤
线程池/队列/信号量是否跑满
Hystrix整体的处理流程
主题流程如图所⽰,Hystrix框架通过命令模式来实现⽅法粒度上的服务保障,主要涉及HystrixCommand和HystrixObservableCommand类,前者提供同步的execute和异步的queue⽅法,后者提供⽴即执⾏observe和延迟执⾏toObservable的回调⽅法。此外,实际项⽬中通常不会使⽤Hystrix集成的本地缓存。
tip:
⽬前在服务保障⽅⾯,除了hystrix框架外,阿⾥巴巴公司开源的也是⼀个不错的可选⽅案。
该节将从基础应⽤、项⽬应⽤、动态配置和监控等⼏个⽅⾯进⾏介绍。Hystrix基础应⽤⽐较简单,包括直接编码和使⽤注解等两种⽅式,⼀般选⽤注解⽅式,其基于javanica⼦包,,之后简要展⽰Hystrix如何在基于gradle依赖管理的Springboot应⽤中集成。
基础应⽤
1.Gradle配置和SpringBoot配置
//Gradle中添加Hystrix核⼼
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
@Configuration
public class HystrixConfiguration {
@Bean
public HystrixCommandAspect hystrixAspect() {
return new HystrixCommandAspect();
}
}
2.同步使⽤⽅式(异步和响应式可以参考javanica的wiki页⾯)
@DefaultProperties(groupKey = "UserQueryGroup", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "20"),
@HystrixProperty(name = "maxQueueSize", value = "20")
})
@Service
public class UserQueryManager {
@HystrixCommand(commandKey = "GetUserCommand",fallbackMethod = "getUserFallback")
public String getUser(long id) {
return "test_user";
}
public String getUserFallback(long id) {
return "test_user_fallback";
}
}
项⽬应⽤
新建项⽬直接在涉及外部依赖服务的⽅法上加上相应注解即可,⽐较简单。⽽对于既有系统,为了降低相关风险,推荐采⽤引⼊开关变
量,AB灰度的⽅式,具体⽅式如下图所⽰。
动态配置
在实际应⽤中,当发现在线应⽤的命令或线程池相关参数不合理时,如何进⾏参数的实时调优?⽬前,Hystrix提供了ConfigurationManager配置管理类来实时管理配置信息,是配置相关的核⼼类,既可以通过实现PolledConfigurationSource类,借助FixedDelayPollingScheduler类定时的PULL最新的配置信息,也可以通过⾃定义的⽅式监听相关配置项的修改以PUSH⽅式对配置进⾏修改。此外,每个Hystrix参数都有4个地⽅可以配置,优先级从低到⾼如下,如果每个地⽅都配置相同的属性,则优先级⾼的值会覆盖优先级低的值。
内置全局默认值:写死在代码⾥的值采⽤ConcurrentHashMap 有 HystrixCommandProperties HystrixThreadPoolProperties
HystrixCollapserProperties
动态全局默认属性:通过属性⽂件配置全局的值
内置实例默认值:写死在代码⾥的实例的值
动态配置实例属性:通过属性⽂件配置特定实例的值
Tip:
Hystrix默认的配置框架,
应⽤监控
Hystrix源⽣提供了单机和集的监控服务,单机借助Hystrix-Dashboard,集借助Turbine,这⾥只介绍单机监控的实现,代码如下。
//Gradle中添加Hystrix⾯板
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix-dashboard')
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
之后直接访问localhost:port/hystrix即可进⼊⾯板管理页⾯,配置好hystrix.stream信息后就可以看到如下监控页⾯。可以看到getArticle⽅法的失败率达到59.0%,断路器已打开,⼤量请求被降级,请求峰值得到缓解,这部分可以使⽤Jmeter进⾏测试。
在了解hystrix时,我最开始就曾被3个KEY给打败过,其分别是CommandGroupKey,CommandKey和
ThreadPoolKey。其实可以通过⼀个很简单的划⽅式就可以将这3个KEY区分开,CommandGroupKey是⼀个纯逻辑的概念,其可以管理多个CommandKey,且在默认情况下ThreadPool和它同名,⽽后两者则带有实际意味,之后的配置信息可以看到,所有的配置都是基于Command和ThreadPool的。
关键配置
Hystrix的配置项⽐较多,⼤概有30个左右,但⽐较基础和关键的就是以下的10来个配置项,主要包
括CommandProperties和ThreadPoolProperties两部分。
命令配置中,隔离策略包括线程池和信号量两种,默认和推荐使⽤前者,线程的超时时间⼀般设置为⽐依赖调⽤的99线平均时间略⾼即可。断路器部分,请求数量的熔断阈值和请求失败⽐例的熔断阈值推荐更加实际的测试请求进⾏设置,统计信息的滑动窗⼝⼤⼩和分桶数采⽤默认值通常就可以满⾜需求。
线程池配置中,主要就是线程⼤⼩的设置,默认为10个,推荐根据所管理服务的单机QPS和TP99线计算得出,这部分⽀持动态配置,可以在线实时调整。这部分配置很重要,虽然Hystrix推荐创建40个左右的线程池,每个10个线程左右,但实际项⽬中,⼀定要对当前应⽤的依赖服务进⾏合理分类,否则⼤量的线程池和线程会对应⽤带来⼀定不良影响。
Hystrix由于引⼊了rxJava响应式编程,代码风格与过去习惯的结构化风格有⼀些差异,接下来从@Hystr
ixCommand注解解析开始,简要展⽰命令执⾏的整个过程,解析在代码注释中。
HystrixCommandAspect
@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {
Method method = getMethodFromTarget(joinPoint);
...
MetaHolderFactory metaHolderFactory = META_HOLDER_(HystrixPointcutType.of(method));
MetaHolder metaHolder = ate(joinPoint);
HystrixInvokable invokable = Instance().create(metaHolder);//1.创建metaHolder
ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
Object result;
try {//2.命令执⾏
if (!metaHolder.isObservable()) {
result = ute(invokable, executionType, metaHolder);
} else {
result = executeObservable(invokable, executionType, metaHolder);
}
} ...
}
CommandExecutor
public static Object execute(HystrixInvokable invokable, ExecutionType executionType, MetaHolder metaHolder) throws RuntimeException {
...
switch (executionType) {
case SYNCHRONOUS: {//1.同步执⾏,其实其内部也是⽤的异步执⾏queue().get()
return castToExecutable(invokable, executionType).execute();
}
case ASYNCHRONOUS: {//2.异步执⾏
HystrixExecutable executable = castToExecutable(invokable, executionType);
if (metaHolder.hasFallbackMethodCommand()
&& ExecutionType.ASYNCHRONOUS == FallbackExecutionType()) {
return new FutureDecorator(executable.queue());
}
return executable.queue();
}
case OBSERVABLE: {//3.响应式执⾏,Observable()是核⼼⽅法
HystrixObservable observable = castToObservable(invokable);
return ObservableExecutionMode.EAGER == ObservableExecutionMode() ? observable.observe() : Observable(); }
...
}
AbstractCommand
public Observable<R> toObservable() {
...
final Func0<Observable<R>> applyHystrixSemantics = new Func0<Observable<R>>() {
@Override
public Observable<R> call() {
if (().equals(CommandState.UNSUBSCRIBED)) {
ver();
}
return applyHystrixSemantics(_cmd);//1.关键步骤,命令处理
}
};
...
private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) {
...
if (circuitBreaker.attemptExecution()) {//1.【断路器相关处理】,之后HystrixCircuitBreaker中展⽰
..
if (Acquire()) {//2.获取信号量,如果是THREAD线程池策略,【直接返回true】,这⾥需要注意,不然流程将进⾏不下去 try {
executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis());
return executeCommandAndObserve(_cmd)//3.核⼼执⾏⽅法
.doOnError(markExceptionThrown)
.doOnTerminate(singleSemaphoreRelease)
.doOnUnsubscribe(singleSemaphoreRelease);
} ...
}
private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) {
...
final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() {
@Override
public Observable<R> call(Throwable t) {
circuitBreaker.markNonSuccess();
Exception e = getExceptionFromThrowable(t);
executionResult = executionResult.setExecutionException(e);
if (e instanceof RejectedExecutionException) {
return handleThreadPoolRejectionViaFallback(e);//1.处理线程池拒绝场景
} else if (t instanceof HystrixTimeoutException) {
return handleTimeoutViaFallback();//2.处理请求超时场景
} else if (t instanceof HystrixBadRequestException) {
return handleBadRequestByEmittingError(e);
} else {
/*
* Treat HystrixBadRequestException from ExecutionHook like a plain HystrixBadRequestException.
*/
if (e instanceof HystrixBadRequestException) {
eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey);
(e);
}
return handleFailureViaFallback(e);//3.处理请求失败场景
}
}
};
HystrixCircuitBreaker
@Override
public boolean attemptExecution() {
if (properties.circuitBreakerForceOpen().get()) {//1.断路器是否强制打开
return false;
}
if (properties.circuitBreakerForceClosed().get()) {//2.断路器是否强制关闭
return true;
}
if (() == -1) {//3.短路器关闭
return true;
} else {
if (isAfterSleepWindow()) {//4.是否经过了指定的窗⼝时间
if (statuspareAndSet(Status.OPEN, Status.HALF_OPEN)) {//5.设置为半打开状态
//only the first request after sleep window should execute
return true;
} else {
return false;
}
} else {
return false;
}
}
}
Tip: ⽼铁告知,⽬前阿⾥已经在部分业务项⽬中落地基于RxJava的全异步应⽤,⼤幅提⾼了资源的利⽤效率,⼤家有空可以关注下,【Java11】本周好像也已新鲜出炉。
名词解释
TP=Top Percentile,Top百分数,是⼀个统计学⾥的术语,与平均数、中位数都是⼀类。
TP50、TP90和TP99等指标常⽤于系统性能监控场景,指⾼于50%、90%、99%等百分线的情况。
OSS = Open Source Software,开源软件(开放源代码软件)。
OSS= Operation Support System,运营⽀撑系统。
参考资料
王兴:“对未来越有信⼼,对现在越有耐⼼!”
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论