漫談Exception與Result

同步自: https://sulin.me/2019/T2ZXZB....

在分佈式系統開發中,咱們常常須要將各類各樣的狀態碼、錯誤信息傳遞給最外層的調用方,這個調用方一般是http/api接口,錯誤信息好比登陸失效參數錯誤等等。html

最外層接口暴露的數據一般是相似於{code, msg, data}這樣的json格式,這一點沒有任何爭議。java

可是分佈式系統的節點之間RPC調用、節點內部方法調用中,一般會用ServiceExceptionResult<T>的方式進行錯誤信息的傳遞,這兩種方式有什麼區別以及孰優孰劣呢?本文側重於開發效率系統性能探討這個問題。git

Result<T>介紹

這是一種比較常見的錯誤信息傳遞方式,某些大廠甚至直接將它們設爲技術規範,強制各個團隊採用這種方式。常見的Result模板以下:github

@Data
public class Result<T> {
    private int code; // 也能夠是String等
    private String msg;
    private T data;
}

在系統開發中的應用一般是這樣的:golang

Result<UserModel> userModelResult = userService.query(userId);
if (!userModelResult.isSuccess() || userModelResult.getData != null) {
    return Result.fail(userModelResult); // 透傳錯誤
}
UserModel userModel = userModelResult.getData();
if (userModel.getStatus() != UserStatusEnum.NORMAL) {
    return Result.fail("user unavaliable"); // 用戶不可用
}
// ... 正常使用UserModel

在比較複雜的分佈式微服務環境中,相似的代碼很是之多,每一個依賴服務的調用都伴隨着一段相似的容錯邏輯。算法

這種模式比較相似Golang語言中的錯誤碼處理,這也是Golang比較被人詬病的地方,即每一步都得進行錯誤判斷。json

更殘酷的現實是,儘管有了Result封裝,可是仍然會有後端系統的Exception透傳過來。在我接觸過的實際應用中,這種突破Result封裝的異常透傳絕非個例,我本身負責的系統在調用更後端的國內最強交易系統時,就曾接到過最內部交易中心TC的業務異常,排查問題時追蹤的團隊就有不止5個。後端

ServiceException介紹

顧名思義,這個方式就是使用異常中斷將正常邏輯與異常邏輯進行拆分。api

在系統開發中,大部分錯誤都須要直接中斷服務,直接將錯誤反饋給用戶,正由於如此,咱們在使用Result<T>時,常常須要寫相似if(result.isFail()){return…}這樣的代碼。而使用ServiceException,咱們就能夠省略掉絕大部分相似的代碼。性能優化

一般ServiceException能夠這樣定義:

@Getter
public class ServiceException extends RuntimeException {
    private final int code;
    private final String msg;
    public ApiException() {
        this(-1, null);
    }
    public ApiException(Code code) {
        this(code, null);
    }
    public ApiException(Code code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}

系統內部組件在遇到數據缺失越權訪問登陸失效帳戶鎖定等異常狀況時,直接拋出ServiceException中斷邏輯,而後由最外層的FilterAspect捕捉異常,提取其中的codemsg返回給用戶。

實際使用的代碼邏輯相似這樣:

UserModel userModel = userService.query(userId); // userID不存在、不可用等隱藏在異常中
// ... 使用userModel

這種方式明顯優雅、精簡了許多,對於開發效率的提升以及後期維護都有幫助。

可是在坊間有許多流言聲稱,使用異常中斷會影響性能,甚至有人經過簡單的性能測試推出異常中斷的性能耗時比返回Result快幾百倍云云。

性能測試

針對性能問題,我也進行了一個簡單的測試,具體測試代碼參見:

https://github.com/sisyphsu/b...

這裏使用JMH進行性能測試,說到benchmark,真的是羨慕golang語言自帶的test庫,實在是太方便了。

測試內部的業務邏輯很是簡單,只是調用一次System.currentTimeMillis()並返回long時間戳。

性能測試中分別使用Result<T>返回值以及拋出Exception,針對拋出異常的性能測試,又增長的不一樣深度的調用棧測試,這是由於Java在拋出異常時,須要分析當前Thread的棧,而調用棧越深,所形成的性能損耗就越大。具體棧深度取值爲一、十、100:

Test.test                  avgt    5  0.027 ± 0.001  us/op
Test.testException         avgt    5  1.060 ± 0.045  us/op
Test.testDeep10Exception   avgt    5  1.826 ± 0.122  us/op
Test.testDeep100Exception  avgt    5  9.802 ± 0.411  us/op

乍一看,異常棧深度爲100的性能損耗確實是普通方法調用的360倍,有的人也確實是基於這種理由得出Java異常中斷性能損耗嚴重的結論。

分析性能的影響

可是須要注意時間單位,只是微秒而已,毫秒的千分之1、秒的百萬分之一。

假設某個微服務單CPU吞吐量爲1000QPS,而其中有10%是非法請求,那麼異常中斷的性能損耗也只是萬分之一而已,對於服務耗時的影響也只是0.001毫秒而已。

在性能測試中,業務耗時只是獲取系統時間,大概耗時爲25ns。正由於如此才顯得異常中斷的性能損耗達到恐怖的「幾百倍」,可是若是業務耗時從25ns變爲25us25ms呢?

再談性能瓶頸

咱們在分析系統性能時,必定要搞清楚它的數量級以及性能瓶頸,切記陷入性能優化的困境。

舉個粗糙例子,在常規服務中,利用了索引的DB操做在1~10毫秒之間,訪問分佈式Cache的耗時在3~30毫秒之間,微服務RPC的網絡性能損耗在3~10毫秒之間,客戶端與服務器之間的網絡耗時在5~300毫秒之間,如此之類等等。在這種狀況下,優化0.001毫秒的性能隱患無異於撿了芝麻丟了西瓜。

我曾經寫過相似TCP的底層網絡協議,在那種高頻場景中,算法優化帶來0.1微秒的性能優化就意味着每秒鐘吞吐量幾成甚至幾倍的提高,可是在分佈式調用的低頻場景中,這種性能用處沒有任何用處。

另一個例子,幾年前我和同事在討論DB數據表設計時,由於訂單狀態使用什麼長度的int而爭執的面紅脖子粗,如今想一想,訂單狀態上優化的1個字節,終年累月下來也只是節省不到1MB的磁盤空間而已,有什麼用呢?

RPC中的異常中斷

對於使用Dubbo、HSF這種遠程調用框架而言,使用異常中斷進行錯誤信息傳遞,須要注意一點就是,異常類型須要設計爲通用的,即各個微服務都引用的基礎類型。

在某廠的技術規範中有說到:

1) 使用拋異常返回方式,調用方若是沒有捕獲到就會產生運行時錯誤。

2) 若是不加棧信息,只是new自定義異常,加入本身的理解的error message,對於調用端解決問題的幫助不會太多。若是加了棧信息,在頻繁調用出錯的狀況下,數據序列化和傳輸的性能損耗也是問題。

我對這種技術規範至關的不覺得然。

首先業務異常原本就須要調用方透傳給最外層,諸如數據不存在登陸失效用戶鎖定這種異常,中間的調用方捕獲了也每每沒有什麼用。

其次又是鬼扯性能損耗,這種低頻的數據序列化和內網傳輸會有什麼樣的性能損耗呢?棧信息透傳給調用方也有益於故障排查,我曾經接到過TC的異常棧信息,根據棧中的package直接就繞過三四層找到了底層出錯的地方,能夠說是節省了大量的時間。

結論

在分佈式微服務中,採用異常中斷能夠大幅精簡業務代碼,對於性能的影響也微乎其微。

輔助以@NotNull@Nullable等註解,可讓分佈式開發如風通常的快速便捷。在複雜的服務網絡中,業務異常也能夠方便開發人員精確地定位錯誤,避免你們順着調用鏈一層一層地追蹤故障點的尷尬情景。

相關文章
相關標籤/搜索