摘要:本文主要講解ACE去霧演算法、暗通道先驗去霧演算法以及霧化生成演算法。 本文分享自華為雲社區《[Python圖像處理] 三十.圖像預處理之圖像去霧詳解(ACE演算法和暗通道先驗去霧演算法)丨【拜托了,物聯網!】》,作者:eastmount 。 一.圖像去霧 隨著社會的發展,環境污染逐漸加劇,越來越多的城 ...
作者:Apache Dubbo Contributor 陳景明
背景
在一些業務場景, 往往需要自定義異常來滿足特定的業務, 主流用法是在catch里拋出異常, 例如:
public void deal() {
try{
//doSomething
...
} catch(IGreeterException e) {
...
throw e;
}
}
或者通過ExceptionBuilder,把相關的異常對象返回給consumer:
provider.send(new ExceptionBuilders.IGreeterExceptionBuilder()
.setDescription('異常描述信息');
在拋出異常後, 通過捕獲和instanceof來判斷特定的異常, 然後做相應的業務處理,例如:
try {
greeterProxy.echo(REQUEST_MSG);
} catch (IGreeterException e) {
//做相應的處理
...
}
在 Dubbo 2.x 版本,可以通過上述方法來捕獲 Provider 端的異常。
而隨著雲原生時代的到來, Dubbo 也開啟了 3.0 的里程碑。
Dubbo 3.0 的一個很重要的目標就是全面擁抱雲原生,
在 3.0 的許多特性中,很重要的一個改動就是支持新的一代Rpc協議Triple。
Triple 協議基於 HTTP 2.0 進行構建,對網關的穿透性強,相容 gRPC,
提供 Request Response、Request Streaming、Response Streaming、
Bi-directional Streaming 等通信模型;
從 Triple 協議開始,Dubbo 還支持基於 IDL 的服務定義。
採用 Triple 協議的用戶可以在 provider 端生成用戶定義的異常信息,
記錄異常產生的堆棧,triple 協議可保證將用戶在客戶端獲取到異常的message。
Triple 的回傳異常會在 AbstractInvoker
的 waitForResultIfSync
中把異常信息堆棧統一封裝成 RpcException
,
所有來自 Provider 端的異常都會被封裝成 RpcException
類型並拋出,
這會導致用戶無法根據特定的異常類型捕獲來自 Provider 的異常,
只能通過捕獲 RpcException 異常來返回信息,
且 Provider 攜帶的異常 message 也無法回傳,只能獲取列印的堆棧信息:
try {
greeterProxy.echo(REQUEST_MSG);
} catch (RpcException e) {
e.printStackTrace();
}
自定義異常信息在社區中的呼聲也比較高,
因此本次改動將支持自定義異常的功能, 使得服務端能拋出自定義異常後被客戶端捕獲到。
Dubbo異常處理簡介
我們從Consumer的角度看一下一次Triple協議 Unary請求的大致流程:
Dubbo Consumer 從 Spring 容器中獲取 bean 時獲取到的是一個代理介面,
在調用介面的方法時會通過代理類遠程調用介面並返回結果。
Dubbo提供的代理工廠類是 ProxyFactory
,通過 SPI 機制預設實現的是 JavassistProxyFactory
,
JavassistProxyFactory
創建了一個繼承自 AbstractProxyInvoker
類的匿名對象,
並重寫了抽象方法 doInvoke
。
重寫後的 doInvoke
只是將調用請求轉發給了 Wrapper
類的 invokeMethod
方法,
並生成 invokeMethod
方法代碼和其他一些方法代碼。
代碼生成完畢後,通過 Javassist
生成 Class
對象,
最後再通過反射創建 Wrapper
實例,隨後通過 InvokerInvocationHandler
-> InvocationUtil
-> AbstractInvoker
-> 具體實現類發送請求到Provider端。
Provider 進行相應的業務處理後返回相應的結果給 Consumer 端,來自 Provider 端的結果會被封裝成 AsyncResult
,在 AbstractInvoker
的具體實現類里,
接受到來自 Provider 的響應之後會調用 appResponse
到 recreate
方法,若 appResponse
里包含異常,
則會拋出給用戶,大體流程如下:
上述的異常處理相關環節是在 Consumer 端,在 Provider 端則是由 org.apache.dubbo.rpc.filter.ExceptionFilter
進行處理,
它是一系列責任鏈 Filter 中的一環,專門用來處理異常。
Dubbo 在 Provider 端的異常會在封裝進 appResponse
中。下麵的流程圖揭示了 ExceptionFilter
源碼的異常處理流程:
而當 appResponse
回到了 Consumer 端,會在 InvocationUtil
里調用 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 通信原理
在上一節中,我們已經介紹了 Dubbo 在 Consumer 端大致發送數據的流程,
可以看到最終依靠的是 AbstractInvoker
的實現類來發送數據。
在 Triple 協議中,AbstractInvoker
的具體實現類是 TripleInvoker
,
TripleInvoker
在發送前會啟動監聽器,監聽來自 Provider 端的響應結果,
並調用 ClientCallToObserverAdapter
的 onNext
方法發送消息,
最終會在底層封裝成 Netty 請求發送數據。
在正式的請求發起前,TripleServer 會註冊 TripleHttp2FrameServerHandler
,
它繼承自 Netty 的 ChannelDuplexHandler
,
其作用是會在 channelRead
方法中不斷讀取 Header 和 Data 信息並解析,
經過層層調用,
會在 AbstractServerCall
的 onMessage
方法里把來自 consumer 的信息流進行反序列化,
並最終由交由 ServerCallToObserverAdapter
的 invoke
方法進行處理。
在 invoke
方法中,根據 consumer 請求的數據調用服務端相應的方法,並非同步等待結果;'
若服務端拋出異常,則調用 onError
方法進行處理,
否則,調用 onReturn
方法返回正常的結果,大致代碼邏輯如下:
public void invoke() {
...
try {
//調用invoke方法請求服務
final Result response = invoker.invoke(invocation);
//非同步等待結果
response.whenCompleteWithContext((r, t) -> {
//若異常不為空
if (t != null) {
//調用方法過程出現異常,調用onError方法處理
responseObserver.onError(t);
return;
}
if (response.hasException()) {
//調用onReturn方法處理業務異常
onReturn(response.getException());
return;
}
...
//正常返回結果
onReturn(r.getValue());
});
}
...
}
大體流程如下:
實現版本
瞭解了上述原理,我們就可以進行相應的改造了,
能讓 consumer 端捕獲異常的關鍵在於把異常對象以及異常信息序列化後再發送給consumer端。
常見的序列化協議很多,例如 Dubbo/HSF 預設的 hessian2 序列化;
還有使用廣泛的 JSON 序列化;以及 gRPC 原生支持的 protobuf(PB) 序列化等等。
Triple協議因為相容grpc的原因,預設採用 Protobuf 進行序列化。
上述提到的這三種典型的序列化方案作用類似,但在實現和開發中略有不同。
PB 不可由序列化後的位元組流直接生成記憶體對象,
而 Hessian 和 JSON 都是可以的。後兩者反序列化的過程不依賴“二方包”,
其序列化和反序列化的代碼由 proto 文件相同,只要客戶端和服務端用相同的 proto 文件進行通信,
就可以構造出通信雙方可解析的結構。
單一的 protobuf 無法序列化異常信息,
因此我們採用 Wrapper + PB
的形式進行序列化異常信息,
抽象出一個 TripleExceptionWrapperUtils
用於序列化異常,
併在 trailer
中採用 TripleExceptionWrapperUtils
序列化異常,大致代碼流程如下:
上面的實現方案看似非常合理,已經能把 Provider 端的異常對象和信息回傳,
併在 Consumer 端進行捕獲。但仔細想想還是有問題的:
通常在 HTTP2 為基礎的通信協議里會對 header 大小做一定的限制,
太大的header size 會導致性能退化嚴重,為了保證性能,
往往以 HTTP2 為基礎的協議在建立連接的時候是要協商最大 header size 的,
超過後會發送失敗。對於 Triple 協議來說,在設計之初就是基於 HTTP 2.0,
能無縫相容 Grpc,而 Grpc header 頭部只有 8KB 大小,
異常對象大小可能超過限制,從而丟失異常信息;
且多一個 header 攜帶序列化的異常信息意味著用戶能加的 header 數量會減少,
擠占了其他 header 所能占用的空間。
經過討論,考慮將異常信息放置在 Body,將序列化後的異常從 trailer 挪至 body,
採用 TripleWrapper + protobuf
進行序列化,把相關的異常信息序列化後回傳。
社區圍繞這個問題進行了一系列的爭論,讀者也可嘗試先思考一下:
1.在 body 中攜帶回傳的異常信息,其對應HTTP header狀態碼該設置為多少?
2.基於 http2 構建的協議,按照主流的 grpc 實現方案,相關的錯誤信息放在 trailer
,理論上不存在body,上層協議也需要保持語義一致性,若此時在payload回傳異常對象,且grpc並沒有支持在Body回傳序列化對象的功能, 會不會破壞Http和grpc協議的語義?從這個角度出發,異常信息更應該放在trailer里。
3.作為開源社區,不能一味滿足用戶的需求,非標準化的用法註定是會被淘汰的,應該儘量避免更改 Protobuf的語義,是否在Wrapper層去支持序列化異常就能滿足需求?
首先回答第二、三個問題:HTTP 協議並沒有約定在狀態碼非 2xx 的時候不能返回 body,返回之後是否讀取取決於用戶。grpc 採用protobuf進行序列化,所以無法返回 exception;且try catch機製為java獨有,其他語言並沒有對應的需求,但Grpc暫時不支持的功能並一定是unimplemented,Dubbo的設計目標之一是希望能和主流協議甚至架構進行對齊,但對於用戶合理的需求也希望能進行一定程度的修改。且從throw本身的語義出發,throw 的數據不只是一個 error message,序列化的異常信息帶有業務屬性,根據這個角度,更不應該採用類似trailer的設計。至於單一的Wrapper層,也沒辦法和grpc進行互通。至於Http header狀態碼設置為200,因為其返回的異常信息已經帶有一定的業務屬性,不再是單純的error,這個設計也與grpc保持一致,未來考慮網關採集可以增加新的triple-status。
更改後的版本只需在異常不為空時返回相關的異常信息,採用 TripleWrapper + Protobuf
進行序列化異常信息,併在consumer端進行解析和反序列化,大體流程如下:
總結
通過對 Dubbo 3.0 新增自定義異常的版本迭代中可以看出,儘管只能新增一個小小的特性,流程下並不複雜,但由於要考慮互通、相容和協議的設計理念,因此思考和討論的時間可能比寫代碼的時間更多。
歡迎在 https://github.com/apache/dubbo 給 Dubbo Star。
搜索關註官方微信公眾號:Apache Dubbo,瞭解更多業界最新動態,掌握大廠面試必備 Dubbo 技能