前言
后端開發的很多同學應該都有過調用第三方HTTP協議服務的經驗。
比如調用百度查詢經緯度編碼信息、調用豆瓣查詢時下熱門電影、調用7牛云存儲接口上傳文件等;以及公司內部其他小組開放的服務接口。
常見開發模式是我們按照服務提供方定義的接口文檔進行接口調用開發。
在java中常見的HTTP客戶端庫有:
庫 | 個人建議 |
---|---|
Java原生HttpURLConnection | 功能簡陋,不推薦使用 |
Apache HttpClient | 老牌框架穩定可靠,懷舊黨可考慮 |
OkHttp,Retrofit | 新興勢力,發展迅猛;支付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/
- 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);
}
- 使用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);
}
}
- 創建完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();
- 新增HystrixCommand實現
- 在run方法中執行具體的方法
- 通過調用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();
}
}
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它的斷路觸發規則是什么樣子的呢?
默認的觸發熔斷的條件是:
- 在最近的一個時間窗口期(hystrix.command.default.metrics.rollingStats.timeInMilliseconds = 10000 // 默認10秒)內
- 總請求次數>=(hystrix.command.default.circuitBreaker.requestVolumeThreshold = 20 //默認20)
- 并且發生異常次數的比例>=(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付出的多太多。深入再深入一點,加油_