gRPC在项目中的运用,本篇文章适用于想要在项目中运用gRPC作为通信框架的技术人员
版本说明
本文基于的框架、工具版本
框架 | 版本 | 备注 |
---|---|---|
go | 1.15.17 | |
Protobuf | v1.5.2 | github.com/golang/protobuf |
gRPC | v1.51.0 | google.golang.org/grpc |
Consul | v1.12.3 | |
grpc-gateway | v2.11.3 | github.com/grpc-ecosystem/grpc-gateway/v2 |
grpc-consul-resolver | v1.4.4 | github.com/mbobakov/grpc-consul-resolver |
powerproto | v0.4.1 | https://github.com/storyicon/powerproto |
功能点
Protobuf项目格式、规范
项目格式
先介绍一下Protobuf的项目格式、规范。所有的Protobuf文件都应该存在在一个统一的仓库、仓库组中,至于具体的存储方案可以看团队的大小,小团队可以无脑使用单仓库存放。
Protobuf的文件目录应该以项目来划分,如果需要新增一个服务,则直接在仓库根目录中新增一个以服务名命名的文件夹,然后在该文件夹中新增一个以服务名命名的proto文件, 例如新增一个名为user
的服务, 则在仓库根目录中新增一个user
文件夹, 然后在user
文件夹中新增一个user.proto
文件即可。
third_party
存放所有项目引用三方proto文件
Protobuf内容只有包名规范比较重要,具体来说就是 package 和 go_package 的定义
package
包名为应用的表示(APPID). 用于生成gRPC的请求路径, 或者在 Proto 之间进行引用Message
例如:
// RequestURL: /grpc_sample.user/${serviceName}/${methodName}
package grpc_sample.user;
其中 grpc_sample
为固定写法, user
为APPID
目前有两种写法:
grpc_sample.${APPID}
和apis.${APPID}
, 两种写法都可以, 但是为了统一, 建议使用grpc_sample.${APPID}
go_package
固定为: ${前缀}/${APPID}
这里的前缀是 go.mod 的mould的地址
// 其中 github.com/helloteemo/pb 为前缀, ${APPID} 为 echo
option go_package = "github.com/helloteemo/pb/echo";
Protobuf编译工具
经过Protobuf的规范之后,我们拥有了如下格式的代码
.
├── echo
│ ├── echo.proto
└── user
├── user.proto
├── go.mod
├── third_party
但是拥有了Protobuf文件还不行,我们需要一个工具来根据IDL来生成Go代码,这个工具就是大名鼎鼎的 protoc
,出名的强大,出名的难配。你可能会遇到如下问题:
- 团队成员的protoc的版本不一致,导致生成的代码不一样,提交的时候就是一大堆的冲突
- protoc的配置项太多,团队成员不会使用
基于上面两个问题给出一个工具: powerproto
,用来统一团队成员的protoc
具体的安装过程、编译都跳过,这里给出一份配置: https://github.com/helloteemo/grpc-sample/blob/main/pb/powerproto.yaml
,这份配置已经包括了:grpc代码生成、grpcGateway代码生成、validate生成、openapi稳定生成。应该是足够大部分的团队使用了。
gRPC服务注册、发现
这里笔者团队架构的分布式存储为consul,但是思路都是相通的。
服务注册的过程发生在grpc服务能够接受访问的上一时刻(或者完全能够接受访问),由服务主动向consul发起注册,注册信息应该包括服务ip+port、服务名等信息
如果程序接受到SIGTERM等信号时,应该将服务转变为不再接受请求的状态,同时将consul服务取消注册。
服务发现则由gRPC的Resolvr机制实现,
参数校验框架
参数校验发生在gRPC服务端,由Protobuf文件生成校验规则之后直接编译成Go代码,在grpc服务中间件中进行校验。
具体规则见框架 校验框架
异常日志打印
这里的异常可以理解为是错误,是需要直接被抛出去处理的。
由于众所周知的Go异常处理问题,所以这里我们引入一个框架 github.com/pkg/errors
。它主要的作用是使 err 携带异常Stack、异常信息。在最底层发生异常的时候一层一层上传,在Service层捕获这个err。最后把异常包裹在 google.golang.org/grpc/status
中。然后使用中间件来打印异常。中间件使用函数 status.Convert(err)
来尝试把异常转化回 status.Status
。
这样就做到了统一的异常日志打印
grpcGateway
前文我们一直在谈论grpcServer,可以知道grpcServer都是使用Protobuf协议、gRPC框架来进行传送的,这显然对客户端、前端同学不太友好,因此我们需要一个工具来进行:http转grpc。这也就是本章的主角:grpcGateway
Gateway的主要作用是拥有一个集中式的网关服务,并使用它中转所有的流量。基于这种特性,我们可以得出一下结论:
- gateway 必须高性能、高吞吐
- 不能做太多的业务逻辑,保持通用、轻量化
- 由于中转所有流量,因此它是服务统一的入口,可以按照这个特性来统计服务性能、服务异常等
对于grpcGateway我们在Protobuf中提前见过一面,它长这样
rpc Echo(EchoRequest) returns (EchoResponse){
option (google.api.http) = {
post:"/grpc-sample/echo/v1/do"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "echo"
description: "",
tags: ["app"]
};
};
大括号里面的内容就是grpcgateway的定义了。它代表的含义是:我这个rpc接口也可以通过/grpc-sample/echo/v1/do这个http请求进行访问,同时我们在 PowerProto 的定义中也加入了 Gateway 的支持。
ok,知道了上面的知识,我们就可以来写Gateway服务了。
具体的代码可以见服务。
RequestID
RequestID的作用在DEBUG的时候再明细不过了。
思路为:gateway中生成RequestID,并携带在上下文中传递到各个service、service之间也需要携带RequestID去调用。
统一响应格式
这里有两种思想可供大家参考
- 在每一个rpc中都携带一个Code,然后在Gateway或者前端nodejs中在包一层Code/Message。这样就可以知道服务端的Code业务异常。微信等是这样做的
- 在rpc不返回Code参数,而是交给
status
包。在Gateway中统一从status
包中拿到code进行Code返回
第二种需要在Gateway中需要加一层响应流处理。第一种则不需要
其它
链路追踪、认证、监控、熔断、限流、异常恢复等均可直接使用框架完成功能