gRPC的错误处理实践

1dbe03c9066a2e0eb5a5faa794d1023a.png

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

背景

c45beebb77a954f3d921d17a03cbc43a.png

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

d7cc49bff1b9c21053681860eec2ef38.png

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

判断Error的错误原理

a6651eafc6104b5724aceb0485dd550b.png

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

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

9cbda93da1da9b221d08e68a19463fc5.png

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

ce0f1809c58cc0f5102033b38175400b.png

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

gRPC网络传输的Error

140fa6e779a6b5b4259de67878747590.png

我们客户端在获取到gRPC的error的时候,是否可以使用上文说的官方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-status、grpc-message、grpc-status-details-bin的header头里。

  • 客户端通过网络获取到这些header头,使用strconv.ParseInt解析到grpc-status信息、decodeGrpcMessage解析到grpc-message信息、decodeGRPCStatusDetails解析为grpc-status-details-bin信息。

  • 客户端通过a.Status().Err()获取到用户代码的错误。

98d841a7b8e10a86a90abd08d82399ca.png

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

3a3b71e17d51c43c4203fe202d52d885.png

检查gRPC的error信息第一版本

146f0b185becb49e66430e236ee257e0.png

通过上文描述,我们已经了解了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来判断根因。

37afd34e69959da3f33fab048934917f.png

检查gRPC的Error信息第二版本

b373acb1a553a95687901ad30a72211e.png

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

44d880e6c3d1002e8853a7151ed77ef1.png

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

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

91161050e2c1b9b8239277d376fc91f5.png

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

演示gRPC的Error的处理

beb396848f6d5e2e91240b0b782a1ff3.png

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

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

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

生成error、grpc的pb文件

我们在该演示代码目录下执行make gen,可以生成对应的error、grpc的pb文件,如下所示:

baf48f2875ca973486564322b8b2c65b.png

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

func ResourceErrUnknown() eerrors.Error {  
 return resourceErrUnknown  
}

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

2b889e3fd249881376b35fe31d8abec1.png

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

f631d14f3e46023920da7d7c93a42a87.png

执行指令

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

服务端展示:

ba818275f08e3e14c27635e06f497454.png

客户端展示:

ed73a8ea092f557f766808e1424d1fa6.png

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

错误码查看

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

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

鸣谢

684f3453b38e1c3cd16e5088f91a29a0.png

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

参考链接:

  1. 项目演示代码:https://github.com/gotomicro/go-engineering/tree/main/chapter_grpc_error/egoerror

  2. 项目框架:https://github.com/gotomicro/ego

    proto生成插error件:https://github.com/gotomicro/ego/tree/master/cmd/protoc-gen-go-errors

  3. 框架对error的处理:https://github.com/gotomicro/ego/blob/master/core/eerrors/errors.go

  4. 常量error:https://dave.cheney.net/2016/04/07/constant-errors

  5. Go1.13Error Wrapping分析:https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html

Kubernetes线下实战与CKA培训

8c1a5c3d131322d18524244d0a96a11b.png

本次培训在北京开班,基于最新考纲,理论结合实战,通过线下授课、考题解读、模拟演练等方式,帮助学员快速掌握Kubernetes的理论知识和专业技能,并针对考试做特别强化训练,让学员能从容面对CKA认证考试,使学员既能掌握Kubernetes相关知识,又能通过CKA认证考试,理论、实践、考证一网打尽,学员可多次参加培训,直到通过认证。点击下方图片或者阅读原文链接查看详情。

2b7b2386e082e054da5d7bbf083e6029.png

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值