乐趣区

关于后端:Triple-协议支持-Java-异常回传的设计与实现

作者: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 的回传异样会在 AbstractInvokerwaitForResultIfSync
中把异样信息堆栈对立封装成 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 的响应之后会调用 appResponserecreate 办法,若 appResponse 里蕴含异样,
则会抛出给用户,大体流程如下:

上述的异样解决相干环节是在 Consumer 端,在 Provider 端则是由 org.apache.dubbo.rpc.filter.ExceptionFilter 进行解决,
它是一系列责任链 Filter 中的一环,专门用来解决异样。

Dubbo 在 Provider 端的异样会在封装进 appResponse 中。上面的流程图揭示了 ExceptionFilter 源码的异样解决流程:

而当 appResponse 回到了 Consumer 端,会在 InvocationUtil 里调用 AppResponserecreate 办法抛出异样,
最终能够在 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 端的响应后果,
并调用 ClientCallToObserverAdapteronNext 办法发送音讯,
最终会在底层封装成 Netty 申请发送数据。

在正式的申请发动前,TripleServer 会注册 TripleHttp2FrameServerHandler
它继承自 Netty 的 ChannelDuplexHandler
其作用是会在 channelRead 办法中一直读取 Header 和 Data 信息并解析,
通过层层调用,
会在 AbstractServerCallonMessage 办法里把来自 consumer 的信息流进行反序列化,
并最终由交由 ServerCallToObserverAdapterinvoke 办法进行解决。

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 技能

退出移动版