共计 5099 个字符,预计需要花费 13 分钟才能阅读完成。
作者: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 技能