zoukankan      html  css  js  c++  java
  • 分布式服务弹性框架“Hystrix”实践与源码研究(一)

    文章初衷

    为了应对将来在线(特别是无线端)业务量的成倍增长,后端服务的分布式化程度需要不断提高,对于服务的延迟和容错管理将面临更大挑战,公司框架和开源团队选择内部推广Netflix的Hystrix,一是为了推进各部门的服务使用覆盖率,二是为了增加C Sharp语言版本的参与度(目前公司至少三成服务由.NET编写)。该博文属于个人对Hystrix研究和实践经验。

    什么是Hystrix?

    Hystrix是世界最大在线影片租赁服务商Netflix开源,针对分布式系统的延迟和容错库。该库由Java写成,项目源于Netflix API团队在2011年启动的弹性工程项目。项目在github上发布至今,已经有接近三千颗星,只有少数优秀的开源项目才能享受到千星级别的待遇,Hystrix成功可见一斑。

     为什么使用Hystrix?

    在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),如下图:

    在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等.

    如下图:QPS为50的依赖 I 出现不可用,但是其他依赖仍然可用.

    当依赖I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性.如下图:

    在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。

    例如:一个依赖30个SOA服务的系统,每个服务99.99%可用。
    99.99%的30次方 ≈ 99.7%
    0.3% 意味着一亿次请求 会有 3,000,00次失败
    换算成时间大约每月有2个小时服务不稳定.
    随着服务依赖数量的变多,服务不稳定的概率会成指数性提高.
    解决问题方案:对依赖做隔离,Hystrix就是处理依赖隔离的框架,同时也是可以帮我们做依赖服务的治理和监控.

    到底能做什么呢?

    1)Hystrix使用命令模式HystrixCommand(Command)包装依赖调用逻辑,每个命令在单独线程中/信号授权下执行

    2)提供熔断器组件,可以自动运行或手动调用,停止当前依赖一段时间(10秒),熔断器默认错误率阈值为50%,超过将自动运行。

    3)可配置依赖调用超时时间,超时时间一般设为比99.5%平均时间略高即可.当调用超时时,直接返回或执行fallback逻辑。

    4)为每个依赖提供一个小的线程池(或信号),如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。

    5)依赖调用结果分:成功,失败(抛出异常),超时,线程拒绝,短路。 请求失败(异常,拒绝,超时,短路)时执行fallback(降级)逻辑。

      

    6)提供近实时依赖的统计和监控

    7)支持异步执行。支持并发请求缓存。自动批处理失败请求。

    Hystrix设计理念

    想要知道如何使用,必须先明白其核心设计理念,Hystrix基于命令模式,通过UML图先直观的认识一下这一设计模式

    可见,Command是在Receiver和Invoker之间添加的中间层,Command实现了对Receiver的封装。那么Hystrix的应用场景如何与上图对应呢?

    API既可以是Invoker又可以是reciever,通过继承Hystrix核心类HystrixCommand来封装这些API(例如,远程接口调用,数据库查询之类可能会产生延时的操作)。就可以为API提供弹性保护了。

    Hello World

    Hello World的例子旨在展示,如何在项目中低侵入式的改造,使API置于Hystrix保护之下!

    引入maven依赖

    <!-- 依赖版本 -->
    <hystrix.version>1.3.16</hystrix.version>
    <hystrix-metrics-event-stream.version>1.1.2</hystrix-metrics-event-stream.version> 
     
    <dependency>
         <groupId>com.netflix.hystrix</groupId>
         <artifactId>hystrix-core</artifactId>
         <version>${hystrix.version}</version>
     </dependency>
         <dependency>
         <groupId>com.netflix.hystrix</groupId>
         <artifactId>hystrix-metrics-event-stream</artifactId>
         <version>${hystrix-metrics-event-stream.version}</version>
     </dependency>
    <!-- 仓库地址 -->
    <repository>
         <id>nexus</id>
         <name>local private nexus</name>
         <url>http://maven.oschina.net/content/groups/public/</url>
         <releases>
              <enabled>true</enabled>
         </releases>
         <snapshots>
              <enabled>false</enabled>
         </snapshots>
    </repository>

    下面是一个没有使用Hystrix保护的sayHello的服务以及它的调用

    //API调用,可能会产生延时
    public class HelloService {
         public static String sayHello(final String name)
         {
             return String.format("Hello %s!", name);
         }
     }
    //客户端直接调用API
    public class Client{
          public static void main(String[] args)
          {
               System.out.println(HelloService.sayHello("World"));     
          }  
    }    

    假设字符串生成过程是一个需要保护的操作,下面我们用Hystrix进行封装。

    需要注意的是,虽然使用命令模式,但是我们这里不建议覆盖execute方法,而是实现run的模版方法,多数框架的实现会采用template设计模式,并且将模版方法设置为protected签名,这样做的好处是,既可以将具体的业务交给业务实现者,又可以为之添加其他功能,而业务实现者只需要关注自己的业务就好了。比如这里HystrixCommand.execute方法实际上是调用了HystrixCommand.queue().get(),而queue方法除了最终调用run之外,还需要为run方法提供超时和异常等保护功能,外部也不能直接调用非安全的run方法,这一实践非常值得我们学习。

    OK,现在我们通过实现run方法来包装sayHello功能,我们通过一个私有域_name,通过构造函数来传递消息,获取构造参数的拷贝来保持不变性。

    public class SayHelloCommand extends HystrixCommand<String> {
        private final String _name;
        public SayHelloCommand(String name)
        {
            super(HystrixCommandGroupKey.Factory.asKey("HelloService"));
            _name = new String(name);//unmutable
            
        }
        @Override
        protected String run() {
    
            return String.format("Hello %s!", _name);
        }
    }

    API改造如下,作为门面方法最好不要改动函数的签名(除非参数和返回类型有变动,这是因为客户端代码的改动代价往往是巨大的),同时提供版本sayHelloAsync,该方法提供了异步功能

    public class HelloService {
    //    public static String sayHello(final String name)
    //    {
    //        return "Hello " + name + "!";
    //    }
        
        /**
         * sayHello under protection of Hystrix
         * @param name
         * @return <code>"Hello " + name + "!"</code> 
         */
        public static String sayHello(final String name)
        {
            return new SayHelloCommand(name).execute();
        }
        
        /**
         * call async
         * @param name
         * @return
         */
        public static Future<String> sayHelloAsync(final String name)
        {
            return new SayHelloCommand(name).queue();
        }
    }

     接下来我们来看看,如何在超时熔断情况下使用FallBack策略,这点在项目中是相当有用的,比如超时后访问数据备库,或者直接返回重试响应

    首先SayHelloCommand构造函数使用Hystrix的Setter来设置超时时间,这里解释下Setter这个类涉及到的几个最佳实践

    1.Setter使用builder模式,想想构造函数有很多参数要设置,作为构造参数传递会大大降低可阅读性,用静态工厂方法一个个设置又可能造成多线程并发下的不一致性,而且这种bug往往非常难以定位,所以builder模式是非常好的实践。将Setter作为构造器传给HystrixCommand的构造函数,Setter中又很多静态方法,可以通过方法名明确的知道元素的意义。

    2.Setter是HystrixCommand内部静态类,Hystrix代码大量的使用了内部静态类,来作为该类的工厂方法,或者构造器,我觉得这样划分使代码职责更加清晰,比单独的工厂类更易于维护。

    3.Setter使用函数式串联,每个静态工厂方法返回Setter实例,这样我们可以把构造过程串联起来,使代码更加易于阅读。

    4.Setter是不可变类,每个静态工厂方法返回一个新的Setter拷贝,所以Setter是线程安全的。

    OK,Setter介绍到这里,这里我们继续设置超时时间为500

    public SayHelloCommand(final String name)
        {
            //builder for HystrixCommand
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloServiceGroup"))
                    .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withTimeoutInMilliseconds(500)));
            _name = new String(name);
        }

    run方法我们使用Thread.sleep(600)来特意达到超时的效果,同时实现getFallback方法,程序超时后会立即运行FallBack

        @Override
        protected String getFallback() {
            return String.format("[FallBack]Hello %s!", _name);
        }
    
        @Override
        protected String run() throws Exception {
            //TimeOut
            Thread.sleep(600);
            return String.format("Hello %s!", _name);
        }

    最终输出:

    [FallBack]Hello World!

    回顾重点

    1.Hystrix可以为分布式服务提供弹性保护

    2.Hystrix通过命令模式封装调用,来实现弹性保护,继承HystrixCommand并且实现run方法,就完成了最简单的封装。

    3. 实现getFallBack方法可以为熔断或者异常提供后备处理方法。

    4.HystrixCommand中Setter类的最佳实践。

    5.模版方法在框架中的实践。

  • 相关阅读:
    POJ 2723 Get Luffy Out(2-SAT)
    ZOJ 3613 Wormhole Transport
    HDU 4085 Peach Blossom Spring
    NBUT 1221 Intermediary
    NBUT 1223 Friends number
    NBUT 1220 SPY
    NBUT 1218 You are my brother
    PAT 1131. Subway Map (30)
    ZSTU OJ 4273 玩具
    ZSTU OJ 4272 最佳淘汰算法
  • 原文地址:https://www.cnblogs.com/zwjing/p/4269023.html
Copyright © 2011-2022 走看看