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

news2024/11/27 21:50:30

作者:陈景明

背景

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

在这里插入图片描述

上述的异常处理相关环节是在 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 新增自定义异常的版本迭代中可以看出,尽管只能新增一个小小的特性,流程下并不复杂,但由于要考虑互通、兼容和协议的设计理念,因此思考和讨论的时间可能比写代码的时间更多。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/143010.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python面向对象(九)

python学习之旅(九) 👍查看更多可以关注查看首页或点击下方专栏目录 一.什么是面向对象 万物皆对象 现实世界的事物都有属性和行为,可在程序中抽离为类来描述现实世界的事物属性和行为。 使用类充当程序内现实事物的“设计图纸”,基于图纸(类)生产实体…

10. 正则表达式匹配

题目链接:https://leetcode.cn/problems/regular-expression-matching/从暴力递归到动态规划,对于状态转移方程不容易推导出来的可以先从递归进行尝试各种策略,最后再从暴力递归转为动态规划,这种尝试方式容易求解dp初始值以及dp更…

数据结构-考研难点代码突破 (图关键路径完全解析(流程+代码) - C++代码)

考研在关键路径上的考察以流程为主 文章目录1. AOE网2. 关键路径问题解决流程C代码1. AOE网 首先区分AOV网&#xff1a; AOV网∶若用DAG 图&#xff08;有向无环图&#xff09;表示一个工程&#xff0c;其顶点表示活动&#xff0c;用有向边<Vi&#xff0c;Vj>表示活动 V…

【ESP32+freeRTOS学习笔记-(五)队列Queue】

目录1、什么是队列Queue2、队列的多任务特性2.1 多任务的访问&#xff1a;2.2 队列读取阻塞&#xff1a;2.3 写队列阻塞&#xff1a;2.4 阻塞于多个队列&#xff1a;3、队列的使用3.1 创建队列--The xQueueCreate() API3.2 写入队列3.3 从队列中接收数据3.4 删除队列4、队列集4…

ReactDOM.render在react源码中执行之后发生了什么?

ReactDOM.render 通常是如下图使用&#xff0c;在提供的 container 里渲染一个 React 元素&#xff0c;并返回对该组件的引用&#xff08;或者针对无状态组件返回 null&#xff09;。本文主要是将ReactDOM.render的执行流程在后续文章中会对创建更新的细节进行分析&#xff0c…

MATLAB-plot3/ezplot3三维绘图

&#xff08;1&#xff09; plot3是三维绘图的基本函数&#xff0c;调用格式如下。1、plot3( X,Y,Z):绘制简单的三维曲线&#xff0c;当X、Y、Z是长度相同的向量时&#xff0c;plot3命令将绘制以向量X、Y、Z为(x, y,z)坐标值的三维曲线;当X、Y、Z是mn矩阵时,plot3命令将绘制m条…

Android 虚拟分区详解(四) 编译开关

Android Virtual A/B 系统简称 VAB,我将其称为虚拟分区。 本系列文章基于 Android R(11) 进行分析,如果没有特别说明,均基于代码版本 android-11.0.0_r46 请已经购买《Android 虚拟分区》专栏的朋友加我 wx 进 "虚拟分区专栏 VIP 答疑"群,作为本专栏文章的附加服…

(6)元对象系统与信号与槽机制

1. 元对象系统 元对象系统是一个基于标准C的扩展&#xff0c;为Qt提供了信号与槽机制、实时类型信息、动态属性系统。 什么是元对象 在计算机科学中&#xff0c;元对象是这样一个东西&#xff1a;它可以操纵、创建、描述、或执行其他对象。元对象描述的对象称为基对象。元对象可…

记一次搭建备库,使用连接串主库无法连接到备库

主库使用连接串连接备库失败 SQL> conn sys/oracleorcldg as sysdba ERROR: ORA-12528: TNS:listener: all appropriate instances are blocking new connections 备库已经建立了静态监听 # listener.ora Network Configuration File: /u01/app/oracle/product/11.2.0/db_1/…

安全寒假第一堂课

一、状态码 200 – 服务器成功返回网页 404 – 请求的网页不存在 503 – 服务器超时 1xx&#xff08;临时响应&#xff09; 表示临时响应并需要请求者继续执行操作的状态码。 100&#xff08;继续&#xff09; 请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一…

OpenCV实战(5)——图像运算详解

OpenCV实战&#xff08;5&#xff09;——图像运算详解0. 前言1. 图像基本运算2. 重载图像运算符2.1 加法运算符重载2.2 分割图像通道2.3 完整代码3. 图像重映射3.1 OpenCV 重映射函数3.2 完整代码小结系列链接0. 前言 图像可以以不同的方式进行组合&#xff0c;因为它们是正则…

XGBoost论文阅读

XGBoost: A Scalable Tree Boosting System 目录 XGBoost: A Scalable Tree Boosting System 1.摘要 2.方法 2.1 正则化学习目标 2.2 梯度提升树 2.3 收缩率和列采样 2.4分裂点查找算法 1.摘要 提出了一种新的稀疏性感知算法&#xff0c;用于稀疏数据和加权全图草图&a…

Python教程:什么是三级模式和二级映像?

美国国家标准学会(American National Standards Institute,ANSI)所属的标准计划与需求委员会&#xff08;Standards Planning and Requirements Committee,SPARC)在1971年公布的研究报告中提出了ANSI-SPARC体系结构&#xff0c;即三级模式结构&#xff08;或称为三层体系结构&a…

ArcGIS基础实验操作100例--实验53导出线、面要素的坐标值

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验53 导出线、面要素的坐标值 目录 一、实验背景 二、实验数据 三、实验步骤 &#xf…

笔记杂项(一)

都是踩过的坑&#xff0c;趟过的水。 ubuntu虚拟机终端字体太小的设置方法&#xff1a;ubuntu18.04调整终端字体大小 这个方法试试看&#xff1a;https://zhuanlan.zhihu.com/p/139305626 容器里面编译内核代码&#xff0c;进程被杀掉的原因是触发了内核OOM killer&#xff0c…

干货| app自动化测试之Andriod微信小程序的自动化测试

随着微信小程序的功能和生态日益完善&#xff0c;很多公司的小程序项目页面结构越来越多&#xff0c;业务逻辑也越来越复杂。如何做好小程序的自动化测试就成为测试同学普遍面临的一大痛点难题。微信小程序小程序内嵌于微信内部&#xff0c;页面包含 Native 原生元素和 Web 元素…

华为防火墙与二层交换机对接配置VLAN上网设置

拓扑图 一、防火墙设置 1、G1/0/0接口设置IP&#xff0c;G1/0/1接口切换二层口设置VLAN&#xff0c;G1/0/0 桥接了本地无线网卡来模拟公网地址 <USG6000V1>sys [USG6000V1]sys FW1 [FW1]un in en# 设置公网IP [FW1]int g1/0/0 [FW1-GigabitEthernet1/0/0]ip addr 192.1…

package.json配置解读之入门

文章目录前言一、描述配置nameversionrepositorydescriptionkeywordslicenseauthor二、文件配置filestypemainbrowsermoduleexportsworkspaces三、脚本配置scriptsconfig四、结语前言 package.json是每个前端项目都会有的json文件&#xff0c;位于项目的根目录中。很多脚手架在…

RHCE(chrony服务器)

chrony服务器 chrony服务器是一个开源自由的网络时间协议NTP的客户端和服务器的软件&#xff0c;他能让计算机保持系统时钟和时钟服务器保持同步&#xff0c;让计算机保持精确的时间&#xff0c;chrony也可以作为服务端软件为其他计算机提供时间同步服务 chrony由两部分组成&…

openAI--十拳剑助你做AI时代的弄潮儿

AI它厉害&#xff08;diao&#xff09;吗&#xff1f; 最近大家玩chatgpt还好吗&#xff1f; 有被它的恋爱情商暴击到吗&#xff1f; 有没有觉得那在leetcode上所向无敌的技巧都是浮云吗&#xff1f; 今天&#xff0c;我为大家带来十个很好的AI平台。这一篇先介绍一下&…