spring cloud 微服務異常記錄與報警

doob發表於2019-03-04

前言

當我們的應用線上上正常運轉起來了,在正常情況下我們不需要再擔心任何的事情,但是bug總是不可避免的會出現;此時我們就需要一種相關的機制能夠發現我們系統中的異常並通知到相關人員,不然等到使用者進行反饋時才能知道發生了bug是很影響使用者體驗的也是不可控的,這兩者都是不可接受的。

介紹

我所在的團隊目前正在使用spring cloud相關套件進行微服務的開發,所以我的介紹與實踐也是在該技術棧下進行,同時可能會使用到elasticsearch。
我們使用spring mvc來進行業務開發,feign來做restful介面遠端呼叫框架,zuul作為服務閘道器來對外開放介面。這個技術棧在使用spring cloud進行開發的團隊是非常常見的。

思路

因為最開始我們就有在閘道器記錄統一的訪問日誌,並使用filebeat將其同步到elasticsearch以方便後期資料的查詢與分析,但是隻是這樣子是不夠的;我們想要直接能夠從日誌中能夠判斷是否發生了畢竟明確的異常,我們需要有一個點或者相關的閾值去確定什麼情況是異常,可能會有bug,需要進行相關的操作去進行報警。所以我們需要在日誌記錄上面做一些文章,讓我們記錄的日誌能夠有足夠有力和準確的資訊讓我們去判斷是不是異常,然後去觸發一系列的操作(報警等等…)。
經過一定的分析之後我認為異常是一個很好的判斷是否有bug的點,因為沒有異常不一定沒有bug,但是有沒有被捕獲異常的請求一定是有bug的;所以以此作為切入點,深入思考。

實現

開發中的定製

首先我們的基礎架構指定了統一的錯誤碼來對外進行提示,同時在業務層以丟擲異常來對外進行提示。
我們將它定義為:

public class DomainServerException extends Exception {
    // 平臺定義錯誤碼
    private int code;
    // http status
    private int status;
    // 具體錯誤資訊,面向開發者的提示, Exception的message用於面向使用者的提示
    private int error;
    // 相關異常的堆疊資訊
    private int stack;

    public DomainServiceException(int code, String message, Throwable throwable) {
        super(message, throwable);
        this.code = code;
        if (isServerError())
            this.stack = ExceptionUtils.getStackTrace(throwable);
    }
}複製程式碼

其中的stack資訊就是為了我們進行異常追蹤而新增的欄位,當http status為5**或者平臺定義錯誤碼為伺服器異常的時候會載入相關異常堆疊資訊並設值到stack欄位。具體是在DomainServiceException建構函式進行或者在spring ErrorController中進行相關設值操作(因為我們使用異常來丟擲錯誤碼,所以我們對spring MVC預設的ErrorController進行了定製)。
我們ErrorController的返回型別定義為

public class HttpErrorResponse implements Serializable {
    private Date timestamp;
    private Integer status;
    private String error;
    private String message;         
    private String stack;           // 異常堆疊  方便記錄同時在前後端除錯的時候資訊也更加豐富
    private String exception;       // 異常型別
    private String path;            // 錯誤請求路徑
    private Integer errorCode;      // 平臺定義錯誤碼
}複製程式碼

對於錯誤的情況我們丟擲DomainServerException或者其他未捕獲異常,DomainServerException預設為我們的業務錯誤,同時也可作為異常錯誤,但是我們在進行異常錯誤處理為DomainServerException是會將上層異常堆疊傳入建構函式生成DomainServerException異常物件(注意:此模式下一定不要去處理你不知道該怎麼處理的異常,如果你處理不了就一直往外拋,ErroController能夠正確的處理並記錄他然後供報警使用)。
此時我們丟擲的HttpErrorResponse可能會被兩個地方用到:

  1. 服務之間的呼叫
  2. zuul轉發來的請求
  • 對於第一種情況,因為我們使用feign來進行服務間遠端呼叫,我們重寫了ErrorDecoder來進行HttpErrorResponseDomainServerException或者DomainServerException的子類(通過exception欄位來進行型別判斷)並向外丟擲。級聯呼叫一次類推,最終都會到閘道器一層進行處理。所以第一種情況最終也會變為第二種情況。

  • 對於第二種情況我們,因為我們有錯誤碼的定義並且在正常情況下我們也會返回錯誤,但是正常的結果卻是沒有錯誤碼的,所以我們在zuul實現了一個type為“post”的filter來對返回值進行格式化,同時也對老的平臺與新的平臺進行輸出格式化。在這裡面我們判斷服務返回的內容是否有異常並進行相關的記錄(存入相關資訊到RequestContext),最後在統一日誌記錄Filter(包含正常filter和zuul 異常filter)進行統一記錄相關資訊。同時因為我們在zuul也寫了一些膠水介面,所以我們在Zuul繼承了普通服務的ErroController實現了ZuulErrorController同時也會記錄異常資訊。

日誌的儲存和報警

記錄怎麼樣的日誌已經確定了,我們使用filebeat來講日誌資料傳輸到elasticsearch中。現在我們elasticsearch中就有錯誤碼和stack的資訊了,很明顯,stack資訊是很明顯的錯誤資訊,紫瑤該欄位一出現就表示我們的程式碼又問題,我們可以根據這個很好的去報警。對於錯誤碼資訊,可能會比較複雜,我們需要判斷他在某些情況下的一個閾值,當我們在某種情況下相關錯誤碼超過了該閾值就報警(目前該塊的應用還需要多思考)
對於elasticsearch查詢報警的工具有elastalert,但是我對於該工具不是很感冒,同時我也疲於應對python部署那些複雜的依賴,我正在使用golang開發一款功能更簡潔,學習成本更低的工具。如果在內部試用的還行應該會進行開源。

未完待續

相關文章