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

简介: 通过对 Dubbo 3.0 新增自定义异常的版本迭代中可以看出,尽管只能新增一个小小的特性,流程下并不复杂,但由于要考虑互通、兼容和协议的设计理念,因此思考和讨论的时间可能比写代码的时间更多。

背景

   在一些业务场景, 往往需要自定义异常来满足特定的业务, 主流用法是在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里包含异常,则会抛出给用户,大体流程如下


image.png


上述的异常处理相关环节是在Consumer端,在Provider端则是由org.apache.dubbo.rpc.filter.ExceptionFilter进行处理,它是一系列责任链Filter中的一环,专门用来处理异常。Dubbo在Provider端的异常会在封装进appResponse中。


下面的流程图揭示了ExceptionFilter源码的异常处理流程:


image.png


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


大体流程如下:

image.png



实现版本

   了解了上述原理,我们就可以进行相应的改造了,能让consumer端捕获异常的关键在于把异常对象以及异常信息序列化后再发送给consumer端


常见的序列化协议很多:

例如 Dubbo/HSF 默认的 hessian2 序列化;

还有使用广泛的 JSON 序列化;

以及 gRPC 原生支持的 protobuf(PB) 序列化等等。


Triple协议因为兼容grpc的原因,默认采用Protobuf进行序列化。


上述提到的这三种典型的序列化方案作用类似,但在实现和开发中略有不同。PB 不可由序列化后的字节流直接生成内存对象,而Hessian和JSON都是可以的。后两者反序列化的过程不依赖“二方包”,其序列化和反序列化的代码由proto文件相同,只要客户端和服务端用相同的proto文件进行通信,就可以构造出通信双方可解析的结构。


单一的protobuf无法序列化异常信息,因此我们采用Wrapper + PB的形式进行序列化异常信息,抽象出一个TripleExceptionWrapperUtils用于序列化异常,并在trailer中采用TripleExceptionWrapperUtils序列化异常,大致代码流程如下:


image.png


上面的实现方案看似非常合理,已经能把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端进行解析和反序列化,大体流程如下:


image.png


总结

  通过对 Dubbo 3.0 新增自定义异常的版本迭代中可以看出,尽管只能新增一个小小的特性,流程下并不复杂,但由于要考虑互通、兼容和协议的设计理念,因此思考和讨论的时间可能比写代码的时间更多。

相关文章
|
JavaScript 安全 Java
谈谈UDP、HTTP、SSL、TLS协议在java中的实际应用
下面我将详细介绍UDP、HTTP、SSL、TLS协议及其工作原理,并提供Java代码示例(由于Deno是一个基于Node.js的运行时,Java代码无法直接在Deno中运行,但可以通过理解Java示例来类比Deno中的实现)。
415 1
|
消息中间件 Java Kafka
【Azure 事件中心】在微软云中国区 (Mooncake) 上实验以Apache Kafka协议方式发送/接受Event Hubs消息 (Java版)
【Azure 事件中心】在微软云中国区 (Mooncake) 上实验以Apache Kafka协议方式发送/接受Event Hubs消息 (Java版)
596 1
|
缓存 网络协议 算法
(二)Java网络编程之爆肝HTTP、HTTPS、TLS协议及对称与非对称加密原理!
作为一名程序员,尤其是Java程序员,那必须得了解并掌握HTTP/HTTPS相关知识。因为在如今计算机网络通信中,HTTP协议的作用功不可没,无论是日常上网追剧、冲���、亦或是接口开发、调用等,必然存在HTTP的“影子”在内。尤其对于WEB开发者而言,HTTP几乎是每天会打交道的东西。
855 10
|
网络协议 Java 数据处理
(一)Java网络编程之计网基础、TCP-IP协议簇、TCP、UDP协议及腾讯QQ通信原理综述
就目前而言,多数网络编程的系列的文章都在围绕着计算机网络体系进行阐述,但其中太多理论概念,对于大部分开发者而言,用途甚微。因此,在本系列中则会以实际开发者的工作为核心,从Java程序员的角度出发,详细解读Java的网络编程核心内容。
689 0
|
Java API 开发者
深入理解APDU协议与Java开发
深入理解APDU协议与Java开发
1368 0
|
网络协议 安全 Java
Java网络编程入门涉及TCP/IP协议理解与Socket通信。
【6月更文挑战第21天】Java网络编程入门涉及TCP/IP协议理解与Socket通信。TCP/IP协议包括应用层、传输层、网络层和数据链路层。使用Java的`ServerSocket`和`Socket`类,服务器监听端口,接受客户端连接,而客户端连接指定服务器并交换数据。基础示例展示如何创建服务器和发送消息。进阶可涉及多线程、NIO和安全传输。学习这些基础知识能助你构建网络应用。
301 1
|
网络协议 Java
在Java中使用TCP协议搭建一个简单的客户端
如何在Java中使用TCP协议搭建一个简单的客户端
688 2
|
消息中间件 Java
Java一分钟之-RabbitMQ:AMQP协议实现
【6月更文挑战第11天】RabbitMQ是基于AMQP协议的开源消息队列服务,支持多种消息模式。本文介绍了RabbitMQ的核心概念:生产者、消费者、交换器、队列和绑定,以及常见问题和解决方案。例如,通过设置消息持久化和确认机制防止消息丢失,配置死信队列处理不可消费消息,以及妥善管理资源防止泄漏。还提供了Java代码示例,帮助读者理解和使用RabbitMQ。通过理解这些基础和最佳实践,可以提升RabbitMQ在分布式系统中的可靠性和效率。
476 0
Java一分钟之-RabbitMQ:AMQP协议实现
|
网络协议 Java API
网络编程套接字(4)——Java套接字(TCP协议)
网络编程套接字(4)——Java套接字(TCP协议)
374 0
|
Java 程序员 Linux
网络编程套接字(3)——Java数据报套接字(UDP协议)
网络编程套接字(3)——Java数据报套接字(UDP协议)
391 0