kitex icon indicating copy to clipboard operation
kitex copied to clipboard

Proposal:业务自定义异常的使用

Open YangruiEmma opened this issue 3 years ago • 17 comments

本 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,因为用户异常和框架异常不会同时出现。

YangruiEmma avatar Jun 24 '22 07:06 YangruiEmma

relevant discussion: https://github.com/cloudwego/kitex/discussions/248

YangruiEmma avatar Jun 24 '22 07:06 YangruiEmma

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是不是已经够用了。

xieyuschen avatar Jun 24 '22 09:06 xieyuschen

另外,个人觉得这里似乎是对errors的简单封装,用户自定义的error无法拓展,比如说用户想在error里面记一个traceId, requestId什么的信息。这个实现没办法让用户拓展

xieyuschen avatar Jun 24 '22 09:06 xieyuschen

可以参考 go-kratos 的错误处理,做的很好,

JellyTony avatar Jun 28 '22 16:06 JellyTony

@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类型用于返回自定义的信息

YangruiEmma avatar Jul 03 '22 17:07 YangruiEmma

可以参考 go-kratos 的错误处理,做的很好,

感谢反馈,请问具体指哪里呢?kratos的rpc是对gRPC框架的封装,异常也是基于grpc的status error额外封装定义

YangruiEmma avatar Jul 03 '22 17:07 YangruiEmma

是的,哪个方式

JellyTony avatar Jul 05 '22 02:07 JellyTony

@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,我对这块还不太熟悉,会再阅读代码的。

xieyuschen avatar Jul 05 '22 04:07 xieyuschen

@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)
}

@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。

是的,哪个方式

@JellyTony 麻烦给一个具体的示例

YangruiEmma avatar Jul 05 '22 06:07 YangruiEmma

@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)
}

@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。

是的,哪个方式

@JellyTony 麻烦给一个具体的示例

Get it, thanks.

xieyuschen avatar Jul 05 '22 07:07 xieyuschen

@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)
}

@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。

是的,哪个方式

@JellyTony 麻烦给一个具体的示例

Get it, thanks.

https://github.com/cloudwego/kitex/issues/511#issuecomment-1174709248

JellyTony avatar Jul 12 '22 02:07 JellyTony

@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)
}

@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。

是的,哪个方式

@JellyTony 麻烦给一个具体的示例

Get it, thanks.

@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)
}

@xieyuschen Kitex 对错误处理的封装会对handler返回的error增加[biz error],所以这里不用增加类型信息也是可以判断的,用户使用完全可以实现自己的error,用户自定义的error就没法控制error信息是否有BizStatusError,整体收敛在框架里更合适。

是的,哪个方式

@JellyTony 麻烦给一个具体的示例

https://go-kratos.dev/docs/component/errors/

JellyTony avatar Jul 12 '22 16:07 JellyTony

简单来说,就是框架层面定义自己的错误类型,然后业务根据这个错误类型去定义自己的错误,上层服务可以根据 定义的错误 proto 生成 错误辅助函数去判断具体的某个错误,而不是用过错误等值判断,相信等值判断在微服务传递过后这个错误的值就会发生变化,这时候就需要一个固定不变的值去做判断,比如 go-kratos error 的reason 就是项目内全局唯一的,微服务传递这个也不会发生变化,

JellyTony avatar Jul 12 '22 16:07 JellyTony

简单来说,就是框架层面定义自己的错误类型,然后业务根据这个错误类型去定义自己的错误,上层服务可以根据 定义的错误 proto 生成 错误辅助函数去判断具体的某个错误,而不是用过错误等值判断,相信等值判断在微服务传递过后这个错误的值就会发生变化,这时候就需要一个固定不变的值去做判断,比如 go-kratos error 的reason 就是项目内全局唯一的,微服务传递这个也不会发生变化,

而不是用过错误等值判断

proto 生成的的确会对错误类型做了封装比较便利,但实质还是值判断,kratos 的reason是在原Status上新增的异常定义信息,没有理解你说的「reason 就是项目内全局唯一的,微服务传递这个也不会发生变化」,你是指idl定义的error类型在链路上可以统一吗?

Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos是不一样的

YangruiEmma avatar Jul 13 '22 08:07 YangruiEmma

是指idl定义的error类型在链路上可以统一吗?

Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos

reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多

JellyTony avatar Jul 14 '22 01:07 JellyTony

是指idl定义的error类型在链路上可以统一吗? Kitex对Protobuf是支持,而不是耦合,在通用的错误处理上不会和idl绑定,这一点和go-kratos

reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多

不懂就问,和ID有啥关系...

li-jin-gou avatar Jul 14 '22 02:07 li-jin-gou

reason 不是 id, 是错误的唯一标识,就跟唯一错误码一样和 code 差不多

你说的这个 ”唯一“ 是基于错误码和idl定义,bizStatusError 对 code 也是强制要求的,至于code如何定义由用户来决定,前者链路上统一idl状态码定义,后者业务统一维护自己的bisStatusError,不论前者还是后者如果错误码没有统一维护都会存在不一致的问题,差别是错误码有变更时是修改idl重新生成还是修改代码

YangruiEmma avatar Jul 19 '22 16:07 YangruiEmma