作者:陈景明

布景

在一些事务场景,往往需求自界说反常来满意特定的事务,干流用法是在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) {
    //做相应的处理
    ...
}

在 dubbo2.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();
    }

自界说反常信息在社区中的呼声也比较高,因而本次改动将支撑自界说反常的功能, 使得服务端能抛出自界说反常后被客户端捕获到,至于 ExceptionBuilder 并不是干流的用法,因而不予支撑。

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 端的成果会被封装成 AyncResult,在 AbstractInvoker 的详细完成类里,接受到来自 Provider 的呼应之后会调用 appResponse 到 recreate 办法,若 appResponse 里包含反常,则会抛出给用户,大体流程如下:

Triple 协议支持 Java 异常回传的设计与实现

上述的反常处理相关环节是在 Consumer 端,在 Provider 端则是由 org.apache.dubbo.rpc.filter.ExceptionFilter 进行处理,它是一系列责任链Filter中的一环,专门用来处理反常。Dubbo 在 Provider 端的反常会在封装进 appResponse 中。下面的流程图揭示了ExceptionFilter源码的反常处理流程:

Triple 协议支持 Java 异常回传的设计与实现

而当 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());
        });
    } 
    ...
}

大体流程如下:

Triple 协议支持 Java 异常回传的设计与实现

完成版别

了解了上述原理,咱们就能够进行相应的改造了,能让 consumer 端捕获反常的关键在于把反常目标以及反常信息序列化后再发送给 consumer 端。常见的序列化协议许多,例如 Dubbo/HSF 默许的 hessian2 序列化;还有运用广泛的 JSON 序列化;以及 gRPC 原生支撑的 protobuf(PB) 序列化等等。Triple 协议因为兼容 grpc 的原因,默许选用 Protobuf 进行序列化。上述说到的这三种典型的序列化计划作用相似,但在完成和开发中略有不同。PB 不可由序列化后的字节流直接生成内存目标,而 Hessian 和 JSON 都是能够的。后两者反序列化的进程不依赖“二方包”,其序列化和反序列化的代码由 proto 文件相同,只要客户端和服务端用相同的 proto 文件进行通讯,就能够构造出通讯双方可解析的结构。单一的 protobuf 无法序列化反常信息,因而咱们选用 Wrapper + PB 的形式进行序列化反常信息,笼统出一个 TripleExceptionWrapperUtils 用于序列化反常,并在 trailer 中选用 TripleExceptionWrapperUtils 序列化反常,大致代码流程如下:

Triple 协议支持 Java 异常回传的设计与实现

上面的完成计划看似十分合理,现已能把 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 端进行解析和反序列化,大体流程如下:

Triple 协议支持 Java 异常回传的设计与实现

总结

通过对 Dubbo 3.0 新增自界说反常的版别迭代中能够看出,虽然只能新增一个小小的特性,流程下并不复杂,但由于要考虑互通、兼容和协议的规划理念,因而思考和评论的时刻可能比写代码的时刻更多。