Triple協(xié)議支持Java異?;貍髟O計實現(xiàn)詳解
背景
在一些業(yè)務場景, 往往需要自定義異常來滿足特定的業(yè)務, 主流用法是在catch里拋出異常, 例如:
public void deal() { try{ //doSomething ... } catch(IGreeterException e) { ... throw e; } }
或者通過ExceptionBuilder,把相關(guān)的異常對象返回給consumer:
provider.send(new ExceptionBuilders.IGreeterExceptionBuilder() .setDescription('異常描述信息');
在拋出異常后, 通過捕獲和instanceof來判斷特定的異常, 然后做相應的業(yè)務處理,例如:
try { greeterProxy.echo(REQUEST_MSG); } catch (IGreeterException e) { //做相應的處理 ... }
在 Dubbo 2.x 版本,可以通過上述方法來捕獲 Provider 端的異常。 而隨著云原生時代的到來, Dubbo 也開啟了 3.0 的里程碑。
Dubbo 3.0 的一個很重要的目標就是全面擁抱云原生, 在 3.0 的許多特性中,很重要的一個改動就是支持新的一代Rpc協(xié)議Triple。
Triple 協(xié)議基于 HTTP 2.0 進行構(gòu)建,對網(wǎng)關(guān)的穿透性強,兼容 gRPC, 提供 Request Response、Request Streaming、Response Streaming、 Bi-directional Streaming 等通信模型; 從 Triple 協(xié)議開始,Dubbo 還支持基于 IDL 的服務定義。
采用 Triple 協(xié)議的用戶可以在 provider 端生成用戶定義的異常信息, 記錄異常產(chǎn)生的堆棧,triple 協(xié)議可保證將用戶在客戶端獲取到異常的message。
Triple 的回傳異常會在 AbstractInvoker
的 waitForResultIfSync
中把異常信息堆棧統(tǒng)一封裝成 RpcException
, 所有來自 Provider 端的異常都會被封裝成 RpcException
類型并拋出, 這會導致用戶無法根據(jù)特定的異常類型捕獲來自 Provider 的異常, 只能通過捕獲 RpcException 異常來返回信息, 且 Provider 攜帶的異常 message 也無法回傳,只能獲取打印的堆棧信息:
try { greeterProxy.echo(REQUEST_MSG); } catch (RpcException e) { e.printStackTrace(); }
自定義異常信息在社區(qū)中的呼聲也比較高, 因此本次改動將支持自定義異常的功能, 使得服務端能拋出自定義異常后被客戶端捕獲到。
Dubbo異常處理簡介
我們從Consumer的角度看一下一次Triple協(xié)議 Unary請求的大致流程:
Dubbo Consumer 從 Spring 容器中獲取 bean 時獲取到的是一個代理接口, 在調(diào)用接口的方法時會通過代理類遠程調(diào)用接口并返回結(jié)果。
Dubbo提供的代理工廠類是 ProxyFactory
,通過 SPI 機制默認實現(xiàn)的是 JavassistProxyFactory
, JavassistProxyFactory
創(chuàng)建了一個繼承自 AbstractProxyInvoker
類的匿名對象, 并重寫了抽象方法 doInvoke
。 重寫后的 doInvoke
只是將調(diào)用請求轉(zhuǎn)發(fā)給了 Wrapper
類的 invokeMethod
方法, 并生成 invokeMethod
方法代碼和其他一些方法代碼。
代碼生成完畢后,通過 Javassist
生成 Class
對象, 最后再通過反射創(chuàng)建 Wrapper
實例,隨后通過 InvokerInvocationHandler
-> InvocationUtil
-> AbstractInvoker
-> 具體實現(xiàn)類發(fā)送請求到Provider端。
Provider 進行相應的業(yè)務處理后返回相應的結(jié)果給 Consumer 端,來自 Provider 端的結(jié)果會被封裝成 AsyncResult
,在 AbstractInvoker
的具體實現(xiàn)類里, 接受到來自 Provider 的響應之后會調(diào)用 appResponse
到 recreate
方法,若 appResponse
里包含異常, 則會拋出給用戶,大體流程如下:
上述的異常處理相關(guān)環(huán)節(jié)是在 Consumer 端,在 Provider 端則是由 org.apache.dubbo.rpc.filter.ExceptionFilter
進行處理, 它是一系列責任鏈 Filter 中的一環(huán),專門用來處理異常。
Dubbo 在 Provider 端的異常會在封裝進 appResponse
中。下面的流程圖揭示了 ExceptionFilter
源碼的異常處理流程:
而當 appResponse
回到了 Consumer 端,會在 InvocationUtil
里調(diào)用 AppResponse
的 recreate
方法拋出異常, 最終可以在 Consumer 端捕獲:
public Object recreate() throws Throwable { if (exception != null) { try { Object stackTrace = exception.getStackTrace(); if (stackTrace == null) { exception.setStackTrace(new StackTraceElement[0]); } } catch (Exception e) { // ignore } throw exception; } return result; }
Triple 通信原理
在上一節(jié)中,我們已經(jīng)介紹了 Dubbo 在 Consumer 端大致發(fā)送數(shù)據(jù)的流程, 可以看到最終依靠的是 AbstractInvoker
的實現(xiàn)類來發(fā)送數(shù)據(jù)。 在 Triple 協(xié)議中,AbstractInvoker
的具體實現(xiàn)類是 TripleInvoker
, TripleInvoker
在發(fā)送前會啟動監(jiān)聽器,監(jiān)聽來自 Provider 端的響應結(jié)果, 并調(diào)用 ClientCallToObserverAdapter
的 onNext
方法發(fā)送消息, 最終會在底層封裝成 Netty 請求發(fā)送數(shù)據(jù)。
在正式的請求發(fā)起前,TripleServer 會注冊 TripleHttp2FrameServerHandler
, 它繼承自 Netty 的 ChannelDuplexHandler
, 其作用是會在 channelRead
方法中不斷讀取 Header 和 Data 信息并解析, 經(jīng)過層層調(diào)用, 會在 AbstractServerCall
的 onMessage
方法里把來自 consumer 的信息流進行反序列化, 并最終由交由 ServerCallToObserverAdapter
的 invoke
方法進行處理。
在 invoke
方法中,根據(jù) consumer 請求的數(shù)據(jù)調(diào)用服務端相應的方法,并異步等待結(jié)果;' 若服務端拋出異常,則調(diào)用 onError
方法進行處理, 否則,調(diào)用 onReturn
方法返回正常的結(jié)果,大致代碼邏輯如下:
public void invoke() { ... try { //調(diào)用invoke方法請求服務 final Result response = invoker.invoke(invocation); //異步等待結(jié)果 response.whenCompleteWithContext((r, t) -> { //若異常不為空 if (t != null) { //調(diào)用方法過程出現(xiàn)異常,調(diào)用onError方法處理 responseObserver.onError(t); return; } if (response.hasException()) { //調(diào)用onReturn方法處理業(yè)務異常 onReturn(response.getException()); return; } ... //正常返回結(jié)果 onReturn(r.getValue()); }); } ... }
大體流程如下:
實現(xiàn)版本
了解了上述原理,我們就可以進行相應的改造了, 能讓 consumer 端捕獲異常的關(guān)鍵在于把異常對象以及異常信息序列化后再發(fā)送給consumer端。 常見的序列化協(xié)議很多,例如 Dubbo/HSF 默認的 hessian2 序列化; 還有使用廣泛的 JSON 序列化;以及 gRPC 原生支持的 protobuf(PB) 序列化等等。 Triple協(xié)議因為兼容grpc的原因,默認采用 Protobuf 進行序列化。 上述提到的這三種典型的序列化方案作用類似,但在實現(xiàn)和開發(fā)中略有不同。 PB 不可由序列化后的字節(jié)流直接生成內(nèi)存對象, 而 Hessian 和 JSON 都是可以的。后兩者反序列化的過程不依賴“二方包”, 其序列化和反序列化的代碼由 proto 文件相同,只要客戶端和服務端用相同的 proto 文件進行通信, 就可以構(gòu)造出通信雙方可解析的結(jié)構(gòu)。
單一的 protobuf 無法序列化異常信息, 因此我們采用 Wrapper + PB
的形式進行序列化異常信息, 抽象出一個 TripleExceptionWrapperUtils
用于序列化異常, 并在 trailer
中采用 TripleExceptionWrapperUtils
序列化異常,大致代碼流程如下:
上面的實現(xiàn)方案看似非常合理,已經(jīng)能把 Provider 端的異常對象和信息回傳, 并在 Consumer 端進行捕獲。但仔細想想還是有問題的: 通常在 HTTP2 為基礎的通信協(xié)議里會對 header 大小做一定的限制, 太大的header size 會導致性能退化嚴重,為了保證性能, 往往以 HTTP2 為基礎的協(xié)議在建立連接的時候是要協(xié)商最大 header size 的, 超過后會發(fā)送失敗。對于 Triple 協(xié)議來說,在設計之初就是基于 HTTP 2.0, 能無縫兼容 Grpc,而 Grpc header 頭部只有 8KB 大小, 異常對象大小可能超過限制,從而丟失異常信息; 且多一個 header 攜帶序列化的異常信息意味著用戶能加的 header 數(shù)量會減少, 擠占了其他 header 所能占用的空間。
經(jīng)過討論,考慮將異常信息放置在 Body,將序列化后的異常從 trailer 挪至 body, 采用 TripleWrapper + protobuf
進行序列化,把相關(guān)的異常信息序列化后回傳。 社區(qū)圍繞這個問題進行了一系列的爭論,讀者也可嘗試先思考一下:
1.在 body 中攜帶回傳的異常信息,其對應HTTP header狀態(tài)碼該設置為多少?
2.基于 http2 構(gòu)建的協(xié)議,按照主流的 grpc 實現(xiàn)方案,相關(guān)的錯誤信息放在 trailer
,理論上不存在body,上層協(xié)議也需要保持語義一致性,若此時在payload回傳異常對象,且grpc并沒有支持在Body回傳序列化對象的功能, 會不會破壞Http和grpc協(xié)議的語義?從這個角度出發(fā),異常信息更應該放在trailer里。
3.作為開源社區(qū),不能一味滿足用戶的需求,非標準化的用法注定是會被淘汰的,應該盡量避免更改 Protobuf的語義,是否在Wrapper層去支持序列化異常就能滿足需求?
首先回答第二、三個問題:HTTP 協(xié)議并沒有約定在狀態(tài)碼非 2xx 的時候不能返回 body,返回之后是否讀取取決于用戶。grpc 采用protobuf進行序列化,所以無法返回 exception;且try catch機制為java獨有,其他語言并沒有對應的需求,但Grpc暫時不支持的功能并一定是unimplemented,Dubbo的設計目標之一是希望能和主流協(xié)議甚至架構(gòu)進行對齊,但對于用戶合理的需求也希望能進行一定程度的修改。且從throw本身的語義出發(fā),throw 的數(shù)據(jù)不只是一個 error message,序列化的異常信息帶有業(yè)務屬性,根據(jù)這個角度,更不應該采用類似trailer的設計。至于單一的Wrapper層,也沒辦法和grpc進行互通。至于Http header狀態(tài)碼設置為200,因為其返回的異常信息已經(jīng)帶有一定的業(yè)務屬性,不再是單純的error,這個設計也與grpc保持一致,未來考慮網(wǎng)關(guān)采集可以增加新的triple-status。
更改后的版本只需在異常不為空時返回相關(guān)的異常信息,采用 TripleWrapper + Protobuf
進行序列化異常信息,并在consumer端進行解析和反序列化,大體流程如下:
總結(jié)
通過對 Dubbo 3.0 新增自定義異常的版本迭代中可以看出,盡管只能新增一個小小的特性,流程下并不復雜,但由于要考慮互通、兼容和協(xié)議的設計理念,因此思考和討論的時間可能比寫代碼的時間更多。
以上就是Triple協(xié)議支持Java異常回傳設計實現(xiàn)詳解的詳細內(nèi)容,更多關(guān)于Java異?;貍鱐riple協(xié)議的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot WebSocket實時監(jiān)控異常的詳細流程
最近做了一個需求,消防的設備巡檢,如果巡檢發(fā)現(xiàn)異常,通過手機端提交,后臺的實時監(jiān)控頁面實時獲取到該設備的信息及位置,然后安排員工去處理。這篇文章主要介紹了SpringBoot WebSocket實時監(jiān)控異常的全過程,感興趣的朋友一起看看吧2021-10-10Java Swing組件實現(xiàn)進度監(jiān)視功能示例
這篇文章主要介紹了Java Swing組件實現(xiàn)進度監(jiān)視功能,結(jié)合完整實例形式詳細分析了Java基于Swing組件實現(xiàn)進度條顯示功能的具體操作技巧與相關(guān)注意事項,需要的朋友可以參考下2018-02-02基于Protobuf動態(tài)解析在Java中的應用 包含例子程序
下面小編就為大家?guī)硪黄赑rotobuf動態(tài)解析在Java中的應用 包含例子程序。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07Java8中forEach語句循環(huán)一個List和Map
這篇文章主要給大家介紹了關(guān)于Java8中forEach語句循環(huán)一個List和Map的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-02-02