spring cloud Hystrix的运用与源码分析
简介
复杂的分布式系统,存在几十或者上百个服务互相依赖,如果由于某些原因导致其中部分服务无法使用,则导致访问这些服务的请求将会阻塞,并且会影响和其有依赖的服务,严重的导致整个系统瘫痪。Hystrix的出现就可以解决这种问题,对不可用的服务的熔断以及故障处理方案保证即使当前服务不可用,也不影响和其具有依赖的服务,并且访问该服务的请求也不会阻塞在服务上。
如图,一个用户请求服务A,服务A调用服务B,服务B依赖服务C。任何一个服务挂掉的话,都会导致请求阻塞和失败。
简单用例
基于Ribbon搭配使用histyix
本文所使用的工程是基于spring cloud Ribbon运用与源码分析
中的工程进行改造
pom依赖
在pom文件里添加histyix的相关依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
启动类改造
需要给启动类添加一个@EnableHystrix
注解开启Hystrix的组件。
@EnableHystrix
@SpringBootApplication
public class RibbonApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonApplication.class, args);
}
}
对service类的改造
@Service
public class RibbonService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloError")
public String hello(String name) {
return restTemplate.getForObject("http://eureka-client/hello/?name=" + name, String.class);
}
public String helloError(String name){
return "hi, " + name + "error";
}
}
在方法上添加@HystrixCommand(fallbackMethod = "helloError")
注解,其中fallbackMethod = "helloError"
表示当前访问的方法出现异常时,回调helloError
方法。
接下来启动相应的eureka工程以及本工程,访问http://localhost:8088/hello/?name=aa。
可以观察服务正常运行,这时停止eureka 的client端服务,再次访问http://localhost:8088/hello/?name=aa。
可以看到已经调用之前写的方法返回结果。
基于Feign上使用Hystrix
在上篇文章中关于feign有过简单的介绍,feign是集成了Hystrix的组件,所以使用Hystrix功能,不需要在引用额外的依赖,只需开启其配置就行。
本文基于spring cloud feign 运用与源码解析
中的工程进行改造
application.properties的修改
spring.application.name=eureka-feign
server.port=8081
eureka.client.serviceUrl.defaultZone=http://localhost:1101/eureka/
# 添加开启Hystrix的配置
feign.hystrix.enabled=true
接口的改造
因为Feign是应用在接口上的,所以在其@FeignClient
注解上需要增加配置。
@FeignClient(value = "eureka-client", configuration = FeginConfig.class, fallback = HiHystrix.class)
public interface EurekaClientFeign {
@GetMapping("/hello/")
String hello(@RequestParam String name);
}
在@FeignClient
注解上增加了fallback = HiHystrix.class
的配置,当访问接口出现异常时,回调HiHystrix类进行处理。
HiHystrix类的编写
@Component
public class HiHystrix implements EurekaClientFeign{
@Override
public String hello(String name) {
return "hi, " + name + ", error";
}
}
HiHystrix作为feign注解使用接口的熔断器逻辑处理类,所以要实现该接口,加上@Component
注解,能让项目启动时就扫描并注入到Ioc容器里。
接下来启动相关工程后,eureka-server, feign工程后,访问http://localhost:8081/hello/?name=aaa,可以看到已经正确调用熔断器类的方法。
HystrixDashboard的介绍
众所周知,在互联网框架中使用Hystrix为了保证服务的实例可用性,即使出现故障也能即使反馈不去扩散并影响到其他服务调用,那Hystrix的运行状况就成了服务的可用性以及健壮性的指标,所以HystrixDashboard就是监控Hystrix的运行状况,并且还提供一个友好的可视化界面。
在Ribbon上开启HystrixDashboard
使用之前的Ribbon项目
添加pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
除了之前已经添加过的Hystrix启动依赖,还需要添加actuator以及HystrixDashboard依赖。
启动类的改造
需要在启动类上加上@EnableHystrixDashboard
上注解开启HystrixDashboard组件。
EnableHystrixDashboard
@EnableHystrix
@SpringBootApplication
public class RibbonApplication {
// 将访问hystrix.stream的servlet当成bean注入到ioc
@Bean
public ServletRegistrationBean hystrixMetricsStreamServlet() {
ServletRegistrationBean registration = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
registration.addUrlMappings("/hystrix.stream");
return registration;
}
public static void main(String[] args) {
SpringApplication.run(RibbonApplication.class, args);
}
}
application.properties的改造
spring.application.name=ribbon
server.port=8088
eureka.client.serviceUrl.defaultZone=http://localhost:1101/eureka/
ribbon.eureka.enabled=false
eureka-client.ribbon.listOfServers= http://localhost:2101,http://localhost:2102
# * 代表暴露所有endoints,也可以赋值为hystrix.steam只暴露hystrix的endpoints
management.endpoints.web.exposure.exclude=*
启动工程后访问http://localhost:8088/hystrix。
从页面直观的可以直到HystrixDashboard有三种监控方式:
- 默认的集群监控:通过URL http://turbine-hostname:port/turine.stream开启,实现对默认集群的监控。
- 指定的集群监控:通过URL http://turbine-hostname:port/turine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
- 单体应用的监控:通过URL http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。
前两者需要整合Turbine才能实现对集群的监控,最后一个是对单个服务实例的监控。
在输入框内填上需要监控的服务url地址:http://localhost:8088/hystrix.stream delay代表着轮询监控的间隔时间, tittle则为字面意思就是标题。
点击按钮进入后发现页面只显示load中,这时访问http://localhost:8088/hello/?name=aa进行正常的接口调用后。可观察到已经有监控数据显示。
关于其中各个字段的解释大家可以看下图(图并非笔者所做)
在feign项目上开启HystrixDashboard的方法和之前一样,可能有读者会好奇feign已经集成了hsytrix组件,为何还需要hystrix的依赖,这是因为feign自带的hystrix依赖并非是starter依赖。
使用Turbine进行集群监控
每个断路器服务都可以单独开启一个HystrixDashboard进行监控,但服务过多的话也难以处理。所以就需要Turbine进行集群监控。
搭建Turbine工程模块
pom依赖
新建一个模块,引入相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
引入eureka的client,Turbine以及actuator的依赖即可
application.properties的修改
server.port=8082
spring.application.name=turbine
# eureka注册中心
eureka.client.serviceUrl.defaultZone=http://localhost:1101/eureka/
# 集群配置
turbine.aggregator.cluster-config=default
# 监控的服务
turbine.app-config=ribbon
# 集群的名称,默认default
turbine.cluster-name-expression=new String("default")
主程序的改造
需要在主程序上添加@EnableTurbine
注解开启turbine功能。
@EnableTurbine
@SpringBootApplication
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
启动eureka-server, eureka-client, ribbon ,turbine模块。访问http://localhost:8088/hystrix,输入http://localhost:8082/turbine.stream,点击按钮进入页面,调用接口进行访问就可看到页面正常输出。
源码分析
Hystrix线程池隔离
分布式系统中,大量的用户请求都是以线程池进行处理,所以了解Hystrix线程的创建是必要的。
查看HystrixCommand的父类AbstractCommand,其构造方法中定义了许多实例化hystrix所需的对象。
protected AbstractCommand(HystrixCommandGroupKey group, HystrixCommandKey key, HystrixThreadPoolKey threadPoolKey, HystrixCircuitBreaker circuitBreaker, HystrixThreadPool threadPool, Setter commandPropertiesDefaults, com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults, HystrixCommandMetrics metrics, AbstractCommand.TryableSemaphore fallbackSemaphore, AbstractCommand.TryableSemaphore executionSemaphore, HystrixPropertiesStrategy propertiesStrategy, HystrixCommandExecutionHook executionHook) {
this.commandState = new AtomicReference(AbstractCommand.CommandState.NOT_STARTED);
this.threadState = new AtomicReference(AbstractCommand.ThreadState.NOT_USING_THREAD);
this.executionResult = ExecutionResult.EMPTY;
this.isResponseFromCache = false;
this.commandStartTimestamp = -1L;
this.isCommandTimedOut = new AtomicReference(AbstractCommand.TimedOutStatus.NOT_EXECUTED);
this.commandGroup = initGroupKey(group);
this.commandKey = initCommandKey(key, this.getClass());
this.properties = initCommandProperties(this.commandKey, propertiesStrategy, commandPropertiesDefaults);
this.threadPoolKey = initThreadPoolKey(threadPoolKey, this.commandGroup, (String)this.properties.executionIsolationThreadPoolKeyOverride().get());
this.metrics = initMetrics(metrics, this.commandGroup, this.threadPoolKey, this.commandKey, this.properties);
this.circuitBreaker = initCircuitBreaker((Boolean)this.properties.circuitBreakerEnabled().get(), circuitBreaker, this.commandGroup, this.commandKey, this.properties, this.metrics);
this.threadPool = initThreadPool(threadPool, this.threadPoolKey, threadPoolPropertiesDefaults);
this.eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
this.concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
HystrixMetricsPublisherFactory.createOrRetrievePublisherForCommand(this.commandKey, this.commandGroup, this.metrics, this.circuitBreaker, this.properties);
this.executionHook = initExecutionHook(executionHook);
this.requestCache = HystrixRequestCache.getInstance(this.commandKey, this.concurrencyStrategy);
this.currentRequestLog = initRequestLog((Boolean)this.properties.requestLogEnabled().get(), this.concurrencyStrategy);
this.fallbackSemaphoreOverride = fallbackSemaphore;
this.executionSemaphoreOverride = executionSemaphore;
}
这个类里还有许多具体初始化hystrix的相关属性对象的方法,读者可自行研究。
然后关注一下initThreadPool
这个方法,这是一个初始化线程池的方法。
private static HystrixThreadPool initThreadPool(HystrixThreadPool fromConstructor, HystrixThreadPoolKey threadPoolKey, com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults) {
return fromConstructor == null ? com.netflix.hystrix.HystrixThreadPool.Factory.getInstance(threadPoolKey, threadPoolPropertiesDefaults) : fromConstructor;
}
从代码中发现调用了com.netflix.hystrix.HystrixThreadPool.Factory.getInstance
方法,进入这个方法。
public static class Factory {
// 保存线程池对象
static final ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap();
public Factory() {
}
// 取得鲜橙汁对象
static HystrixThreadPool getInstance(HystrixThreadPoolKey threadPoolKey, Setter propertiesBuilder) {
String key = threadPoolKey.name();
HystrixThreadPool previouslyCached = (HystrixThreadPool)threadPools.get(key);
// 从concurrentHashMap中去判断是否有此线程池对象
if (previouslyCached != null) {
return previouslyCached;
} else {
Class var4 = HystrixThreadPool.class;
// 上锁,线程安全
synchronized(HystrixThreadPool.class) {
// 将线程对象添加到concurrentHashMap中
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPool.HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}
}
return (HystrixThreadPool)threadPools.get(key);
}
}
从上面的代码中可知线程池的构造最终会到HystrixThreadPool.Factory这个内部类。这个类定义一个ConcurrentHashMap用于缓存线程池对象,当传入的HystrixThreadPoolKey已经构造过了相应的ThreadPool,将会直接从ConcurrentHashMap里返回已经生成的ThreadPool。如果传入的HystrixThreadPoolKey没有相应的ThreadPool,将构造新的ThreadPool并放入到ConcurrentHashMap这个缓存对象上。
继续查看HystrixThreadPool.HystrixThreadPoolDefault
这个方法
public HystrixThreadPoolDefault(HystrixThreadPoolKey threadPoolKey, Setter propertiesDefaults) {
this.properties = HystrixPropertiesFactory.getThreadPoolProperties(threadPoolKey, propertiesDefaults);
HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
this.queueSize = (Integer)this.properties.maxQueueSize().get();
# 调用了构造线池的方法
this.metrics = HystrixThreadPoolMetrics.getInstance(threadPoolKey, concurrencyStrategy.getThreadPool(threadPoolKey, this.properties), this.properties);
this.threadPool = this.metrics.getThreadPool();
this.queue = this.threadPool.getQueue();
HystrixMetricsPublisherFactory.createOrRetrievePublisherForThreadPool(threadPoolKey, this.metrics, this.properties);
}
进入到concurrencyStrategy.getThreadPool
方法,发现线程池最终在这里进行创建。
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties threadPoolProperties) {
ThreadFactory threadFactory = getThreadFactory(threadPoolKey);
boolean allowMaximumSizeToDivergeFromCoreSize = (Boolean)threadPoolProperties.getAllowMaximumSizeToDivergeFromCoreSize().get();
int dynamicCoreSize = (Integer)threadPoolProperties.coreSize().get();
int keepAliveTime = (Integer)threadPoolProperties.keepAliveTimeMinutes().get();
int maxQueueSize = (Integer)threadPoolProperties.maxQueueSize().get();
BlockingQueue<Runnable> workQueue = this.getBlockingQueue(maxQueueSize);
if (allowMaximumSizeToDivergeFromCoreSize) {
int dynamicMaximumSize = (Integer)threadPoolProperties.maximumSize().get();
if (dynamicCoreSize > dynamicMaximumSize) {
logger.error("Hystrix ThreadPool configuration at startup for : " + threadPoolKey.name() + " is trying to set coreSize = " + dynamicCoreSize + " and maximumSize = " + dynamicMaximumSize + ". Maximum size will be set to " + dynamicCoreSize + ", the coreSize value, since it must be equal to or greater than the coreSize value");
return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, (long)keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
} else {
return new ThreadPoolExecutor(dynamicCoreSize, dynamicMaximumSize, (long)keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
}
} else {
return new ThreadPoolExecutor(dynamicCoreSize, dynamicCoreSize, (long)keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory);
}
}
综上所述,hystrix根据hystrixCommandGroupKey进行分组,每个group分组创建自己的线程池。
Hystrix熔断源码分析
继续回到AbstractCommand
这个类里,观察一下initCircuitBreaker
这个方法。
private static HystrixCircuitBreaker initCircuitBreaker(boolean enabled, HystrixCircuitBreaker fromConstructor, HystrixCommandGroupKey groupKey, HystrixCommandKey commandKey, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
// 判断是否打开熔断器
if (enabled) {
return fromConstructor == null ? com.netflix.hystrix.HystrixCircuitBreaker.Factory.getInstance(commandKey, groupKey, properties, metrics) : fromConstructor;
} else {
return new NoOpCircuitBreaker();
}
}
进入到com.netflix.hystrix.HystrixCircuitBreaker.Factory.getInstance
观察
public static class Factory {
// 维护一个ConcurrentHashMap保存熔断器
private static ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand = new ConcurrentHashMap();
public Factory() {
}
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key, HystrixCommandGroupKey group, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
HystrixCircuitBreaker previouslyCached = (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
// 判断ConcurrentHashMap中是否存有对应key的熔断器对象
if (previouslyCached != null) {
return previouslyCached;
} else {
// 实例化并添加到ConcurrentHashMap里
HystrixCircuitBreaker cbForCommand = (HystrixCircuitBreaker)circuitBreakersByCommand.putIfAbsent(key.name(), new HystrixCircuitBreaker.HystrixCircuitBreakerImpl(key, group, properties, metrics));
return cbForCommand == null ? (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name()) : cbForCommand;
}
}
public static HystrixCircuitBreaker getInstance(HystrixCommandKey key) {
return (HystrixCircuitBreaker)circuitBreakersByCommand.get(key.name());
}
static void reset() {
circuitBreakersByCommand.clear();
}
}
}
接下来观察HystrixCircuitBreaker的实现类HystrixCircuitBreakerImpl
public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
// 熔断器的属性常量类
private final HystrixCommandProperties properties;
// 熔断器的熔断指标类
private final HystrixCommandMetrics metrics;
// 定义一个原子类型的布尔值状态
private AtomicBoolean circuitOpen = new AtomicBoolean(false);
private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();
protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
this.properties = properties;
this.metrics = metrics;
}
// 重置统计数据
public void markSuccess() {
if (this.circuitOpen.get() && this.circuitOpen.compareAndSet(true, false)) {
this.metrics.resetStream();
}
}
// 是否放行请求
public boolean allowRequest() {
// 熔断器开启则不放行
if ((Boolean)this.properties.circuitBreakerForceOpen().get()) {
return false;
} else if ((Boolean)this.properties.circuitBreakerForceClosed().get()) {
this.isOpen();
return true;
} else {
return !this.isOpen() || this.allowSingleTest();
}
}
// 部分放行
public boolean allowSingleTest() {
long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();
return this.circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + (long)(Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get() && this.circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis());
}
// 判断熔断器是否开启
public boolean isOpen() {
if (this.circuitOpen.get()) {
return true;
} else {
// 根据采集到的数据进行判断是否开启
HealthCounts health = this.metrics.getHealthCounts();
if (health.getTotalRequests() < (long)(Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()) {
return false;
} else if (health.getErrorPercentage() < (Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()) {
return false;
} else if (this.circuitOpen.compareAndSet(false, true)) {
this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
return true;
} else {
return true;
}
}
}
}
总结
Hystrix提供了以下功能:
- 防止单个服务故障消耗所有的servlet容器的线程资源
- 快速失败机制,某个服务出现故障,快速失败而不是让线程等待
- 提供fallback回退方案,遇到服务故障,可以执行设定好的方案。
- 提供熔断机制,保证故障不会扩散到其他服务。
- 提供HystrixDashboard,可以监视Hystrix运行状况。