Hystrx权威指南--Hystrix是什么

来源:互联网 发布:linux curl 请求url 编辑:程序博客网 时间:2024/06/09 13:56
 随着互联网发展,传统的大型机单块应用到后来的分布式计算及发展到现在的微服务,Docker的发展更是让微服务如虎添翼。微服务的优点不必细说,网上一搜一大把,这里就聊一下微服务带来的问题和如何去解决这些问题。通过这些问题分析下Hystrix是如何成为设计微服务利器的。
          微服务带来的问题:
          一、增加服务依赖复杂度:单块应用拆分成微服务之后,原来运行在同一个JVM进程内的功能,可能要分到多个JVM进程中,拆分后的微服务之间通过RPC调用来共同完成之前的功能,这就必然会增加服务依赖的复杂度;

          二、不能FailFast : 原来系统是在同一个JVM进程内的,不存在网络调用,异常就会直接失败,不会导致处理hang住。微服务之后,逻辑功能则是由网络上的另一个服务提供的,如果提供者处理速度很慢,甚至最后会失败(超时),服务的请求端在返回失败之前就会一直hang在这里;另外在一段时间内90%的请求都返回失败,第91个请求就不需要再发送给服务端以增加服务端的压力了,直接fast fail,走后续流程即可。一段时间之后,再次尝试请求。

          三、不能平均分配资源:微服务之后,服务的依赖会增多,每个依赖的服务端性能和吞吐量都不尽相同,这时就要隔离每个依赖所占有的资源。比如某个机器P1有三个服务SA、SB、SC,分别依赖于A、B、C三个依赖,一段时间之后,A依赖的服务端响应速度开始变慢,这时S1所在机器的CPU、线程、IO等资源都会耗尽到A依赖上,而没有空闲CPU、线程、IO等资源去处理B、C的依赖。以至于S1不仅不能对外正常提供SA服务,连SB、SC服务都会受到影响,最后甚至会导致机器P1宕机。

以上提到的这三点主要问题也是Hystrix提供的核心功能:监控、熔断器、隔离。

          Hystrix简介
          Hystrix天生就是为微服务提供的,是由Netflix公司开发,目前为Netflix公司每天处理着数百亿的线程隔离和更多的信号隔离请求,代码托管在Github(https://github.com/Netflix/Hystrix)上。Hystrix的监控可以随时查看一个微服务运行的健康情况,内部线程、信号占用率;熔断器则可以实现快速失败,同时还能保护后端服务;隔离机制更是为微服务的高效运行保驾护航,当某一个依赖出现异常时能够及时进行降级保护。

隔离
          上面提到计算及的CPU、线程、IO等资源都是非常宝贵而且是有限的,当被使用达到上限时,那么距离该机器宕机也就不远了。另外一个容器对外提供的服务会有多个,可能每个服务都有一个依赖,当某一个依赖运行出现异常时,只能使该依赖对应的那个服务对外不可用,而其他的服务都应该是正常运行的。
          而现实中恰恰是当某一个服务不可用时,紧接着第二个第三个服务都会出现问题。这就是没有对依赖进行隔离的原因。因为有一个依赖运行异常,该机器上的所有CPU、线程等资源都过来处理这个依赖资源的请求了,没有空闲的资源去处理其他服务的请求。这时就需要对每个依赖进行资源隔离当某个依赖出现问题,只会有分配给该依赖的线程、IO等处于忙碌状态,其他线程、IO等资源应该正常处理其他的依赖
          Hystrix针对依赖资源的隔离提供了两种策略,分别是:线程池隔离和信号隔离。


为什么用Hystrix       
          在一个复杂的分布式架构系统中,依赖服务是避免不了的,特别是在一个微服务化的时代。如果一个系统不能对相关的依赖进行隔离,那该系统就会有被拖垮被宕机的风险。在一个高并发高访问量的系统中,当一个被依赖的服务出现网络连接缓慢、处理延迟等情况,在几分钟甚至几秒内,所有的线程都会block在被依赖的资源上,进而导致所有资源被耗尽,系统被拖垮。
     所有依赖服务都正常时:
     当其中一个依赖出现延迟时,服务器的大部分线程都会block在依赖I上,在一个高流量的系统中,几秒内所有的线程资源都被blocking在了依赖上,从而导致系统的其他服务也不可用,甚至整个系统被拖垮,不可用。
  Hystrix对依赖进行隔离和控制,通常这些依赖都是网络调用。Hystrix通过线程池和信号进行隔离,给每一个依赖分配指定的资源,当该依赖的资源不足(线程池爆满、信号tryAcquire) 就会直接返回失败,不会占用其他依赖的资源。
   同时,Hystrix还提供了CircuitBreaker机制,当失败率达到某一个阀值时,Hystrix会Fast Fail 并迅速恢复,或者优雅的降级。

环境搭建    
       使用Maven搭建的项目,在pom.xml文件中引入hystrix-core坐标:
      <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-core</artifactId>
            <version>1.4.18</version>
        </dependency>  
      环境搭建好之后就可以开发了,接下来会写如何开发Hystrix应用。

Hystrix开发
       首先还从HelloWorld 开始,首先定义一个Command类,继承HystrixCommand类,在构造函数中设置相关参数。重写run() 和 getFallback() ,业务逻辑实现放在run方法里。所有非HystrixBadRequestException异常都会调用getFallback方法,降级方案一般在getFallback方法中实现。
    class HelloWorldCommand extends HystrixCommand<String> {

        protected HelloWorldCommand(String name) {
            super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(name)));
        }

        @Override
        protected String run() throws Exception {
            System.out.println("run success " + Thread.currentThread().getName());
            return "run success " + Thread.currentThread().getName();
        }
    }
        很简单,在run方法里面返回了当前的线程名字。执行下 TestCase  ,观察打印数据
@Test
    public void helloWorldCommand() {
        HelloWorldCommand helloWorldCommand = new HelloWorldCommand("klov");
        String result = helloWorldCommand.execute();
        System.out.println("【HelloWorldCommand】 result = "+result);
    }
    
       打印数据如下:
     run success hystrix-klov-1
    【HelloWorldCommand】 result = run success hystrix-klov-1
     由输出数据可知线程池的名字默认是 hystrix+commandGroupKey+inde。

     到此,Hystrix的开发环境以及基本的开发已经完成,后面会继续介绍Hystrix两个隔离方式的开发及原理。

  线程池隔离
          使用线程会在以下三个场景带来性能消耗:
           1、线程的创建和销毁;
           2、线程上下文空间的切换,线程池调度需要操作系统介入,系统需要从用户态空间切换到内核态空间,调度完成后又需要切回到用户态空间。
          Hystrix通过使用固定线程池大小的方式解决了第一个问题,在创建线程池时,Hystrix只允许你设置CoreSize,而不允许你设置MaxSize。在系统初始化线程池时,MaxSize等于CoreSize。这样就避免了线程的频繁创建和销毁带来的性能消耗。Hystrix的线程池创建源码是在HystrixConcurrencyStrategy类getThreadPool方法中实现的:
public ThreadPoolExecutor getThreadPool(final HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {        return new ThreadPoolExecutor(corePoolSize.get(), maximumPoolSize.get(), keepAliveTime.get(), unit, workQueue, new ThreadFactory() {            protected final AtomicInteger threadNumber = new AtomicInteger(0);            @Override            public Thread newThread(Runnable r) {                Thread thread = new Thread(r, "hystrix-" + threadPoolKey.name() + "-" + threadNumber.incrementAndGet());                thread.setDaemon(true);                return thread;            }        });    }
跟普通的创建线程池没什么区别,根据传进来的线程池参数创建一个线程池。但是玄机出现在调用的地方,传递的corePoolSize和maximumPoolSize是同一个值。调用方是HystrixThreadPool类的173行

public HystrixThreadPoolDefault(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties.Setter propertiesDefaults) {            this.properties = HystrixPropertiesFactory.getThreadPoolProperties(threadPoolKey, propertiesDefaults);            HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();            this.queueSize = properties.maxQueueSize().get();            this.queue = concurrencyStrategy.getBlockingQueue(queueSize);            this.metrics = HystrixThreadPoolMetrics.getInstance(                    threadPoolKey,                    //这里是调用线程池创建的地方,corePoolSize和maximumPoolSize传递的都是coreSize()这个值                    concurrencyStrategy.getThreadPool(threadPoolKey,properties.coreSize(), properties.coreSize(), properties.keepAliveTimeMinutes(), TimeUnit.MINUTES, queue),                    properties);            this.threadPool = metrics.getThreadPool();            /* strategy: HystrixMetricsPublisherThreadPool */            HystrixMetricsPublisherFactory.createOrRetrievePublisherForThreadPool(threadPoolKey, this.metrics, this.properties);        }

  信号量隔离
          为解决线程池隔离带来的第二性能开销的场景,Hystrix使用了信号量隔离。信号量隔离通过原子操作类AtoInteger实现Permits的管理,AtoInteger类使用的是CAS操作,相对于锁,CAS是通过硬件实现的原子操作,减小了性能开销。下面是获取Permit的源码分析:
public boolean tryAcquire() {          //count是AtoInteger类型的成员变量            int currentCount = count.incrementAndGet();            if (currentCount > numberOfPermits.get()) {                count.decrementAndGet();                return false;            } else {                return true;            }        } tryAcquire获取Permit成功,调用方要在finally中释放Permit,调用release方法。 public void release() {          //对Permit进行减一操作            count.decrementAndGet();        }       

  建议不同的业务之间通过线程池隔离,同一个业务不同的依赖资源则可以通过信号量隔离,以提高吞吐量和性能

熔断器
          熔断器的功能等同于家庭电路中的自动跳闸器,当电路中流经的电流负荷过高或者有漏电等非安全用电情况时,跳闸器就会自动跳闸,起到对家用电器保护的作用。软件系统中的熔断器和跳闸器是类似的,当请求回路中发生异常时,熔断器打开。只是Hystrix会在一段时间后试着关闭回路,让部分请求发送成功,以检测异常是否恢复。
  熔断器机制是指,在后端服务可用率降低到阀值以下时,新来的请求不再发给后端服务,直接返回请求失败即可,以实现 Fail-Fast机制,快速给用户请求,执行后续的失败流程。这样既可以拦截不必要的请求,减少对后端本来就异常的服务的压力,还可以实现Fail-Fast机制。 Hystrix默认熔断器机制是开启的,可以在HystrixCommand的run方法中设置为开启,设置代码如下:
HystrixCommandProperties.Setter().withCircuitBreakerEnabled(true)

开启熔断器机制后,Hystrix默认的阀值是50%。如果在10秒内请求失败率达到50%及以上,Hystrix会自动断开回路,后面的请求不会再被发往后端的服务器中,会直接返回给客户端。5秒之后,Hystrix会试着关闭回路,放一部分请求过去,以检测异常是否恢复。

HystrixCircuitBreakerImpl类的allowSingleTest方法中实现该功能,源码如下:

public boolean allowSingleTest() {            long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();            // 1) if the circuit is open            // 2) and it's been longer than 'sleepWindow' since we opened the circuit            if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested +properties.circuitBreakerSleepWindowInMilliseconds().get()) {                // We push the 'circuitOpenedTime' ahead by 'sleepWindow' since we have allowed one request to try.                // If it succeeds the circuit will be closed, otherwise another singleTest will be allowed at the end of the 'sleepWindow'.                if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {                    // if this returns true that means we set the time so we'll return true to allow the singleTest                    // if it returned false it means another thread raced us and allowed the singleTest before we did                    return true;                }            }            return false;        }
properties.circuitBreakerSleepWindowInMilliseconds().get()取的值就是设置的间隔时间,默认是5秒。在每次打开熔断器时都要保存当前时间,在下次决策是否需要关闭回路时,判断距离上次开启时间是否达到设置的值。在Hystrix中大量使用了CAS在保证成员变量的原子操作前提下,又提高了性能。

原创粉丝点击