rpc 服务中,“业务错误”的返回应该如何设计?

76 天前
 justdoit123

是返回类似下面的业务错误码结构,

{
    "rc": 10001,
    "msg": "部分商品没有库存",
    "data": {
        "out_of_stock_ids": [
            1,
            2,
            3
        ]
    }
}

还是抛出下面这样的自定义异常?

class BaseBusinessError(Exception):
    rc = 500
    msg = 'Unknown error'
    data = None

    def __repr__(self):
        return f'<Error {self.rc}: {self.msg}. data: {self.data}>'

    def __str__(self):
        return self.__repr__()


class ShopOutOfStockError(BaseBusinessError):
    rc = 10001
    msg = "部分商品没有库存"
    data = None

    def __init__(self, product_ids: list):
        self.data = product_ids

如果采用异常方案,调用方没有自定义异常类的定义怎么办?如何反序列化?是不是要共享异常的定义代码给所有调用方?要是调用方是其它语言呢?

这篇博客 下的这句:

查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 try...catch ,并且不能进行有效处理。

中的 checked 异常是什么意思?是否可以理解为查询方法不抛异常。比如,查询某个数据没有权限,直接返回空,而不是抛一个没有权限的异常。

573 次点击
所在节点    Coding
8 条回复
timethinker
76 天前
在 Java 中,受检(checked)异常指的是必须要用 try..catch 来调用一个可能会抛出受检异常的方法。python 中应该所有的都是非受检异常,也就是不强迫你使用 try..catch 。

言归正传,不要把异常跟传输搞混了,RPC 本质上也是通过网络传递数据,至于怎么处理这个数据,你可以参考一下 HTTP 协议,返回不同的状态码来表示协议级别的成功与否。或者不需要状态码,不管成功与否永远返回一个就像你发的那样的数据结构,然后再进一步根据数据里面的 code 再决定是否要抛出异常。
justdoit123
76 天前
@timethinker 感谢~
justdoit123
76 天前
话说,才发现 coding 节点算是 CodingNet 的营销节点。。。
povsister
76 天前
通常对于 rpc 来说,其框架已经定义过了错误,并且提供了错误的扩展(描述、details 等),就不要再把“业务错误”包装成正常响应返回了,否则你在可观测上会遇到不小麻烦。

而且,看你的设计,你在异常里,又返回了 data ,并且还描述了业务相关数据( out of stock ids ),那么你本质就不是想返回错误,而是告诉请求方你提交的哪些数据不合法。
这时候你就要反思你的设计:你究竟是想交互,还是想抛出错误?对于一个错误来说你返回的信息里包含了太多业务场景相关信息,没有通用性。应该考虑这是一次正常交互。

错误描述的扩展,应该是描述错误本身,首先,应该提供一个统一注册管理的 bizErrorCode ,这个是可观测的基础,同时应该提供 description ,这个是对错误的描述,提供一些场景、debug 信息,而且仅应该用于 localization 或者程序员调试使用。
justdoit123
75 天前
@povsister

我可不可以这样理解:

1. 对于我描述的这类异常,应该属于 “业务异常”。说是异常,其实是正常的数据交互。类似的还有,邮箱已被使用、账号或密码不存在等。这些不属于程序错误,监控也不需要关心这些业务异常。

2. 还存在一类异常,为了区分称为“程序错误”吧。这些是程序员真正需要关心的,需要有你说的描述、场景、debug 信息。我能想到的典型,可能是 “配置不存在”、验证 Token 解析失败这类错误。总之是不符合预期的、需要监控关心的问题。
justdoit123
75 天前
回到这个 “业务异常” 上来。我在 rpc 服务供应端实现的时候,是不是可以通过抛出异常然后在出口统一拦截这类业务异常转成正常数据交换的实现方式?当然,这类所谓的“业务异常”并不是程序错误,就不需要上报监控了,不混入其它的错误监控中去。
povsister
75 天前
@justdoit123
粗略意义上正确,不过各家有各家的做法,rpc 的错误机制设计是要和可观测+trace+SLO 大盘联合起来考虑的。

业务错误不代表监控不需要关心,简单来说:
如果你只需要拒绝服务+返回错误,并且需要有一个自定义的 errcode 去表示这是什么类型的错误,同时附带一个简短的 readable 的说明(可以理解为 errcode 作为程序使用的描述,可以依赖 errcode 进一步执行某些程序化步骤,readable 的描述作为程序员阅读使用,方便快速识别问题),除此之外不需要向调用方传递更多的错误内容,那么就适合用 rpc error 来表示。
如果你要向调用方提供更多的关于错误的具体信息,深入绑定业务场景,那么就不适合当做 err 来处理,应该作为一个正常的 rpc 响应,此时的观测方案设计应该交由调用方手动填写 metric/log ,作为业务指标监控来看。

更深一步来说,还可以细分为业务错误(入参不合法,执行条件不满足,业务逻辑错误等等),框架及中间件错误( token 无效,malformed request ,调用超时,BBR 自适应限流,熔断错误等等),以 int64 作为 errcode 的取值范围的话,常见的方案是 0 为正常响应,errcode>0 的部分作为业务错误( eg:10001=余额不足),errcode<0 的部分作为框架及通用错误( eg:-400=请求错误/参数错误,-504=调用超时,-429=BBR 限流)

rpc error 本身已经有一套错误码了,这个通常称为大码,含义可以简单认为和 http code 相同,通常作为 transport 层的可观测手段,适用对象 API 网关,SLB 等基础 L3-L7 设施
业务 errcode 通常是选择 rpc error 中的 unknown error (例如 GRPC 为 status 2 )作为可扩展对象,这个称为小码,其作用就是,在 rpc 的底层 transport 没有问题的时候,用于传递业务+业务框架的错误,主要目的用于微服务治理和更细粒度的 SLO 观测
justdoit123
75 天前
感谢 感谢~ 感觉拓宽了视野,也清晰了很多。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1054326

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX