Proposal:业务自定义异常的使用
本 Proposal 目的是制定一种异常规范用于区分 RPC 异常和用户自定义的异常。我们希望将 RPC 错误和业务的错误能够区分开,RPC 错误表示一次RPC 请求失败,比如超时、熔断、限流,从 RPC 层面是失败的请求,但业务错误属于业务逻辑层面,在 RPC 层面其实是请求成功。服务监控建议对于 RPC 错误上报为请求失败,而业务层面错误,上报为请求成功,但上报 status_code 用于识别错误码。该能力对于工程实践具有一定的价值。
BizError 接口定义
内置 BizStatusErrorIface 提供用户实现自定义异常接口,框架同时提供默认实现,用户也可以自定义实现,比如 gRPC 用户自定义的 Error 可同时实现 interface{ GRPCStatus() *status.Status },可以复用 Status 的 Detail 用于透传更丰富的业务信息。
type BizErrorIface interface {
BizStatusCode() int32
BizMessage() string
BizExtra() map[string]string
Error() string
}
type BizError struct {
code int32
msg string
extra map[string]string
}
func (e *BizError) BizStatusCode() int32 {
return e.code
}
func (e *BizError) BizMessage() string {
return e.msg
}
func (e *BizError) BizExtra() map[string]string {
return e.extra
}
用户使用
// Server side
func (*MyServiceHandler) TestError(ctx context.Context, req *myservice.Request) (r *myservice.Response, err error) {
// ...
err = kerrors.NewBizError(-100, "mock error")
return nil, err
}
// Client side
resp, err := cli.TestError(ctx, req)
bizErr, isBizErr := kerrors.FromBizError(err)
框架实现
依赖传输协议透传自定义异常的错误码和错误信息,Thrift 和 Kitex Protobuf 依赖 TTHeader,Kitex gRPC 依赖HTTP2。
- Thrift:使用 TTHeader
- Kitex Protobuf:使用 TTHeader
- gRPC:使用 HTTP2 Header
| 优点 | 缺点 |
|---|---|
| 对用户来说 Thrift/Protobuf 使用方式统一,不需要修改idl | 1. 框架层面异常处理有些分裂:框架异常通过Payload中的错误信息传递;用户自定义异常通过header传递;2. 裸Thrift无法支持,用户必须指定 TTHeader |
框架处理
TTHeader
新增三个string key,分别是 biz-status 、 biz-message 和 biz-extra。
对于服务端,如果用户通过 NewBizStatusError 构造了 error,将 errorCode 、 message 和 extra信息 分别装填到biz-status、biz-message 和 biz-extra;
对于调用端,如果 TTHeader 中 biz-status != 0,则构造 BizStatusError 返回给用户。
Streaming - gRPC
gRPC 的异常信息是通过 Header传递的,errorCode 和 message 分别对应 header 中的 grpc-status 和 grpc-message。框架处理时只需将 errorCode 和 errorMsg 装填到 header 中即可。
但 gRPC 定义的 Status Error 也未区分框架异常和用户自定义异常,grpc-status 作为框架对异常类型的识别,Kitex 增加错误码约束, grpc-status=10000 表示用户异常,message 复用 grpc-message,因为用户异常和框架异常不会同时出现。
relevant discussion: https://github.com/cloudwego/kitex/discussions/248
func (e *bizStatusError) Error() string {
return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg)
}
这里的字符串信息加上具体error的名称是不是会更好,至少应该指明这是一个什么样的错误。
// Client 侧判断错误类型
func FromErr(err error) (BizStatusErrorIface, bool) {
// ...
return
}
这个是否需要提供API,errors.As是不是已经够用了。
另外,个人觉得这里似乎是对errors的简单封装,用户自定义的error无法拓展,比如说用户想在error里面记一个traceId, requestId什么的信息。这个实现没办法让用户拓展
可以参考 go-kratos 的错误处理,做的很好,
@xieyuschen 感谢反馈,邮件太多这里的回复被淹没了,才注意到
func (e *bizStatusError) Error() string { return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) }这里的字符串信息加上具体error的名称是不是会更好,至少应该指明这是一个什么样的错误。
请问可以举个例子吗?
// Client 侧判断错误类型 func FromErr(err error) (BizStatusErrorIface, bool) { // ... return }这个是否需要提供API,
errors.As是不是已经够用了。
是的,用户直接使用 errors.As就可以,这里提供FromErr是为了让用户更加明确如何获取自定义异常,可以不提供。
另外,个人觉得这里似乎是对errors的简单封装,用户自定义的error无法拓展,比如说用户想在error里面记一个traceId, requestId什么的信息。这个实现没办法让用户拓展
使用Kitex-gRPC和kitex-protobuf,用户自定义的 Error 可同时实现 interface{ GRPCStatus() *status.Status },可以复用 Status 的 Detail 用于透传更丰富的业务信息; 使用thrift按照这个定义的确是不能直接支持的,不过可以类似status提供用户自定义struct编码,如果没有复杂结构体的定义需求,也可以直接增加map[string]string类型用于返回自定义的信息
可以参考 go-kratos 的错误处理,做的很好,
感谢反馈,请问具体指哪里呢?kratos的rpc是对gRPC框架的封装,异常也是基于grpc的status error额外封装定义
是的,哪个方式
@YangruiEmma
第一个问题来说,可以在error信息里显示出来这是一个BizStatusError error, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.
func (e *bizStatusError) Error() string {
-- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg)
++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg)
}
这里提供FromErr是为了让用户更加明确如何获取自定义异常,可以不提供.
我的想法是如果只是简单的封装就不需要提供,可以在文档处或者注释处提醒用户如何获取校验error,作为库这种api容易导致代码库膨胀。当然如果这是一个用户使用的高频场景,那加上API会更好。
使用Kitex-gRPC和kitex-protobuf,用户自定义的 Error 可同时实现 interface{ GRPCStatus() *status.Status },可以复用 Status 的 Detail 用于透传更丰富的业务信息; 使用thrift按照这个定义的确是不能直接支持的,不过可以类似status提供用户自定义struct编码,如果没有复杂结构体的定义需求,也可以直接增加map[string]string类型用于返回自定义的信息.
谢谢feedback,我对这块还不太熟悉,会再阅读代码的。
@YangruiEmma 第一个问题来说,可以在error信息里显示出来这是一个
BizStatusErrorerror, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.func (e *bizStatusError) Error() string { -- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) ++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg) }
@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。
是的,哪个方式
@JellyTony 麻烦给一个具体的示例
@YangruiEmma 第一个问题来说,可以在error信息里显示出来这是一个
BizStatusErrorerror, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.func (e *bizStatusError) Error() string { -- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) ++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg) }@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。
是的,哪个方式
@JellyTony 麻烦给一个具体的示例
Get it, thanks.
@YangruiEmma 第一个问题来说,可以在error信息里显示出来这是一个
BizStatusErrorerror, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.func (e *bizStatusError) Error() string { -- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) ++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg) }@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。
是的,哪个方式
@JellyTony 麻烦给一个具体的示例
Get it, thanks.
https://github.com/cloudwego/kitex/issues/511#issuecomment-1174709248
@YangruiEmma 第一个问题来说,可以在error信息里显示出来这是一个
BizStatusErrorerror, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.func (e *bizStatusError) Error() string { -- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) ++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg) }@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。
是的,哪个方式
@JellyTony 麻烦给一个具体的示例
Get it, thanks.
@YangruiEmma 第一个问题来说,可以在error信息里显示出来这是一个
BizStatusErrorerror, 这样查日志时候可以更明确这个error来自哪里。比如net库的error都会明确一个network error: blablabla.func (e *bizStatusError) Error() string { -- return fmt.Sprintf("code=%s, msg=%s", e.code, e.msg) ++ return fmt.Sprintf("BizStatusError: code=%s, msg=%s", e.code, e.msg) }@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。
是的,哪个方式
@JellyTony 麻烦给一个具体的示例
https://go-kratos.dev/docs/component/errors/
简单来说,就是框架层面定义自己的错误类型,然后业务根据这个错误类型去定义自己的错误,上层服务可以根据 定义的错误 proto 生成 错误辅助函数去判断具体的某个错误,而不是用过错误等值判断,相信等值判断在微服务传递过后这个错误的值就会发生变化,这时候就需要一个固定不变的值去做判断,比如 go-kratos error 的reason 就是项目内全局唯一的,微服务传递这个也不会发生变化,
简单来说,就是框架层面定义自己的错误类型,然后业务根据这个错误类型去定义自己的错误,上层服务可以根据 定义的错误 proto 生成 错误辅助函数去判断具体的某个错误,而不是用过错误等值判断,相信等值判断在微服务传递过后这个错误的值就会发生变化,这时候就需要一个固定不变的值去做判断,比如 go-kratos error 的reason 就是项目内全局唯一的,微服务传递这个也不会发生变化,
而不是用过错误等值判断
proto 生成的的确会对错误类型做了封装比较便利,但实质还是值判断,kratos 的reason是在原Status上新增的异常定义信息,没有理解你说的「reason 就是项目内全局唯一的,微服务传递这个也不会发生变化」,你是指idl定义的error类型在链路上可以统一吗?
Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos是不一样的
是指idl定义的error类型在链路上可以统一吗?
Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos
reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多
是指idl定义的error类型在链路上可以统一吗? Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos
reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多
不懂就问,和ID有啥关系...
reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多
你说的这个 ”唯一“ 是基于错误码和idl定义,bizStatusError 对 code 也是强制要求的,至于code如何定义由用户来决定,前者链路上统一idl状态码定义,后者业务统一维护自己的bisStatusError,不论前者还是后者如果错误码没有统一维护都会存在不一致的问题,差别是错误码有变更时是修改idl重新生成还是修改代码