同步自: https://sulin.me/2019/T2ZXZB....
在分布式系统开发中,咱们常常须要将各类各样的状态码、错误信息传递给最外层的调用方,这个调用方一般是http/api
接口,错误信息好比登陆失效
、参数错误
等等。html
最外层接口暴露的数据一般是相似于{code, msg, data}
这样的json
格式,这一点没有任何争议。java
可是分布式系统的节点之间RPC调用、节点内部方法调用中,一般会用ServiceException
或Result<T>
的方式进行错误信息的传递,这两种方式有什么区别以及孰优孰劣呢?本文侧重于开发效率
与系统性能
探讨这个问题。git
Result<T>
介绍这是一种比较常见的错误信息传递方式,某些大厂甚至直接将它们设为技术规范,强制各个团队采用这种方式。常见的Result模板以下:github
@Data public class Result<T> { private int code; // 也能够是String等 private String msg; private T data; }
在系统开发中的应用一般是这样的:golang
Result<UserModel> userModelResult = userService.query(userId); if (!userModelResult.isSuccess() || userModelResult.getData != null) { return Result.fail(userModelResult); // 透传错误 } UserModel userModel = userModelResult.getData(); if (userModel.getStatus() != UserStatusEnum.NORMAL) { return Result.fail("user unavaliable"); // 用户不可用 } // ... 正常使用UserModel
在比较复杂的分布式微服务环境中,相似的代码很是之多,每一个依赖服务的调用都伴随着一段相似的容错逻辑。算法
这种模式比较相似Golang
语言中的错误码处理,这也是Golang比较被人诟病的地方,即每一步都得进行错误判断。json
更残酷的现实是,尽管有了Result
封装,可是仍然会有后端系统的Exception
透传过来。在我接触过的实际应用中,这种突破Result
封装的异常透传绝非个例,我本身负责的系统在调用更后端的国内最强交易系统时,就曾接到过最内部交易中心TC的业务异常,排查问题时追踪的团队就有不止5个。后端
ServiceException
介绍顾名思义,这个方式就是使用异常中断
将正常逻辑与异常逻辑进行拆分。api
在系统开发中,大部分错误都须要直接中断服务,直接将错误反馈给用户,正由于如此,咱们在使用Result<T>
时,常常须要写相似if(result.isFail()){return…}
这样的代码。而使用ServiceException
,咱们就能够省略掉绝大部分相似的代码。性能优化
一般ServiceException
能够这样定义:
@Getter public class ServiceException extends RuntimeException { private final int code; private final String msg; public ApiException() { this(-1, null); } public ApiException(Code code) { this(code, null); } public ApiException(Code code, String msg) { super(msg); this.code = code; this.msg = msg; } }
系统内部组件在遇到数据缺失
、越权访问
、登陆失效
、帐户锁定
等异常状况时,直接抛出ServiceException
中断逻辑,而后由最外层的Filter
或Aspect
捕捉异常,提取其中的code
和msg
返回给用户。
实际使用的代码逻辑相似这样:
UserModel userModel = userService.query(userId); // userID不存在、不可用等隐藏在异常中 // ... 使用userModel
这种方式明显优雅、精简了许多,对于开发效率的提升以及后期维护都有帮助。
可是在坊间有许多流言声称,使用异常中断会影响性能,甚至有人经过简单的性能测试推出异常中断的性能耗时比返回Result快几百倍云云。
针对性能问题,我也进行了一个简单的测试,具体测试代码参见:
https://github.com/sisyphsu/b...
这里使用JMH
进行性能测试,说到benchmark
,真的是羡慕golang
语言自带的test
库,实在是太方便了。
测试内部的业务逻辑很是简单,只是调用一次System.currentTimeMillis()
并返回long
时间戳。
性能测试中分别使用Result<T>
返回值以及抛出Exception
,针对抛出异常的性能测试,又增长的不一样深度的调用栈测试,这是由于Java
在抛出异常时,须要分析当前Thread
的栈,而调用栈越深,所形成的性能损耗就越大。具体栈深度取值为一、十、100:
Test.test avgt 5 0.027 ± 0.001 us/op Test.testException avgt 5 1.060 ± 0.045 us/op Test.testDeep10Exception avgt 5 1.826 ± 0.122 us/op Test.testDeep100Exception avgt 5 9.802 ± 0.411 us/op
乍一看,异常栈深度为100的性能损耗确实是普通方法调用的360倍,有的人也确实是基于这种理由得出Java异常中断性能损耗严重的结论。
可是须要注意时间单位,只是微秒而已,毫秒的千分之1、秒的百万分之一。
假设某个微服务单CPU吞吐量为1000QPS,而其中有10%是非法请求,那么异常中断的性能损耗也只是万分之一而已,对于服务耗时的影响也只是0.001毫秒而已。
在性能测试中,业务耗时
只是获取系统时间,大概耗时为25ns
。正由于如此才显得异常中断的性能损耗达到恐怖的“几百倍”,可是若是业务耗时从25ns
变为25us
、25ms
呢?
咱们在分析系统性能时,必定要搞清楚它的数量级以及性能瓶颈,切记陷入性能优化的困境。
举个粗糙例子,在常规服务中,利用了索引的DB操做在1~10毫秒之间,访问分布式Cache的耗时在3~30毫秒之间,微服务RPC的网络性能损耗在3~10毫秒之间,客户端与服务器之间的网络耗时在5~300毫秒之间,如此之类等等。在这种状况下,优化0.001毫秒的性能隐患无异于捡了芝麻丢了西瓜。
我曾经写过相似TCP的底层网络协议,在那种高频场景中,算法优化带来0.1微秒的性能优化就意味着每秒钟吞吐量几成甚至几倍的提高,可是在分布式调用的低频场景中,这种性能用处没有任何用处。
另一个例子,几年前我和同事在讨论DB数据表设计时,由于订单状态使用什么长度的int
而争执的面红脖子粗,如今想一想,订单状态上优化的1个字节,终年累月下来也只是节省不到1MB的磁盘空间而已,有什么用呢?
对于使用Dubbo、HSF这种远程调用框架而言,使用异常中断进行错误信息传递,须要注意一点就是,异常类型须要设计为通用的,即各个微服务都引用的基础类型。
在某厂的技术规范中有说到:
1) 使用抛异常返回方式,调用方若是没有捕获到就会产生运行时错误。2) 若是不加栈信息,只是new自定义异常,加入本身的理解的error message,对于调用端解决问题的帮助不会太多。若是加了栈信息,在频繁调用出错的状况下,数据序列化和传输的性能损耗也是问题。
我对这种技术规范至关的不觉得然。
首先业务异常原本就须要调用方透传给最外层,诸如数据不存在
、登陆失效
、用户锁定
这种异常,中间的调用方捕获了也每每没有什么用。
其次又是鬼扯性能损耗
,这种低频的数据序列化和内网传输会有什么样的性能损耗呢?栈信息透传给调用方也有益于故障排查,我曾经接到过TC的异常栈信息,根据栈中的package直接就绕过三四层找到了底层出错的地方,能够说是节省了大量的时间。
在分布式微服务中,采用异常中断能够大幅精简业务代码,对于性能的影响也微乎其微。
辅助以@NotNull
、@Nullable
等注解,可让分布式开发如风通常的快速便捷。在复杂的服务网络中,业务异常也能够方便开发人员精确地定位错误,避免你们顺着调用链一层一层地追踪故障点的尴尬情景。