gRPC的错误处理实践

GoCN

共 3671字,需浏览 8分钟

 ·

2021-11-19 09:12

基于石墨文档基于K8S的Go微服务实践,我们这次把该内容中的错误码做了一个详细的介绍。

0 背景

我们内部系统全部统一采用gRPC协议和protobuf编解码。统一的好处在于不需要在做任何协议、编解码转换,这样就可以使我们所有业务采用同一个protobuf仓库,基于CI/CD工具实现许多自动化功能。

我们要求所有服务提供者提前在独立的路径下定义好接口和错误码的protobuf文件,然后提交到GitLab,我们通过GitLab CIcheck阶段对变更的protobuf文件做formatlintbreaking 检查。然后在build阶段,会基于protobuf文件中的注释自动产生文档,并推送至内部的微服务管理系统接口平台中,还会根据protobuf文件自动构建Go/PHP/Node/Java等多种语言的桩代码和错误码,并推送到指定对应的中心化仓库。推送到仓库后,我们就可以通过各语言的包管理工具拉取客户端、服务端的gRPC和错误码的依赖,不需要口头约定对接数据的定义,也不需要通过IM工具传递对接数据的定义文件,极大的简化了对接成本。

1 判断Error的错误原理

要了解怎么处理gRPCerror之前,我们首先来看下Go普通的error是怎么处理的。

我们在判断一个error的根因,需要根因error是一个固定地址的指针类型,这样我们才能够使用官方的errors.Is方法判断他是否为根因。以下是一个代码示例:

我们先看这个代码errors.Is(wrapNewPointerError(), fmt.Errorf("i am error"))的执行步骤,首先构造了一个error,然后使用官方%w的方式将error进行了包装,我们在使用errors.Is方法判断的时候,底层函数会将error解包来判断两个error的地址是否一致。

因此我们第一个errors.Is执行的是个false。在使用这个代码errors.Is(wrapConstantPointerError(), sentinelErr),因为是固定地址的error,所以判断根因错误的时候,执行的是true

2 gRPC网络传输的Error

我们客户端在获取到gRPCerror的时候,是否可以使用上文说的官方errors.Is进行判断呢。如果我们直接使用该方法,通过判断error地址是否相等,是无法做到的。原因是因为我们在使用gRPC的时候,在远程调用过程中,客户端获取的服务端返回的error,在tcp传递的时候实际上是一串文本。客户端拿到这个文本,是要将其反序列化转换为error,在这个反序列化的过程中,其实是new了一个新的error地址,这样就无法判断error地址是否相等。

为了更好的解释gRPC网络传输的error,以下描述了整个error的处理流程。

  • 客户端通过invoker方法将请求发送到服务端。
  • 服务端通过processUnaryRPC方法,获取到用户代码的error信息。
  • 服务端通过status.FromError方法,将error转化为status.Status
  • 服务端通过WriteStatus方法将status.Status里的数据,写入到grpc-statusgrpc-messagegrpc-status-details-binheader头里。
  • 客户端通过网络获取到这些header头,使用strconv.ParseInt解析到grpc-status信息、decodeGrpcMessage解析到grpc-message信息、decodeGRPCStatusDetails解析为grpc-status-details-bin信息。
  • 客户端通过a.Status().Err()获取到用户代码的错误。

为了方便理解,我们抓个包,看下error具体的报文情况。

3 检查gRPC的error信息第一版本

通过上文描述,我们已经了解了gRPC在网络中如何传输error,可以看到new出来的error是无法判等的。所以我们就想到,使用工具提前生成好error,这样error的地址是不会改变的。这样我们就可以使用errors.Is的方法去检查根因error

首先我们可以将错误码编写在proto里,注释,如下所示:

syntax = "proto3";
package engineering.helloworld;
option go_package = "engineering/helloworld;helloworld";
// @plugins=protoc-gen-go-errors
// 错误
enum Error {
  // 未知类型
  // @code=UNKNOWN
  RESOURCE_ERR_UNKNOWN = 0;
  // 找不到资源
  // @code=NOT_FOUND
  RESOURCE_ERR_NOT_FOUND = 1;
  // 获取列表数据出错
  // @code=INTERNAL
  RESOURCE_ERR_LIST_MYSQL = 2;
  // 获取详情数据出错
  // @code=INTERNAL
  RESOURCE_ERR_INFO_MYSQL = 3;
}

然后我们可以通过执行proto错误插件,生成固定地址的error,将error注册到全局map里,同时我们还可以根据@code的注释,生成gRPC的状态码。

func init() {
 resourceErrUnknown = eerrors.New(int(codes.Unknown), "engineering.helloworld.RESOURCE_ERR_UNKNOWN", Error_RESOURCE_ERR_UNKNOWN.String())
 eerrors.Register(resourceErrUnknown)
 resourceErrNotFound = eerrors.New(int(codes.NotFound), "engineering.helloworld.RESOURCE_ERR_NOT_FOUND", Error_RESOURCE_ERR_NOT_FOUND.String())
 eerrors.Register(resourceErrNotFound)
 resourceErrListMysql = eerrors.New(int(codes.Internal), "engineering.helloworld.RESOURCE_ERR_LIST_MYSQL", Error_RESOURCE_ERR_LIST_MYSQL.String())
 eerrors.Register(resourceErrListMysql)
 resourceErrInfoMysql = eerrors.New(int(codes.Internal), "engineering.helloworld.RESOURCE_ERR_INFO_MYSQL", Error_RESOURCE_ERR_INFO_MYSQL.String())
 eerrors.Register(resourceErrInfoMysql)
}

func ResourceErrUnknown() eerrors.Error {
 return resourceErrUnknown
}
....

接着我们在获取gRPC error后,需要使用FromError方法,转换为我们proto生成的error。在这个转换过程中,我们会从之前注册的全局error map里,通过reason方法,找到对应的error,返回给用户。用户这个时候就可以通过errors.Is来判断根因。

4 检查gRPC的Error信息第二版本

按以上方案,确实可以解决根因问题,但该error,无法携带messagemetadata信息。这就导致我们,很难准确定位一些问题。所以这个时候,我们需要在error里做一些扩展,增加两个方法。

这种方式可以让我们携带信息,但是他会对原有的error错误做一次克隆,导致了error的地址变化,无法在通过error判等的方式进行校验是否是根因。

这个时候,我们只能通过errors.Is中的(interface{ Is(error) bool })断言方式,在我们自定义的error中,增加一个Is方法来判断。

通过这种方式,我们不仅可以判断根因,并且还可以将error里携带更多排查有用的信息。

5 演示gRPC的Error的处理

为了更好的演示error,我们将error处理的方式做成了工具,通过执行脚本,我们就可以下载到对应的工具

bash <(curl -L https://raw.githubusercontent.com/gotomicro/egoctl/main/getlatest.sh)

通过该工具,就可以执行我们ego error的演示代码

5.1 生成error、grpc的pb文件

我们在该演示代码目录下执行make gen,可以生成对应的errorgrpcpb文件,如下所示。

这些error为了防止其他人不小心篡改,获取error的时候,都是用方法来获取,如下所示。

func ResourceErrUnknown() eerrors.Error {
 return resourceErrUnknown
}

我们在server里根据客户端发送的error,返回我们proto生成的error信息。

我们在client里,判断是否是这个error,并记录error里的错误信息。

5.2 执行指令

在目录下执行make svc,我们可以启动服务端 然后在目录下,我们在执行make cli,我们可以启动客户端 执行完后,可以看到如下日志:

服务端展示:

客户端展示:

可以看到客户端红框里,就是我们业务代码里记录的日志。我们通过官方的errors.Is判断,能够很优雅的做一些业务逻辑处理。

5.3 错误码查看

错误码,我们可以全部放在proto里管理。那么我们就可以很方便在proto里查看错误码,或者做的更好一点,将proto生成更好看的错误码文档。

自此我们将错误码进行了详细的介绍,下次我们会介绍gRPC如何做单元测试和mock服务的实践,如何通过proto文件生成单元测试代码。

6 鸣谢

感谢kratoserror的处理和生成工具,通过学习它的代码和思想,我们将框架Ego基于error处理做了更多的改进,例如通过proto的注解生成grpc错误码,生成固定地址的error。并且我们做了更多的proto工具,可以通过proto文件生成单元测试代码、API文档等。

7 相关链接

  • 项目演示代码:https://github.com/gotomicro/go-engineering/tree/main/chapter_grpc_error/egoerror
  • 项目框架:https://github.com/gotomicro/ego
  • 石墨文档基于K8S的Go微服务实践
  • proto生成插error件:https://github.com/gotomicro/ego/tree/master/cmd/protoc-gen-go-errors
  • 框架对error的处理:https://github.com/gotomicro/ego/blob/master/core/eerrors/errors.go
  • 常量error:https://dave.cheney.net/2016/04/07/constant-errors
  • Go1.13Error Wrapping分析:https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html


浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报