Hystrix实现分布式系统中的故障容错

Hystrix是什么

分布式服务系统通常会通过HTTP或RPC方式调用所依赖的服务,例如支付服务通过HTTP或RPC调用银行卡服务。在高并发请求的情景下,依赖的服务可能会出现服务异常、网络连接缓慢、资源繁忙、暂时不可用、服务脱机等情况,这些异常情况将会严重影响整个线上系统的稳定性和可用性,最糟糕的情况是产生服务雪崩效应。复杂的分布式服务系统往往会依赖更多的其它服务,在高并发的情况下,如果没有做好隔离措施,这些依赖将会拖垮整个服务调用者。Hystrix是Netflix的一个帮助解决分布式服务系统交互时超时处理和容错的类库,它具有降级和熔断的保护能力,可以优雅的解决上述问题。

Hystrix能做什么

Hystrix提供了如下功能特性:
1.Hystrix把服务调用统称为依赖调用,Hystrix通过使用命令模式将依赖调用逻辑封装在HystrixCommand中,每一次的依赖调用将在Hystrix的单独线程池(或信号)中执行;
2.可根据业务需要配置依赖分组名、线程池,使不同分组的依赖可以在不同的线程池中执行,隔离不同依赖调用的资源;
3.可配置依赖调用超时时间(一般配置为比99.5%平均调用时间略高),当依赖调用超时时直接返回或执行getFallback方法;
4.依赖调用异常、超时、短路时将执行getFallback方法;
5.提供了熔断器(CircuitBreaker)机制,可根据设定的条件(如调用失败率大于50%)判断依赖调用是否可以继续被调用,如果某个依赖调用的错误百分比超过阈值,则通过手动或自动地中断一个熔断器,一段时间内依赖调用无法被执行;
6.提供了对依赖调用的实时统计和监控。

Hystrix如何使用

1.添加Hystrix依赖

<dependency> 
     <groupId>com.netflix.hystrix</groupId> 
     <artifactId>hystrix-core</artifactId> 
     <version>1.5.12</version> 
</dependency>

2.使用HystrixCommand封装依赖调用
通过继承HystrixCommand封装依赖调用,示例代码如下:

public class CommandHelloWorld extends HystrixCommand<String> {

    private final String name;

    public CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"));
        this.name = name;
    }

    @Override
    protected String run() throws Exception {
        // 在这里调用依赖
        Thread.sleep(500L);
        return "Hello " + name + "--" + Thread.currentThread().getId();
    }

    // 超时、异常后执行该方法
    @Override
    protected String getFallback() {
        return "fallback";
    }
}

run方法:run方法体中进行依赖调用
getFallback方法:当run方法中依赖调用超时、异常(除了HystrixBadRequestException)时会执行getFallback方法快速返回;当run方法中的依赖调用在设置的时间内超时、异常(除了HystrixBadRequestException)的频率超过阈值,后续对这个依赖的调用将直接执行getFallback方法,待冷却一段时间后,对这个依赖的调用会重新进入run方法执行。

3.执行封装的依赖调用
3.1同步执行
调用execute方法即为同步执行,当前线程将一直阻塞,直到获取结果,示例代码如下:

    @Test
    public void testSynchronous() {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack");
        System.out.print(commandHelloWorld.execute() + "--" + Thread.currentThread().getId());
    }

输出结果如下:

    Hello jack--16--1

从输出结果可以看到依赖调用线程和主线程不是同一个,实现了线程隔离。

HystrixCommand默认的调用超时时间是1000毫秒,如果将上述run方法中的线程休眠时间改成1100毫秒,再次运行testSynchronous单元测试,将得到如下结果:

    fallback--1

可以看到在依赖调用时间超过设置的默认超时时间时,将执行getFallback方法快速返回,实现优雅降级,其过程如下图所示。

getFallback.png

3.2异步执行
调用queue方法即为异步执行,不阻塞当前线程,返回一个Future对象,示例代码如下:

    @Test
    public void testAsynchronous() throws Exception {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack");
        Future<String> future = commandHelloWorld.queue();
        System.out.println(future.get() + "--" + Thread.currentThread().getId());
    }

输出结果如下:

    Hello jack--16--1

queue().get()等同于同步调用execute()

3.3热注册观察者执行
调用observe方法即为热注册观察者执行,返回一个Observable对象,当run方法执行完成后,进入观察者订阅的事件中,示例代码如下:

    @Test
    public void testHotObservable() throws Exception {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack");
        Observable<String> ho = commandHelloWorld.observe();
        //订阅结果回调事件
        ho.subscribe(new Action1<String>() {
            public void call(String result) {
                //result为run方法执行返回的结果
                System.out.println(result + "--" + Thread.currentThread().getId());
            }
        });
        Thread.sleep(1000);

        //订阅一个完整的回调事件
        ho.subscribe(new Subscriber<String>() {
            //在onNext执行后执行
            public void onCompleted() {
                System.out.println("oonCompleted ");
            }

            //在run/onNext方法执行异常后执行
            public void onError(Throwable throwable) {

            }

            //在run方法返回结果后执行
            public void onNext(String s) {
                System.out.println("onNext: " + s );
            }
        });
    }

输出结果如下:

    Hello jack--16--16
    onNext: Hello jack--16
    oonCompleted

3.4冷注册观察者执行
调用toObservable方法即为冷注册观察者执行,同样返回Observable对象,但它是在注册的时即执行run方法,示例代码如下:

    @Test
    public void testColdObservable() throws Exception {
        CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack");
        Observable<String> ho = commandHelloWorld.toObservable();
        ho.subscribe(new Action1<String>() {
            public void call(String s) {
                System.out.println(s + "--" + Thread.currentThread().getId());
            }
        });
        Thread.sleep(1000);
    }

输出结果如下:

    Hello jack--16--16

前面三种调用方式,最终都是依赖toObservable方式,这其中的转换如下图所示:

hystrix-return-flow.png

属性配置

查看HystrixCommand源码,可以发现一个常用的构造方法HystrixCommand(HystrixCommand.Setter setter),使用方法如下:

        HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("HelloWorld");
        HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("hello");
        HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("hello");
        HystrixCommand.Setter setter = HystrixCommand.Setter
                .withGroupKey(groupKey)
                .andCommandKey(commandKey)
                .andThreadPoolKey(threadPoolKey);

        HystrixCommand<String> helloCommand = new HystrixCommand<String>(setter) {

            protected String run() throws Exception {
                //依赖调用
                return "run";
            }

            @Override
            protected String getFallback() {
                //fail back
                return super.getFallback();
            }
        };

这个HystrixCommand.Setter中包含了如下属性:

        protected final HystrixCommandGroupKey groupKey;
        protected HystrixCommandKey commandKey;
        protected HystrixThreadPoolKey threadPoolKey;
        protected com.netflix.hystrix.HystrixCommandProperties.Setter commandPropertiesDefaults;
        protected com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults;

1 HystrixCommandKey
Hystrix使用单例模式存储HystrixCommand,熔断机制就是根据单实例上的调用情况统计实现的,所以每个HystrixCommand要有自己的名字,用于区分,同时用于依赖调用的隔离。HystrixCommandKey就是用于定义这个名字,如果没有定义这个名字,Hystrix会使用其类名作为其名字,可以使用HystrixCommandKey.Factory.asKey(String name)方法定义一个名称。

2 HystrixThreadPoolKey
HystrixThreadPoolKey是HystrixCommand所在的线程池,如果该参数不设置则使用HystrixCommandGroupKey作为HystrixThreadPoolKey,这种情况下同一个HystrixCommandGroupKey下的依赖调用共用同一个线程池内,如果不想共用同一个线程池,则需要设置该参数。可以使用HystrixThreadPoolKey.Factory.asKey(String name)方法设置。

3 HystrixCommandGroupKey
Hystrix需要对HystrixCommand进行分组,便于统计、管理,所以需要一个分组名称,HystrixCommandGroupKey就是用于定义分组名称,可以使用HystrixCommandGroupKey.Factory.asKey(String name)方法定义一个分组名。每个HystrixCommand必须要配置一个分组名,一个是用于分组,还有如果没有配置HystrixThreadPoolKey,这个分组名将会用于线程池名。

4 HystrixThreadPoolProperties
从名称上可以看出这是线程池的属性配置,可以通过它设置核心线程数大小、最大线程数、任务队列大小等,当然它也又一些默认的配置参数。

5 HystrixCommandProperties
这个就是HystrixCommand的属性配置,它可以设置熔断器是否可用、熔断器熔断的错误百分比、依赖调用超时时间等,它有一些默认的配置参数,如熔断器熔断的错误百分比默认值是50%、依赖调用超时时间默认值是1000毫秒。

隔离方式

Hystrix支持线程隔离和信号量隔离:

线程隔离

不同的依赖调用分配到不同的线程池中执行,使用线程对依赖调用进行隔离,上述的示例代码就是使用线程隔离。优点是隔离性能好,可设置短路机制(依赖调用失败后执行getFallback()或依赖调用熔断后,一段时间内对该依赖的调用将直接返回失败),缺点是涉及到线程切换的性能损耗,但是官方给出的结果是性能损耗是可以接受的。

信号量隔离

信号量隔离可实现对依赖调用最高并发请求数的限制,每次依赖调用都会先判断信号量是否达到阈值,如果达到极限值则拒绝调用,优点是不用新启线程,缺点是每次都需要获取信号量,使用方式如下:

    HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("HelloWorld");
        HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("hello");
        HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("hello");
        //配置信号量隔离
        HystrixCommandProperties.Setter commandPropertiesSetter = HystrixCommandProperties.Setter().withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE);
        HystrixCommand.Setter setter = HystrixCommand.Setter
                .withGroupKey(groupKey)
                .andCommandKey(commandKey)
                .andThreadPoolKey(threadPoolKey)
                .andCommandPropertiesDefaults(commandPropertiesSetter);

Hystrix工作过程

工程过程如下图所示:

hystrix-command-flow-chart.png

步骤描述如下:
1.使用HystrixCommand或HystrixObservableCommand封装一个依赖调用;
2.执行封装的依赖调用;
3.判断本次调用是否可以从缓存中取结果,如果可以,直接返回缓存的结果;如果不可以进入第4步判断;
4.判断熔断器是否打开,如果打开则进入第8步;如果没打开则进入第5步;
5.判断信号量或线程池是否已满,如果已满则进入第8步;如果没满则进入第6步;
6.执行依赖调用,调用失败或超时进入第8步;调用成功返回结果;
7.根据依赖调用成功、失败或超时计算熔断值;
8.getFallback()执行失败或没实现getFallback()方法,将抛出异常;getFallback()执行成功返回fallback值。

Hystrix熔断保护机制

Hystrix熔断保护就像电路中的熔断器一样,在电压过高时,保险丝会熔断,防止火灾,做到用电安全。熔断保护机制的工作过程如下图所示:

circuit-breaker-1280.png

熔断器工作过程如下:
1.假设大量的请求数量超过了HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()的阈值,并且依赖调用失败的百分比超过了HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()的阈值,熔断器将会从关闭状态变成打开状态;
2.在熔断器处于打开状态的期间,所有对这个依赖进行的调用都会短路,即不进行真正的依赖调用,返回失败;
3.在等待(冷却)的时间超过HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()的值后,熔断器将处于半开的状态,将允许单个请求去调用依赖,如果这次的依赖调用还是失败,熔断器状态将再次变成打开,这个打开状态持续时间是HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()配置的值;如果这次的依赖调用成功,熔断器状态将变成关闭,后续依赖调用可正常执行。

依赖调用监控

Hystrix提供了Hystrix Dashboard功能,可以实时监控依赖的调用情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容