spring cloud Hystrix的运用与源码分析

简介

复杂的分布式系统,存在几十或者上百个服务互相依赖,如果由于某些原因导致其中部分服务无法使用,则导致访问这些服务的请求将会阻塞,并且会影响和其有依赖的服务,严重的导致整个系统瘫痪。Hystrix的出现就可以解决这种问题,对不可用的服务的熔断以及故障处理方案保证即使当前服务不可用,也不影响和其具有依赖的服务,并且访问该服务的请求也不会阻塞在服务上。

histyix
如图,一个用户请求服务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运行状况。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值