为第三方HTTP协议依赖增加Hystrix保护你的系统

前言

后端开发的很多同学应该都有过调用第三方HTTP协议服务的经验。

比如调用百度查询经纬度编码信息、调用豆瓣查询时下热门电影、调用7牛云存储接口上传文件等;以及公司内部其他小组开放的服务接口。

常见开发模式是我们按照服务提供方定义的接口文档进行接口调用开发。

在java中常见的HTTP客户端库有:

个人建议
Java原生HttpURLConnection 功能简陋,不推荐使用
Apache HttpClient 老牌框架稳定可靠,怀旧党可考虑
OkHttpRetrofit 新兴势力,发展迅猛;支付Android,支持HTTP2
Spring RestTemplate 可替换底层实现,Spring生态内简单的HTTP协议调用推荐使用
OpenFeign 可替换底层实现;源于Retrofit灵感支持注解驱动;支持Ribbon负载均衡、支持Java 11 Http2、支持Hystrix断路保护... 强烈推荐

你的第三方依赖挂了怎么办?

系统并发很高的情况下,我们依赖的第三方服务挂了,调用HTTP协议接口超时线程阻塞一直得不到释放,系统的线程资源很快被耗尽,导致整个系统不可用。

试想一下如果业务系统中我们依赖的第三方服务只是一个增强型的功能没有的化也不影响主体业务的运行或者只是影响一部分服务,如果导致系统整体不可用这是绝对不允许的。

有什么办法可以解决这个问题呢?

我们可以使用代理模式增加服务调用的监控统计,在发现问题的时候直接进行方法返回从而避免产生雪崩效应。

伪代码如下

public interface ApiService {

    /**
     * 获取token
     *
     * @param username
     * @param password
     * @return
     */
    String getToken(String username, String password);
    
}

public static <S> S getSafeApiService(Class<S> serviceClass) {

    S instance = ApiServiceFactory.createService(serviceClass);
    
    return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass},
                (proxy, method, args) -> {
                    
                    // 代理接口发现服务不可用时直接返回不执行下面的invoke方法
                    if (failStatus) {
                        log.error("error info")
                        return null;
                    } else {
                        // 执行具体的调用
                        Object result = method.invoke(instance, args);
                        return result;
                    }
                    
                });
}

总结就是我们需要包裹请求,对请求做隔离。那么业内有没有此类功能成熟的框架呢?

答案是肯定的,Netflix这家公司开源的微服务组件中Hystrix就有对于服务隔离、降级和熔断的处理。

如何使用Hystrix

下面以调用百度地图地理位置反编码接口来演示Hystrix的使用

项目依赖

使用Spring Initializr初始化SpringBoot工程,在pom文件中新增如下依赖Retrofit和Hystrix依赖

    <properties>
        <java.version>1.8</java.version>
        <hytrix.version>1.5.18</hytrix.version>
        <retrofit.version>2.3.0</retrofit.version>
        <slf4j.version>1.7.7</slf4j.version>
        <logback.version>1.1.2</logback.version>
        <lombok.version>1.16.14</lombok.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--Retrofit 客户端框架-->
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>retrofit</artifactId>
            <version>${retrofit.version}</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>converter-gson</artifactId>
            <version>${retrofit.version}</version>
        </dependency>

        <!--Hystrix 断路器框架-->
        <dependency>
            <groupId>com.netflix.hystrix</groupId>
            <artifactId>hystrix-core</artifactId>
            <version>${hytrix.version}</version>
        </dependency>

        其他依赖...
    </dependencies>

创建HTTP接口的调用

HTTP客户端这里选择Retrofit,相关文档可查看 https://square.github.io/retrofit/

  1. Retrofit将百度的HTTP API转换为Java接口
public interface BaiduMapApi {

    @GET("reverse_geocoding/v3/")
    Call<AddressBean> decode(@Query("ak") String ak,
                                       @Query("output") String output,
                                       @Query("coordtype") String coordtype,
                                       @Query("location") String location);

}

  1. 使用Retrofit类生成的实现BaiduMapApi接口
@SpringBootApplication
public class HystrixDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixDemoApplication.class, args);
    }


    @Bean
    public BaiduMapApi baiduMapApi() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://api.map.baidu.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        return retrofit.create(BaiduMapApi.class);
    }

}

  1. 创建完BaiduMapApi的实现后就可以直接使用这个接口了
@Slf4j
@SpringBootTest
class BaiduMapApiTest {

    @Autowired
    private BaiduMapApi baiduMapApi;

    @Test
    void decode() throws IOException {

        AddressBean addressBean = baiduMapApi.decode(
                "v1Xba4zeGLr6CScN39OFgvhiADPaXezd",
                "json",
                "wgs84ll",
                "31.225696563611,121.49884033194").execute().body();
        if (addressBean != null) {
            log.info(addressBean.toString());
        }
    }
}

执行单元测试后显示


单元测试执行结果

表明接口实现成功

为HTTP调用增加Hystrix保护

Hystrix官方示例:
https://github.com/Netflix/Hystrix/wiki/How-To-Use

Hello World!
Code to be isolated is wrapped inside the run() method of a HystrixCommand similar to the following:

public class CommandHelloWorld extends HystrixCommand<String> {

    private final String name;

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

    @Override
    protected String run() {
        return "Hello " + name + "!";
    }
}

This command could be used like this:

String s = new CommandHelloWorld("Bob").execute();
Future<String> s = new CommandHelloWorld("Bob").queue();
Observable<String> s = new CommandHelloWorld("Bob").observe();
  1. 新增HystrixCommand实现
  2. 在run方法中执行具体的方法
  3. 通过调用HystrixCommand对象的execute或者queue或者observe方法触发命令执行

参考官方示例按照前面的分析对调用做隔离保护需要使用代理模式包裹请求,我们包装一下对百度接口的调用

    @Autowired
    private BaiduMapApi baiduMapApi;

    @GetMapping("/decode")
    public AddressBean test(Double lon, Double lat) {
        // 使用HystrixCommand包裹请求
        return HystrixCommandUtil.execute(
                "BaiduMapApi",
                "decode",
                baiduMapApi.decode("v1Xba4zeGLr6CScN39OFgvhiADPaXezd",
                        "json",
                        "wgs84ll",
                        lat + "," + lon)
                , throwable -> {
                    log.error("触发出错返回,告警!", throwable);
                    return null;
                });


    }
@Slf4j
public class HystrixCommandUtil {
    
    /**
     * 客户端参数异常时将抛出HystrixBadRequestException
     *
     * @param groupKey
     * @param commandKey
     * @param call
     * @param fallback
     * @param <T>
     * @return
     * @throws HystrixBadRequestException
     */
    public static <T> T execute(String groupKey, String commandKey, Call<T> call, HystrixFallback<T> fallback) throws HystrixBadRequestException {
        if (groupKey == null) {
            throw new IllegalArgumentException("groupKey 不能为空");
        }
        if (commandKey == null) {
            throw new IllegalArgumentException("CommandKey 不能为空");
        }
        if (call == null) {
            throw new IllegalArgumentException("call 不能为空");
        }
        if (fallback == null) {
            throw new IllegalArgumentException("fallback 不能为空");
        }

        return new HystrixCommand<T>(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey))
            .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey))) {

            @Override
            protected T run() throws Exception {
                Response<T> response = call.execute();
                if (response != null) {
                    if (response.code() >= 200 && response.code() < 300) {
                        return response.body();
                    } else if (response.code() >= 400 && response.code() < 500) {
                        if (response.errorBody() != null) {
                            throw new HystrixBadRequestException(response.errorBody().string());
                        } else {
                            throw new HystrixBadRequestException("客户端参数非法");
                        }
                    } else {
                        if (response.errorBody() != null) {
                            throw new RuntimeException(response.errorBody().string());
                        } else {
                            throw new RuntimeException("服务端未知异常");
                        }
                    }
                } else {
                    throw new RuntimeException("未知异常");
                }
            }

            @Override
            protected T getFallback() {
                return fallback.fallback(getExecutionException());
            }

        }.execute();
    }
}

上述示例代码GitHub地址

Hystrix原理

hystrix是如何隔离调用的?

hystrix缺省使用了线程池进行隔离,HystrixCommand中的run方法是在异步线程池中执行的。

  • 线程池的名称缺省为HystrixCommand中groupKey的名称。
  • 线程池的核心线程数为10(hystrix.threadpool.default.coreSize = 10 // 缺省为10)
  • 线程池最大线程数为10(hystrix.threadpool.default.maximumSize = 10 // 缺省为10)
  • 线程池满了以后立即触发拒绝策略加速熔断(hystrix.threadpool.default.maxQueueSize = -1)

使用了hystrix它的断路触发规则是什么样子的呢?

默认的触发熔断的条件是:

  1. 在最近的一个时间窗口期(hystrix.command.default.metrics.rollingStats.timeInMilliseconds = 10000 // 默认10秒)内
  2. 总请求次数>=(hystrix.command.default.circuitBreaker.requestVolumeThreshold = 20 //默认20)
  3. 并且发生异常次数的比例>=(hystrix.command.default.circuitBreaker.errorThresholdPercentage = 50 // 默认50%)

满足1~3条件后断路器打开,触发熔断后续的执行会被拦截直接走getFallback方法返回。5秒以后(hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds = 5000 // 缺省5秒)下一个的请求会被通过(处于半打开状态),如果该请求执行失败,回路器会在睡眠窗口期间返回OPEN,如果请求成功,回路器会被置为关闭状态。

有些异常是客户端问题却错误地统计进了熔断监控统计中该怎么办?

查看官方文档可知:

Execution Exception types

Failure Type Exception class Exception.cause subject to fallback
FAILURE HystrixRuntimeException underlying exception (user-controlled) YES
TIMEOUT HystrixRuntimeException j.u.c.TimeoutException YES
SHORT_CIRCUITED HystrixRuntimeException j.l.RuntimeException YES
THREAD_POOL_REJECTED HystrixRuntimeException j.u.c.RejectedExecutionException YES
SEMAPHORE_REJECTED HystrixRuntimeException j.l.RuntimeException YES
BAD_REQUEST HystrixBadRequestException underlying exception (user-controlled) NO

HystrixBadRequestException 不会记录进熔断统计中我们可以此异常包装我们的客户端异常

客户端异常不纳入熔断统计

官方wiki的两张图很好的展示了Hystrix原理

工作流程图
熔断触发机制

尾巴

通过Hystrix的引入再次深入了服务的容错处理实现。

构建强健的系统远比demo付出的多太多。深入再深入一点,加油_

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

推荐阅读更多精彩内容

  • 一、认识Hystrix Hystrix是Netflix开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔...
    新栋BOOK阅读 4,024评论 0 19
  • 一、认识Hystrix Hystrix是Netflix开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔...
    新栋BOOK阅读 26,460评论 1 37
  • 在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这...
    阿靖哦阅读 978评论 0 6
  • 0. Hystrix是什么? Hystrix的本意是指 豪猪 的动物,它身上长满了很长的较硬的空心尖刺,当受到攻击...
    亦山札记阅读 14,828评论 4 36
  • 原文:https://my.oschina.net/7001/blog/1619842 摘要: Hystrix是N...
    laosijikaichele阅读 4,300评论 0 25