作為一名應用開發,大家是否有遇到以下現象,為什么一套非常優秀的兜底機制還是會出現頁面空窗現象?本文將會通過實體和大家分享,作者在執行緒池使用程序中遇到的問題:例外處理,以及下執行緒池的引數設定經驗,
什么現象
先解釋下什么是空窗,就是資料缺失導致某塊或整頁出現空白的現象,
事情有點早了,剛接聚劃算,還沒來得及看邏輯,就被告知,壓測時頁面出現了空窗,像這樣:
原因是什么
其實就是對應的介面超時或者資料處理例外,導致該塊兒資料沒有回傳,
我們的代碼是運行在阿拉丁容器里的,阿拉丁本身是有兜底機制的,并且有兩層:
如果介面發生例外,阿拉丁會從tair里取快取的資料回傳給前端做兜底
如果阿拉丁也沒有兜住,前端接收到錯誤的code,會自動從cdn取對應介面的資料做兜底
這套機制還是非常優秀的,但為什么還是出現了空窗了,
翻看代碼發現,是我們把對應的例外給吃掉了,沒有拋給阿拉丁容器,代碼是這樣的:
try {
executorService.invokeAll(callableHashSet);
} catch (Exception e) {
throw new RuntimeException(e);
}
初看,是不是以為把try catch拿掉就沒問題了,然而不是,我們看看java.util.concurrent.ExecutorService#invokeAll的實作,先看我們最常用的ThreadPoolExecutor,它的invokeAll方法在父類AbstractExecutorService里實作:
這里變數ignore的命名非常漂亮,想都不用想,它被忽略了,為什么要看這個ExecutionException,是因為執行緒里發生的例外都被包裝成了ExecutionException,我們跟著AbstractExecutorService##invokeAll看下,上圖有個newTaskFor,看下實作:
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
看看FutureTask#get方法:
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
最終在report方法里實作:
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
可以看到,如果執行緒里拋出了例外,都被包裝成了ExecutionException,而ThreadPoolExecutor#invokeAll方法里忽略了這個例外,導致我們根本捕捉不到例外,
上邊說的是ThreadPoolExecutor,我們再看另一個常用的ExecutorService的實作類ForkJoinPool:
看到了吧,方法命名就告訴你了,我不會拋例外給你,進去看看ForkJoinTask#quietlyJoin:
注釋說得很清楚,不拋例外,
怎么解決
首先,根據上邊的分析,要慎用invokeAll,解決也很簡單,可以有以下幾種方式:
1. 能讓主執行緒感知到例外,并向外拋,就可以觸發阿拉丁的兜底
2. 模塊內部做資料快取,捕捉到例外以后取快取資料做兜底
? 第一回合
因為對整體的邏輯沒摸透,不敢直接替換掉invokeAll,會影響整個聚劃算首頁,時間又比較急,就先縮小改動范圍,選取方法二:
在對應模塊內容做資料快取,為了兼顧時效性(聚劃算商品有上團和下團時間,所以時效性很強),做了1分鐘的快取和5分鐘的快取,如果發生例外,按優先級取快取,優先1分鐘的快取,
為了減輕寫壓力,只針對一定比例的請求寫快取
效果
上線之后沒問題,然而第二次全鏈路壓測,半夜又收到訊息說空窗了,
第一回合失敗,
? 第二回合
經過分析,可能有多個原因:
1. 應該是壓測狀態下,下游服務持續壓力大,導致快取資料過期,
2. 寫入快取的資料也沒有做好校驗,可能寫入不合法的資料
繼續做調整:
1. 嚴格校驗寫入快取的資料,保持快取資料的合法性
2. 既然是兜底資料,可以直接快取在記憶體,這樣就不用關心寫比例,直接100%快取合法資料,并且不設定失效時間,這樣保證兜底時總能取到最新的合法資料
3. 把該組件的Callable從invokeAll里拎出來,增加預案,可以觸發整頁兜底,作為最后的保命手段,如下:
效果
后續壓測和日常沒再出現過空窗,就這個模塊來說,應該沒問題了,
這樣就好了嗎
其實不應該結束,上述方案都是在時間緊張的情況下做的臨時補救措施,代碼里到處是特判邏輯,我們應該有更系統的設計方案:
1. 模塊例外都外拋,觸發阿拉丁的兜底,但阿拉丁的兜底是介面級別的,我們一個介面里邊通常包含多個模塊,如果因為次要模塊導致用戶看到的主要模塊也是兜底的資料,用戶體驗不好
2. 針對每一個模塊做獨立的兜底,但像上述方法一樣,一個模塊一個模塊來改,太累,也容易遺漏,我們應該有一個框架性設計,讓以后的開發只需要關心業務邏輯,而不用關心這些非功能性問題,這點我準備在EasyWidget里邊來實作,基礎設施已經具備,只需要在模板方法里加幾行就能實作,
總結一下
這里邊遇到的主要問題是沒有正確處理執行緒池的例外和兜底設計不完善導致,兜底的設計上邊提到了思路,我們再看下處理子執行緒內部例外的常用方式:
? 通過原子變數
AtomicBoolean exception = new AtomicBoolean(false);
Callable<Void> qwbkt = () -> {
try {
qwbktSections.add(qwbktManager.query(context, null));
} catch (Throwable t) {
context.getLogger().error("qwbkt exception:", t);
exception.set(true);
}
return null;
};
//...
if (exception.get()) {
throw new RuntimeException("queryError");
}
? 以code形式回傳
Callable<String> task = new Callable<String>() {
@Override
public String call() throws Exception {
Result<String> result = new Result<>();
try {
//..
} catch (Exception e) {
result.setCode("500");
}
return result;
}
};
? 老老實實future#get
try {
String s = future.get();
} catch (InterruptedException e) {
//..
} catch (ExecutionException e) {
//todo: 這里處理執行緒內部例外
}
再說說執行緒池的其它問題
? 執行緒池設定不合理
看到很多應用里的執行緒池引數不合理,尤其是很多新同學,分不清前臺應用和后臺任務需要的執行緒數和拒絕策略怎么設定,
很多同學從教程里邊或者某些框架原始碼里邊看到執行緒池的執行緒數盡量跟機器核數保持一致,就一直保持這個設定,
還有看到前臺應用了設定了少量的執行緒,佇列長度是10000,這種情況在遇到突發流量的情況下很容易把自己拖垮,之所以一直沒觸發問題,一種原因可能是沒有遇到過大流量,另一種可能是被限流保護了,一旦限流沒有設定好,就可能遇到致命問題,
這里簡單說下自己的經驗:
搞清楚核心執行緒數、最大執行緒數、任務佇列的作業原理,核心執行緒用完了是先放任務佇列,佇列滿了才會繼續增加執行緒數至最大執行緒數
前臺應用佇列長度一定不能太大,根據執行緒數、介面RT、客戶端所能接受的RT來計算佇列長度
分清我們的應用是CPU密集型還是IO密集型,大多數情況我們的業務應用都是IO密集型的,這種情況下不必拘泥于執行緒數跟核數保持一致
用Runtime.getRuntime().availableProcessors()設定執行緒數的時候,你以為取到的是虛擬機的執行緒數,但很可能取到的是物理機的執行緒數,要注意這個坑
前臺應用的執行緒數必須通過壓測不斷調整,才能獲得合理的執行緒數,但一旦依賴介面的RT等情況發生變化,執行緒數就可能不再合理,所以合理的執行緒數很難保持
后臺應用如果不關心回應的及時性,可以設定較大的佇列,但要關注機器記憶體,也要主要機器重啟時的任務丟失問題
? 執行緒池的關閉
任務不能丟失的時候一定要在jvm關閉的時候通過鉤子關閉執行緒池,
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run()
{
threadPool.shutdown();
}
}));
上述方法只在jvm正常關閉的時候有效,如果強殺或斷電等情況還是有問題,就要做更強有力的保障,如先發訊息佇列,再處理,
? 拓展閱讀
作者|朱天富(海培)
編輯|橙子君
出品|阿里巴巴新零售淘系技術
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/261419.html
標籤:其他
