為第三方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付出的多太多。深入再深入一點,加油_

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、認識Hystrix Hystrix是Netflix開源的一款容錯框架,包含常用的容錯方法:線程池隔離、信號量隔...
    新棟BOOK閱讀 4,074評論 0 19
  • 一、認識Hystrix Hystrix是Netflix開源的一款容錯框架,包含常用的容錯方法:線程池隔離、信號量隔...
    新棟BOOK閱讀 26,516評論 1 37
  • 在分布式環境中,許多服務依賴項中的一些必然會失敗。Hystrix是一個庫,通過添加延遲容忍和容錯邏輯,幫助你控制這...
    阿靖哦閱讀 1,020評論 0 6
  • 0. Hystrix是什么? Hystrix的本意是指 豪豬 的動物,它身上長滿了很長的較硬的空心尖刺,當受到攻擊...
    亦山札記閱讀 14,871評論 4 36
  • 原文:https://my.oschina.net/7001/blog/1619842 摘要: Hystrix是N...
    laosijikaichele閱讀 4,333評論 0 25