摘要:分析性能的影響但是需要注意時間單位,只是微秒而已,毫秒的千分之一秒的百萬分之一。在這種情況下,優化毫秒的性能隱患無異于撿了芝麻丟了西瓜。
同步自:https://sulin.me/2019/T2ZXZB....
在分布式系統開發中,我們經常需要將各種各樣的狀態碼、錯誤信息傳遞給最外層的調用方,這個調用方通常是http/api接口,錯誤信息比如登錄失效、參數錯誤等等。
最外層接口暴露的數據通常是類似于{code, msg, data}這樣的json格式,這一點沒有任何爭議。
但是分布式系統的節點之間RPC調用、節點內部方法調用中,通常會用ServiceException或Result
這是一種比較常見的錯誤信息傳遞方式,某些大廠甚至直接將它們設為技術規范,強制各個團隊采用這種方式。常見的Result模板如下:
@Data public class Result{ private int code; // 也可以是String等 private String msg; private T data; }
在系統開發中的應用通常是這樣的:
ResultuserModelResult = 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比較被人詬病的地方,即每一步都得進行錯誤判斷。
更殘酷的現實是,盡管有了Result封裝,但是仍然會有后端系統的Exception透傳過來。在我接觸過的實際應用中,這種突破Result封裝的異常透傳絕非個例,我自己負責的系統在調用更后端的國內最強交易系統時,就曾接到過最內部交易中心TC的業務異常,排查問題時追蹤的團隊就有不止5個。
ServiceException介紹顧名思義,這個方式就是使用異常中斷將正常邏輯與異常邏輯進行拆分。
在系統開發中,大部分錯誤都需要直接中斷服務,直接將錯誤反饋給用戶,正因為如此,我們在使用Result
通常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中斷邏輯,然后由最外層的Filter或Aspect捕捉異常,提取其中的code和msg返回給用戶。
實際使用的代碼邏輯類似這樣:
UserModel userModel = userService.query(userId); // userID不存在、不可用等隱藏在異常中 // ... 使用userModel
這種方式明顯優雅、精簡了許多,對于開發效率的提高以及后期維護都有幫助。
但是在坊間有許多流言聲稱,使用異常中斷會影響性能,甚至有人通過簡單的性能測試推出異常中斷的性能耗時比返回Result快幾百倍云云。
性能測試針對性能問題,我也進行了一個簡單的測試,具體測試代碼參見:
https://github.com/sisyphsu/b...
這里使用JMH進行性能測試,說到benchmark,真的是羨慕golang語言自帶的test庫,實在是太方便了。
測試內部的業務邏輯非常簡單,只是調用一次System.currentTimeMillis()并返回long時間戳。
性能測試中分別使用Result
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異常中斷性能損耗嚴重的結論。
分析性能的影響但是需要注意時間單位,只是微秒而已,毫秒的千分之一、秒的百萬分之一。
假設某個微服務單CPU吞吐量為1000QPS,而其中有10%是非法請求,那么異常中斷的性能損耗也只是萬分之一而已,對于服務耗時的影響也只是0.001毫秒而已。
在性能測試中,業務耗時只是獲取系統時間,大概耗時為25ns。正因為如此才顯得異常中斷的性能損耗達到恐怖的“幾百倍”,但是如果業務耗時從25ns變為25us、25ms呢?
再談性能瓶頸我們在分析系統性能時,一定要搞清楚它的數量級以及性能瓶頸,切記陷入性能優化的困境。
舉個粗糙例子,在常規服務中,利用了索引的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等注解,可以讓分布式開發如風一般的快速便捷。在復雜的服務網絡中,業務異常也可以方便開發人員精確地定位錯誤,避免大家順著調用鏈一層一層地追蹤故障點的尷尬情景。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73640.html
摘要:能幫我們解決什么痛點實現異步執行,在未出現前,我們通常是使用嵌套的回調函數來解決的。那么,接下來我們看一下使用的實例可以傳入兩個參數表示兩個狀態的回調函數,第一個是,必選參數第二個是,可選參數的方便之處。 深入理解promise 對于現在的前端同學來說你不同promise你都不好意思出門了。對于前端同學來說promise已經成為了我們的必備技能。 那么,下面我們就來說一說promise...
摘要:你能學到什么如何使用實現異步編程異步編程的原理解析前言結合上一篇文章,我們來聊聊基礎原理說到異步編程,你想到的是和,但那也只是的語法糖而已。表示一個異步操作的最終狀態完成或失敗,以及其返回的值。至此實現異步操作的控制。 你能學到什么 如何使用 Generator + Promise 實現異步編程 異步編程的原理解析 前言 結合 上一篇文章 ,我們來聊聊 Generator 基礎原理...
摘要:數據的層級意味著迭代數據結構并提取它的數據。對于技術人而言技是單兵作戰能力,術則是運用能力的方法。在前端娛樂圈,我想成為一名出色的人民藝術家。 聊聊 for of 說起 for of 相信每個寫過 JavaScript 的人都用過 for of ,平時我們用它做什么呢?大多數情況應該就是遍歷數組了,當然,更多時候,我們也會用 map() 或者 filer() 來遍歷一個數組。 但是就...
閱讀 6865·2021-09-22 15:36
閱讀 5685·2021-09-02 10:20
閱讀 1868·2019-08-30 15:44
閱讀 2652·2019-08-29 14:06
閱讀 1152·2019-08-29 11:17
閱讀 1585·2019-08-26 14:05
閱讀 3092·2019-08-26 13:50
閱讀 1550·2019-08-26 10:26