gRPC在项目中的运用

本文最后更新于:2023年6月7日 下午

gRPC在项目中的运用,本篇文章适用于想要在项目中运用gRPC作为通信框架的技术人员

版本说明

本文基于的框架、工具版本

框架版本备注
go1.15.17
Protobufv1.5.2github.com/golang/protobuf
gRPCv1.51.0google.golang.org/grpc
Consulv1.12.3
grpc-gatewayv2.11.3github.com/grpc-ecosystem/grpc-gateway/v2
grpc-consul-resolverv1.4.4github.com/mbobakov/grpc-consul-resolver
powerprotov0.4.1https://github.com/storyicon/powerproto

功能点

  • [x] Protobuf项目格式、规范
  • [x] Protobuf编译工具
  • [x] gRPC 服务注册、发现
  • [x] 参数校验框架
  • [x] 异常恢复
  • [x] 链路追踪
  • [x] 监控
  • [x] 认证
  • [x] 异常日志打印
  • [x] grpcGateway
  • [x] RequestID
  • [x] 统一响应格式
  • [x] 熔断、限流
  • [x] Protobuf文档生成

Protobuf项目格式、规范

项目格式

先介绍一下Protobuf的项目格式、规范。所有的Protobuf文件都应该存在在一个统一的仓库、仓库组中,至于具体的存储方案可以看团队的大小,小团队可以无脑使用单仓库存放。

Protobuf的文件目录应该以项目来划分,如果需要新增一个服务,则直接在仓库根目录中新增一个以服务名命名的文件夹,然后在该文件夹中新增一个以服务名命名的proto文件, 例如新增一个名为user的服务, 则在仓库根目录中新增一个user文件夹, 然后在user文件夹中新增一个user.proto文件即可。

third_party 存放所有项目引用三方proto文件

Protobuf内容只有包名规范比较重要,具体来说就是 package 和 go_package 的定义

package

包名为应用的表示(APPID). 用于生成gRPC的请求路径, 或者在 Proto 之间进行引用Message

例如:

1
2
// 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的地址

1
2
// 其中 github.com/helloteemo/pb 为前缀, ${APPID} 为 echo
option go_package = "github.com/helloteemo/pb/echo";

Protobuf编译工具

经过Protobuf的规范之后,我们拥有了如下格式的代码

1
2
3
4
5
6
7
.
├── echo
│   ├── echo.proto
└── user
├── user.proto
├── go.mod
├── third_party

但是拥有了Protobuf文件还不行,我们需要一个工具来根据IDL来生成Go代码,这个工具就是大名鼎鼎的 protoc ,出名的强大,出名的难配。你可能会遇到如下问题:

  1. 团队成员的protoc的版本不一致,导致生成的代码不一样,提交的时候就是一大堆的冲突
  2. 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的主要作用是拥有一个集中式的网关服务,并使用它中转所有的流量。基于这种特性,我们可以得出一下结论:

  1. gateway 必须高性能、高吞吐
  2. 不能做太多的业务逻辑,保持通用、轻量化
  3. 由于中转所有流量,因此它是服务统一的入口,可以按照这个特性来统计服务性能、服务异常等

对于grpcGateway我们在Protobuf中提前见过一面,它长这样

1
2
3
4
5
6
7
8
9
10
11
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去调用。

统一响应格式

这里有两种思想可供大家参考

  1. 在每一个rpc中都携带一个Code\Message参数,然后在Gateway或者前端nodejs中在包一层Code/Message。这样就可以知道服务端的Code业务异常。微信等是这样做的
  2. 在rpc不返回Code参数,而是交给status 包。在Gateway中统一从 status 包中拿到code进行Code返回

第二种需要在Gateway中需要加一层响应流处理。第一种则不需要

其它

链路追踪、认证、监控、熔断、限流、异常恢复等均可直接使用框架完成功能


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!