基于石墨文档基于K8S的Go微服务实践,我们这次把该内容中的错误码做了一个详细的介绍。
0 背景
我们内部系统全部统一采用gRPC协议和protobuf编解码。统一的好处在于不需要在做任何协议、编解码转换,这样就可以使我们所有业务采用同一个protobuf仓库,基于CI/CD工具实现许多自动化功能。
我们要求所有服务提供者提前在独立的路径下定义好接口和错误码的protobuf文件,然后提交到GitLab,我们通过GitLab CI的check阶段对变更的protobuf文件做format、lint、breaking检查。然后在build阶段,会基于protobuf文件中的注释自动产生文档,并推送至内部的微服务管理系统接口平台中,还会根据protobuf文件自动构建Go/PHP/Node/Java等多种语言的桩代码和错误码,并推送到指定对应的中心化仓库。推送到仓库后,我们就可以通过各语言的包管理工具拉取客户端、服务端的gRPC和错误码的依赖,不需要口头约定对接数据的定义,也不需要通过IM工具传递对接数据的定义文件,极大的简化了对接成本。
1 判断Error的错误原理
要了解怎么处理gRPC的error之前,我们首先来看下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
我们客户端在获取到gRPC的error的时候,是否可以使用上文说的官方errors.Is进行判断呢。如果我们直接使用该方法,通过判断error地址是否相等,是无法做到的。原因是因为我们在使用gRPC的时候,在远程调用过程中,客户端获取的服务端返回的error,在tcp传递的时候实际上是一串文本。客户端拿到这个文本,是要将其反序列化转换为error,在这个反序列化的过程中,其实是new了一个新的error地址,这样就无法判断error地址是否相等。
为了更好的解释gRPC网络传输的error,以下描述了整个error的处理流程。
为了方便理解,我们抓个包,看下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,无法携带message,metadata信息。这就导致我们,很难准确定位一些问题。所以这个时候,我们需要在error里做一些扩展,增加两个方法。
这种方式可以让我们携带信息,但是他会对原有的error错误做一次克隆,导致了error的地址变化,无法在通过error判等的方式进行校验是否是根因。
这个时候,我们只能通过errors.Is中的(interface{ Is(error) bool })断言方式,在我们自定义的error中,增加一个Is方法来判断。
通过这种方式,我们不仅可以判断根因,并且还可以将error里携带更多排查有用的信息。
5 演示gRPC的Error的处理
为了更好的演示error,我们将error处理的方式做成了工具,通过执行脚本,我们就可以下载到对应的工具
bash
通过该工具,就可以执行我们ego error的演示代码
5.1 生成error、grpc的pb文件
我们在该演示代码目录下执行make gen,可以生成对应的error、grpc的pb文件,如下所示。
来源【首席数据官】,更多内容/合作请关注「辉声辉语」公众号,送10G营销资料!
版权声明:本文内容来源互联网整理,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 jkhui22@126.com举报,一经查实,本站将立刻删除。