0. 概要
本文分享一些在線問題診斷的經驗,主要是業務層面,服務層面的在線問題診斷一般需要依賴服務監控系統和報警系統來輔助定位問題。
1.診斷分類
在服務端的開發中,我覺得有這幾類問題的診斷。
- 僅僅知道請求關鍵參數的診斷。比如某個手機出了不該出的東西,我們診斷一下,只需要知道這個手機的imei即可;某個知乎用戶,他的知乎爲什麼會出不該出的廣告,只需要給他的知乎id,其他信息自己構造就可以。
- 需要知道設備和請求詳細參數的診斷。上面第一種診斷,一般問題可以解決,但是有時候線上的某個設備,你自己構造請求過去,返回的結果也是符合預期的,但是實際就是返回的內容不符合預期的,這個時候需要在線系統有能力嗅探和捕捉線上的真實請求。
- 數據分析層面的診斷。有時候會有這麼一種情況,1和2都是正常的,單獨構造某個請求是正常的,那麼可能是部分設備出了問題,這個時候需要動用一些數據分析,數據分析通常是和各業務指標在一起的,比較依賴於經驗。
今天主要圍繞第1類診斷來展開談談這類系統的設計與實現。
2.整體架構
在線診斷架構圖
如上圖,該診斷系統需要具備以下幾個能力。
- 界面。提供給開發人員方面構造請求的友好的界面。
- 請求構造。讓開發人員只需替換關鍵信息(如手機imei,賬戶ID等)即可構造請求。
- Porxy。該模塊需要支持各種不同環境(如分機房)的請求客戶端構造、需要支持某個固定IP的服務的客戶端、每個模塊的客戶端。Http或者是RPC的。
- 結果展示。需要方便的給研發人員診斷問題的結果,一般以json來展示即可。
3.數據結構設計
診斷日誌最重要的功能是:需要知道系統中每一步關鍵邏輯發生了什麼。同時又不能夠給在線系統帶來相應的時間和空間的開銷。所以需要給流量打上標記,標記這個流量是debug的模擬流量。
request: { "deviceId": "xxx", "timeStamp":"1533101903000", ... "debug":"on"; //線上實際流量沒有這個標記. }
返回結果:
response: { "status": "0", "timeStamp":"1533101903000", ... //線上實際流量沒有這個標記. "debugOnlineInfo": { step1:{"", ""}, step2:{"", ""}, step3:{"", ""}, ... }; }
數據結構定好之後,如果很粗糙的在需要在日誌埋點的地方都需要工程師去判斷是不是debug=on,那工程師會崩潰的。所以功能上要做成無侵入業務的。我想沒有人會願意付出額外成本不斷的去寫如下代碼:
...//業務邏輯A if (debugOn == true) { //工程師內心OS: 幹嘛要讓我判斷啊,這種操作不應該封裝好嗎? fuck... debugOnlineInfo.putLog("after loggic A, the result is xxx"); } ...//業務邏輯B List<CommonBean> commonBeans = xxx. if (debugOn == true) { //工程師內心OS: 幹嘛要讓我遍歷啊,這種操作不應該封裝好嗎? fuck... debugOnlineInfo.putLog("after loggic B, the result is ", commonBeans.stream().map(e => e.getId()).collect(collectors.joining(","))); }
4.實現 基於以上的分析,我們實現的時候需要考慮以下幾個點:
- 不能讓業務方感知是不是debugOn的日誌,也不能對線上系統有額外的開銷;
- 要提供常用遍歷List的接口,打印關鍵信息即可;如果讓業務代碼自己去實現,會打印太多信息。
- 做好NPE的判斷,並記錄信息; 不要讓請求直接掛了;
- 需要線程安全,因爲在實際的線上環境,有的會多個線程都向這個數據裏面去put數據,尤其是多條pipeline執行同一邏輯的時候;
- 需要有序,我們需要知道日誌的每一步發生了什麼。 每一次請求進來的時候都構造一個DebugOnlineInfo對象,根據debugOn的信息構建類的實現,關係超級簡單。
image
DebuggerOnlineImpl用作在線診斷時候的實現,DebuggerOnlineNoOp作爲線上實際流量的實現,線上的真實流量DebugOnline實現爲空。
DebuggerOnlineImpl實現的時候有一些細節需要注意。
1.爲了只打印關鍵信息,重載appendLog方法:
public <T> void appendLog(String key, Collection<T> collection, IdExtractor<T> idExtractor) { if (StringUtils.isBlank(key)) { return; } String value = collection.stream().map(idExtractor::get).collect(Collectors.joining(",")); debugMessage.put(key, value); }
這裏IdExtractor爲一個解析類關鍵信息的接口,如果不這麼做,直接toString()的話,那麼會導致日誌信息特別多,日誌過多不利於我們定位問題。所以提供一個解析的類,可以供常用的遍歷Collection,實現的時候用單例即可。
- 爲了更加通用,用Supplier重載appendLog方法,使用lamda參數,執行延後。
public void putLog(String key, Supplier<String> stringSupplier) { if (StringUtils.isBlank(key) || stringSupplier == null) { return; } String value = stringSupplier.get(); debugMessage.put(key, value); }
比如想埋點某個map的所有Key,工程師埋點的時候一行代碼即可搞定:
debugOnline.putLog("after logic A ", () -> map.keySet().stream().collect(Collectors.joining(",")));
3.線程安全,有序
Map<String, StringBuilder> debugMessage = Collections.synchronizedMap(new LinkedHashMap<>());
要保證最後輸出有序,所以我們最後用的是LinkedHashMap,保證線程安全,用的是Collections.synchronizedMap。
祝:搬磚愉快:)