我們在使用Hystrix時,大部分情況下都是直接基於SpringCloud的相關注解來完成請求調用的。我們有個項目中是手動創建HystrixCommand來包裹RestTemplate發起請求的。但是在服務運行過程中,發現一個情況,就是當HystrixCommand超時返回fallback結果後,RestTemplate請求過程還沒有結束,導致線程池佔用較多。
這裏通過一個簡單的測試,對RestTemplate和HystrixCommand設置不同的超時時間,來觀察在HystrixCommand執行過程中的細節。
測試觀察
模擬外部服務:
創建一個springboot服務,提供一個接口:等待5秒後返回數據
@RestController
public class DataController {
@RequestMapping("queryData")
public String queryData() {
// 等待5s後,返回隨機字符串
long sleepTime = 5000;
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
return UUID.randomUUID().toString();
}
}
測試代碼:
public class HystrixCommandTest {
private static final SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
// http請求超時時間
private static final int HTTP_TIMEOUT = 10000;
// hystrix超時時間
private static final int HYSTRIX_TIMEOUT = 10000;
private RestTemplate restTemplate;
@Before
public void init() {
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
httpRequestFactory.setReadTimeout(HTTP_TIMEOUT);
restTemplate = new RestTemplate(httpRequestFactory);
}
@Test
public void test() {
// 創建HystrixCommand.Setter
HystrixCommandProperties.Setter propSetter = HystrixCommandProperties.Setter()
.withExecutionTimeoutEnabled(true) //開啓超時機制
.withExecutionTimeoutInMilliseconds(HYSTRIX_TIMEOUT) //設置超時時間
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) //線程隔離
.withExecutionIsolationThreadInterruptOnTimeout(true); //這裏設置超時中斷線程,但其實沒有實際效果
HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("queryData"))
.andCommandPropertiesDefaults(propSetter);
// 通過Setter創建創建HystrixCommand
HystrixCommand<String> hystrixCommand = new HystrixCommand<String>(setter) {
@Override
protected String run() throws Exception {
// 發起http請求
print("send request");
String result = restTemplate.getForObject("http://127.0.0.1:9001/queryData", String.class);
print("get response");
return result;
}
@Override
protected String getFallback() {
print("fallback");
return null;
}
};
print("execute command");
// 執行HystrixCommand
String result = hystrixCommand.execute();
print("get result=" + result);
// 阻塞main線程,防止程序終止
while (true) {
}
}
private void print(String msg) {
System.out.println(df.format(new Date()) + " [" + Thread.currentThread().getName() + "]:" + msg);
}
}
測試場景1:RestTemplate和HystrixCommand都沒有超時
參數設置:
RestTemplate超時時間>接口響應時間(5s),Hystrix超時時間>接口響應時間(5s)
HTTP_TIMEOUT和HYSTRIX_TIMEOUT都設置爲10s
輸出結果:
主線程創建HystrixCommand並執行,Hystrix創建子線程發起http請求,5秒後收到響應。最後主線程收到正確響應結果。
測試場景2:RestTemplate超時,HystrixCommand沒有超時
參數設置:
RestTemplate超時時間<接口響應時間(5s),Hystrix超時時間>接口響應時間(5s)
HTTP_TIMEOUT設置爲3s,HYSTRIX_TIMEOUT設置爲10s
輸出結果:
整個流程爲:主線程創建HystrixCommand並執行,Hystrix創建子線程發起http請求,3秒後http請求超時,進入fallback方法。最後主線程收到結果爲null。
測試場景3:RestTemplate沒有超時,HystrixCommand超時
參數設置:
RestTemplate超時時間>接口響應時間(5s),Hystrix超時時間<接口響應時間(5s)
HTTP_TIMEOUT設置爲10s,HYSTRIX_TIMEOUT設置爲3s
輸出結果:
整個流程爲:主線程創建HystrixCommand並執行,Hystrix創建子線程發起http請求,3秒後Hystrix超時,HystrixTimer線程調用fallback方法。最後主線程收到結果爲null。
但注意,這裏main方法收到返回結果後,發起http請求的線程在5s後還是收到了請求。也就是說,這裏即使HystrixCommand超時結束了,其實際發起請求的子線程並不會結束,即使設置了
withExecutionIsolationThreadInterruptOnTimeout(true)
也沒有用。
底層機制
我參考的Hystrix源碼解析相關文章,並閱讀了Hystrix部分源碼後瞭解到:
HystrixCommand執行過程中,有兩個線程,一個是HystrixCommand任務執行線程,一個是等着給HystrixCommand判定超時的線程(HystrixTimer)。當其中一個線程完成自己的邏輯時,會嘗試將HystrixCommand的狀態置換(CAS),只要任何一個線程對HystrixCommand打上標就意味着超時判定結束。
如果任務執行線程先完成,就會將status設置爲completed,超時監聽線程在到達超時時間時,發現status已經被標記爲完成狀態,直接結束。(對應上面的場景1和2)
如果超時監聽線程先到達超時時間點,就會將status設置爲timeout,此時HystrixCommand會執行fallback中的流程,同時任務執行線程依舊在運行,直到其流程終止。(對應上面的場景3)
流程圖
結合底層的原理,對上述三種場景簡單地畫一下流程圖,便於理解: