客户端
配置
RpcClientConf
| 名称 | 类型 | 含义 | 注意 | 默认值 | 是否必选 |
|---|---|---|---|---|---|
| Etcd | EtcdConf | 服务发现配置,当需要使用 etcd 做服务发现时配置 | 当服务端已经注册etcd,需要让客户端动态获取服务地址时配置 | 无 | 否 |
| Endpoints | string 类型数组 | RPC Server地址列表,用于直连(不用服务发现),当需要直连 rpc server 集群时配置 | 直接指定rpc服务器地址列表,不适用服务发现,当配置etcd时会被忽略 | 无 | 否 |
| Target | string | 域名解析地址 | gRPC原生的命名解析机制,格式为scheme://authority/endpoint_name |
无 | 否 |
| App | string | rpc 认证的 app 名称,仅当 rpc server 开启认证时配置 | 当鉴权开启时,客户端必须提供合法app,会自动插入每次请求的metadata | 无 | 否 |
| Token | string | rpc 认证的 token,仅当 rpc server 开启认证时配置 | 当鉴权开启时,客户端必须提供合法token,会自动插入每次请求的metadata | 无 | 否 |
| NonBlock | bool | 是否阻塞模式,当值为 true 时,不会阻塞 rpc 链接 | false时Dial会阻塞到建立成功或超时,true时即使RPC服务不可达,客户端也不会卡死,但是前几次请求可能会失败 |
false | 否 |
| Timeout | int64 | 超时时间 | 客户端发起 RPC 调用的最大等待时间,会覆盖gRPC上下文的超时 | 2000ms | 否 |
| KeepaliveTime | Time.Duration | 保活时间 | gRPC 客户端向服务端发送 PING 帧的间隔时间,用于保持长连接不被中间代理或防火墙断开 | 20s | 否 |
| Middlewares | ClientMiddlewaresConf | 是否启用中间件 |
若需要使用etcd,在yaml文件中注册如下:
1 | Etcd: |
- hosts:etcd集群地址
- key:服务注册的key,通常为服务名
EtcdConf
当使用etcd做服务发现时配置
| 名称 | 类型 | 含义 | 默认值 | 是否必选 |
|---|---|---|---|---|
| Hosts | string 类型数组 | etcd 集群地址 | 无 | 是 |
| Key | string | 服务发现 key | 无 | 是 |
| ID | int64 | etcd 租户 id | 无 | 否 |
| User | string | etcd 用户名 | 无 | 否 |
| Pass | string | etcd 密码 | 无 | 否 |
| CertFile | string | etcd 证书文件 | CertFile | 否 |
| CertKeyFile | string | etcd 证书私钥文件 | 无 | 否 |
| CACertFile | string | etcd 证书根文件 | CertFile | 否 |
| InsecureSkipVerify | bool | etcd 是否跳过证书验证 | 无 | 否 |
ServerMiddlewaresConf
中间件配置,当需要控制中间件时配置,控制需要开启哪些中间件
| 名称 | 类型 | 含义 | 默认值 | 是否必选 |
|---|---|---|---|---|
| Trace | bool | 是否开启链路追踪 | true | 否 |
| Recover | bool | 是否开启异常恢复 | true | 否 |
| Stat | bool | 是否开启统计 | true | 否 |
| Prometheus | bool | 是否开启 prometheus | true | 否 |
| Breaker | bool | 是否开启熔断 | true | 否 |
客户端实现
gRPC客户端封装在zrpc.Client
中,自动集成了服务发现、负载均衡、熔断器和链路追踪
直连模式
1 | import ( |
直连模式会显式指定服务地址,不走注册中心,客户端会和endpoints列表中的地址建立gRPC连接
缺点为地址写死,迁移后需要手动更新代码或重新部署
适用于本地开发与单元测试和服务器地址固定不变的场景
服务发现模式
1 | func NewServiceContext(c config.Config) *ServiceContext { |
客户端通过服务注册中心动态获取可用服务节点,具体ip集中在app.yaml中
运行流程:
- 客户端启动时连接etcd
- 根据key从etcd拉取一组节点(服务端启动时注册的)
- 内置的负载均衡器会从这组地址中选择以恶搞发起调用
- etcd中的节点发生变更时,客户端自动更新本地地址列表
优点在于适合动态扩缩容,配合Breaker中间件,可以自动摘除故障节点,与整个go-zero生态结合紧密
适用于生产环境
newClient的方法
NewClient会返回client和err,需要手动处理错误,启动时连接失败不会panicMustNewClient在内部调用client,如果出错则直接panic
大部分项目使用第二个即可,需要特殊处理初始化错误时,才使用第一个
发起调用
在gozero中HTTP服务调用下游RPC服务的写法:
1 | func (l *HelloLogic) Hello(req *types.HelloReq) (*types.HelloResp, error) { |
在逻辑层结构体,包含两个重要成员:
svcCtx *ServiceContext包含所有依赖ctx context.Context由框架传入,携带了本次HTTP请求的全部上下文- 当 go-zero 的 HTTP 网关收到请求时,会从请求头中提取
traceparent、tracestate等 W3C TraceContext 字段,生成带有SpanContext的context,存入l.ctx - 当调用 gRPC 客户端时,go-zero 的 gRPC 拦截器会自动将
l.ctx中的 trace 信息注入到 gRPC 的 metadata 中,生成traceparentheader - 下游 RPC 服务收到请求后,若同样使用了 go-zero 并开启了 trace 中间件,会自动解析该 header,并创建子 Span,将其父 Span 设为当前 HTTP Span
- 当 go-zero 的 HTTP 网关收到请求时,会从请求头中提取
注意:
l.ctx除了trace,还会携带deadline(由http超时或上层调用设置),gRPC会调用并自动遵循,超时返回context.DeadlineExceeded若需要在RPC传递业务自定义元数据,可以使用
metadata.NewOutgoingContext(l.ctx)将 kv 放入 context,gRPC 拦截器会自动传递。
配置文件
RPC客户端在对应etc/app.yaml中配置,参数如下:
1 | GreeterRpc: |
两种方案是互斥的,框架会根据是否填入Etcd Key来选择使用哪种模式,etcd的优先级更高,其中:
Timeout:客户端认为一个RPC调用失败前的最大等待时间,防止线程被长时间挂住KeepaliveTime:gRPC长连接每隔多久发送一次ping,防止负载均衡或防火墙因空闲断开连接
错误处理
gRPC的错误是一个结构化状态,包含一个标准的status.Code和描述
1 | import "google.golang.org/grpc/status" |
status.FromError(err)从 gRPC 错误中提取出*status.Status对象st.Code()得到对应的 gRPC 状态码- 可以根据这些状态码使用
NewCodeError将错误转换成自己需要的格式,然后再返回给 HTTP 客户端。 - 内置的
errorx会和httpx配合,将带code的错误直接映射为HTTP响应状态码
熔断器
gozero的RPC客户端默认启用了熔断器,基于自适应熔断,逻辑是:
在滑动时间窗口内(默认为10秒),如果下游请求的错误率超过50%,熔断器就会打开,拒绝后续请求。一段时间后会进入半开状态,试探性放行少量请求,如果成功率回升则关闭
拦截器
有些场景需要向每个gRPC请求的metadata中注入业务信息,如x-request-id,认证token等,gozero可以使用WithUnaryClientInterceptor添加自定义拦截器
1 | conn, err := zrpc.NewClient(c.GreeterRpc, |
metadata.AppendToOutgoingContext向 gRPC 的 outgoing metadata 中添加 key‑value 对invoker(ctx, method, req, reply, cc, opts...)继续后续的拦截器链并最终发起调用
常用场景包括:
x-request-id:全链路追踪 ID,便于日志关联Authorization/Token:服务间认证x-user-id:传递当前操作用户身份
客户端连接
通过goctl生成一个rpc server服务:
1 | # 生成一个 greet 服务 |
直连
直连下有两种模式
地址解析模式:
1 | func main() { |
其中Target是gRPC内置的URI风格解析器标识,语法为schema://authority/endpoint
如dns:///127.0.0.1:8080:告诉gRPC的resolver通过DNS查询解析这个地址,当指定的是IP地址时,DNS解析直接返回该IP,类似直连;当指定的是域名时,才会做真正的DNS解析
- 若DNS返回多个IP,gRPC会在它们之间做客户端负载均衡
- 适合K8s Headless Service场景配合DNS服务发现使用
- 其中target和endpoints互斥,同时设置时,前者会被忽略
多节点直连模式:
1 | func main() { |
gozero会遍历给出的endpoints列表,将每个地址构造为gRPC的sunConn,客户端内置的负载均衡策略(P2C,即随机选择两个,选择其中最优的一个)会在这些节点间发送请求,若某个节点故障,gRPC层面的连接重试和gozero的熔断器会将其临时剔除
这样处理配置直观,且不需要DNS解析或注册中心,多个节点之间自动负载均衡,配合上熔断器可以提高可用性,但是地址列表是静态的,需要手动更新配置并重启
etcd 服务发现
go-zero下,可以使用etcd做服务发现,初始化如下面的例子:从 etcd
中找到名为 greet.rpc 的服务节点,然后建立 gRPC 连接并调用其
Ping 方法
1 | func main() { |
- 首先通过conf和fillDefault将RPC客户端配置结构体填入默认值,防止服务异常
clientConf.Etcd用于配置etcd:Hosts:etcd的集群地址Key:服务在etcd中的注册key(服务名)- 其余字段当etcd开启ACL/TLS时才需要填写
- 接下来使用
MustNewClient创建一个gRPC客户端,此时内部执行的动作:- 根据etcd配置启动服务发现(初始化
discov.DiscovClient) - 从etcd监听到的节点列表中,选择一个可用地址,建立一条gRPC
ClientConn - 把该
ClientConn包装进zrpc.Client并返回
- 根据etcd配置启动服务发现(初始化
conn.Conn()返回真正的*grpc.ClientConngreet.NewGreetClient是根据protobuf文件编译出的 gRPC client 构造函数,负责将结构体方法映射成 RPC 调用client.Ping发送请求并等待响应,超时使用gRPC配置中的timeout,gozero自动在拦截层添加context超时
原生支持
可以不适用go-zero的zrpc的client初始化,而直接使用grpc.ClientConn原生支持
原生grpc去掉了服务发现,负载均衡,拦截器等能力,适合连接固定地址的场景
1 | func main() { |
grpc.Dial用于建立连接:- 第一个参数是目标服务地址,直连而不走服务中心
grpc.WithBlock()使Dial阻塞等待连接成功,如果不加这个选项,Dial会立即返回,底层在后台异步建立grpc.WithTransportCredentials(insecure.NewCredentials()):指定传输证书,这里使用insecure.NewCredentials()无认证,生产环境下可以使用credentials.NewTLS(...)- 返回的
conn是*grpc.ClientConn,它负责实际的网络连接管理与复用 - 注意:使用
WithBlock()且没有设置超时时,如果目标服务不存在或网络不通,Dial可能一直阻塞而不会返回,建议配合带超时的ctx:ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
client := greet.NewGreetClient(conn)创建业务客户端,该方法是通过protobuf文件生成的client桩代码,接受一个grpc.ClientConn接口,返回一个可以调用具体方法的客户端对象,只负责将方法调用封装成 gRPC 请求并发送client.Ping发送一个ping请求等待响应,这里也是没有超时的,最好自己传入一个带超时的ctx
使用场景:
- 本地功能调试
- 服务发现由自己实现
服务端
go-zero 在标准 gRPC 服务端之上封装了弹性治理、服务注册和链路追踪等能力
大体流程:定义 Protobuf → 用 goctl 生成服务骨架 → 填充业务逻辑 → 配置 etcd 注册 → 启动 RPC 服务
开发
定义proto
1 | syntax = "proto3"; |
通过protobuf文件定义接口逻辑,生成代码时会以此为依据生成 Handler 接口、消息类型、client stub 和 server 注册逻辑
生成代码
通过goctl生成代码:
1 | goctl rpc protoc greeter.proto --go_out=. --go-grpc_out=. --zrpc_out=. |
目录结构:
1 | greeter/ |
greeter/:存放*.pb.go和*_grpc.pb.go,这是由 protoc 生成的纯 gRPC 代码,不渗透任何 go-zero 关联internal/config:定义从 yaml 读取的配置结构体。例如Name、ListenOn、Etcdinternal/svc:保存ServiceContext结构体,相当于一个“依赖容器”,把一个服务需要用到的数据库、缓存等资源组装好。internal/logic:业务代码唯一需要写的地方。包含每个 RPC 方法的逻辑internal/server:由 goctl 生成,负责将 gRPC 请求转交给 logic 层处理,本质是适配器。greeter.go:启动入口,会组装整个服务并启动监听
实现业务逻辑
下面是一个logic文件逻辑实现示例:
1 | func (l *SayHelloLogic) SayHello(in *greeter.SayHelloReq) (*greeter.SayHelloResp, error) { |
只需要关注逻辑层并返回resp即可,不需要关注server层
配置文件
配置文件处于etc/*.yaml
1 | Name: greeter.rpc |
Name指服务名,客户端在etcd中寻找key时使用的是这里的名字ListenOn指监听地址Etcd是服务注册,启动时 go-zero 会将本服务地址(通常是本机外网 IP +:8080)注册到 etcd 的/services/greeter.rpc(或等价路径)下,这样etcd才能通过key找到该服务
服务启动流程
启动入口greeter.go入口的流程如下:
1 | func main() { |
首先加载配置,构建serverContext
注册
GreeterServer的实现:greeter.RegisterGreeterServer(server, server.NewGreeterServer(svcCtx))然后自动向etcd注册
greeter.rpc服务,并定期续约,监听注册的地址提供请求只要客户端配置的etcd的key和服务端的etcd的key一致,客户端就可以从etcd发现该服务的多个实例,进而做负载均衡,动态更新连接池等
配置
zRPC 服务配置主要用于控制 rpc 的监听地址、服务注册发现、链路追踪、日志、中间件等
RpcServerConf
这里是服务总配置的入口
1 | type RpcServerConf struct { |
service.ServiceConf是通用配置,决定了服务全局行为ListenOn是监听地址,若只写端口号表示监听所有网卡etcd是服务注册与发现配置,不需要向etcd注册时可以为空,此时客户端通过直连地址访问服务auth表示开启rpc调用认证,客户端必须要在gRPC的metadata中携带token,服务端才会接受请求Redis保存了合法token的映射,服务端会去Redis校验StrictControl指定校验失败时的策略,true表示失败即拒绝,false记录waring但不拒绝,适合调试场景Timeout指服务端处理单个请求的超时时间CpuThreshold是自适应降载阈值,默认900,表示当CPU利用率达到90%时开始拒绝部分服务Health表示是否注册gRPC健康检查服务,默认开启,这样k8s或gRPC health client可以探测服务存活状态:grpc_health_v1.NewHealthClient(conn).Check(...)Middlewares控制服务器内置中间件开关,生产环境保持默认即可
EtcdConf
这里是etcd的详细配置
1
2
3
4
5
6
7
8
9
10
11type EtcdConf struct {
Hosts []string
Key string
ID int64
User string
Pass string
CertFile string
CertKeyFile string
CACertFile string
InsecureSkipVerify bool
}
Hosts:etcd 集群节点地址Key:服务唯一标识,建议与服务 Name 保持一致(如greeter.rpc),客户端也使用这个 Key 去 etcd 中寻找服务ID:可选的实例 ID,如果不填,go-zero 会使用本机 IP + 端口自动生成唯一 ID,用于多实例注册时区分- 其余字段:当 etcd 开启 TLS/ACL 时需要提供
- 注意:gozero的etcd注册路径一般是
/services/{Key}/{ID}的形式,key相同但id不同会被视为同一服务的不同副本
RedisKeyConf
用于认证存储,RedisKeyConf是当你需要使用 redis 来管理
rpc 认证时需要配置的参数
1 | type ( |
Key是存储 token 的 redis key 前缀,例如rpcAuth:- 服务端会根据客户端传来的 token,去 redis 的
${Key}${token}中查找是否存在且未过期
ServerMiddlewaresConf
中间件配置,控制常用中间件的开启和关闭
1 | ServerMiddlewaresConf struct { |
示例
以下是创建一个gRPC服务端的示例
初始化
首先创建目录并初始化go module,并生成一个proto文件,然后根据proto生成gRPC服务
1 | $ mkdir demo && cd demo |
连接模式
有服务发现和直连模式两种连接模式,通过调整etc/文件夹下的yaml文件即可
etcd服务注册使用etcd作为注册中心,只需要在文件下添加etcd的配置即可,最简配置如下:
1 | Etcd: |
这样,服务启动后,会自动把ip注册到注册中心,其他服务如果使用gozero的zrpc客户端,只需要设置相同的etcd key,就可以自动负载均衡到这台服务
使用etcdctl get --prefix greet.rpc查看注册结果
使用etcdctl get --prefix获取所有可用ip和节点
由于 etcd 注册的 key 都是 greet.rpc,从业务表现层来看,是将一个 key 注册到了 etcd,实则 go-zero 底层是将该 key 拼上了一个 etcd 的租户 id 来存储到 etcd 的,类似于:
1
2 key = /services/greet.rpc/{租户ID}
value = "192.168.72.53:8080"
- 如果多个实例都注册同一个
greet.rpc,直接使用固定 key 会导致互相覆盖- 加入租户 ID(实际上是实例唯一标识)后,每个实例都能独立注册,且服务发现方可以通过
get --prefix greet.rpc一次取出所有实例这也是gozero客户端可以实现多实例负载均衡的基础
若使用直连模式,去除etcd配置即可
1 | Name: greet.rpc |
stub实现
通过goctl生成的代码就不用stub方法了,goctl会全部实现
代码存在于/internal/server/greetserver.go下,用于实现
gRPC 框架定义的服务接口greet.GreetServer,并把请求转交给
internal/logic 层的业务代码。
结构体定义:
1 | type GreetServer struct { |
svcCtx保存了服务上下文,包括配置、数据库连接、redis等共享依赖,它是 go-zero 依赖注入的核心:所有 logic 方法都通过它访问全局资源greet.UnimplementedGreetServer是官方gRPC生成器在pb文件中定义的,即使未来proto新增了rpc方法但你的代码没有实现,也不会panic而是返回未实现,这是一种向前兼容
构造函数:
1 | func NewGreetServer(svcCtx *svc.ServiceContext) *GreetServer { |
将 ServiceContext 注入到服务端对象中,之后每个 RPC
方法都可以使用这个上下文
核心方法:
1 | func (s *GreetServer) Ping(ctx context.Context, in *greet.Request) (*greet.Response, error) { |
假设是一个Ping方法,gozero的处理流程如下:
1 | gRPC 请求 |
- server 层:只做“路由转发”,代码千篇一律,交给 goctl 生成
- logic 层:包含全部业务规则,由开发者编写
- 当
.proto接口变更,goctl 会自动更新 server 层并生成新的 logic 占位代码,你只需在新占位方法中实现业务
1 | package logic |
对于逻辑层:
NewPingLogic接收 context 和 svcCtxPing()方法直接从GreetServer.Ping()调过来,in就是解码后的请求参数,返回即为响应体
对于客户端:
目录中的greetclient/greet.go是为调用方生成的客户端代码,为了方便业务方生成的代码,当你不是服务端实现者时,只需要greetclient.NewGreetClient(...)
就能请求这个 RPC 服务
编写业务代码
通过goctl生成代码后,只需要在logic下填入我们的业务代码即可
开启gRPC调试开关
gRPC提供了调试功能,以便于通过grpcurl等工具调试,建议在开发环境和测试环境开启,预生产环境和正式环境建议关闭的,相关方法为reflection.Register(grpcServer)
为什么需要:
gRPC
默认使用protobuf二进制协议,请求和响应的结构对外部工具不可见。普通 HTTP
接口可以用 curl 直接调试,但 gRPC 很难,grpcurl
这类工具就是为了解决这个问题:它向服务端发送 server
reflection
请求,服务端返回自己支持的服务列表、方法、消息定义。于是 grpcurl 就能像
RESTful 工具一样,动态构造请求和解析响应,不需要预先拿到
.proto 文件
只需要在程序入口文件中添加如下:
1 | if c.Mode == service.DevMode || c.Mode == service.TestMode { |
且在配置文件内声明模式为dev或test:
1 | Mode: dev |
Mode是基础配置
service.ServiceConf中的一个字段,dev是开发环境,test是测试环境,pro/prod是生产环境
grpcurl调试:
假如你的服务运行在 127.0.0.1:8080,并且
Mode: dev
1 | # 列举所有 service |
若没有开启reflection,会报错Error: server does not support the reflection API,此时只能通过-proto参数显式执行protobuf文件来调试
中间件的使用
gozero中有大量拦截器,对应grpc两种调用模式:
| 拦截器类型 | 作用 |
|---|---|
| UnaryXXXInterceptor | 拦截普通一元 RPC 请求(一个请求一个响应) |
| StreamXXXInterceptor | 拦截流式 RPC 请求(客户端/服务端双向流) |
RpcServerConf.Middlewares中可以通过配置控制五个内置中间件:
Trace:链路追踪Recover:捕获 panicStat:服务调用统计Prometheus:指标暴露Breaker:熔断
其中这五种可以通过配置开启关闭,默认开启
实际项目中,一般将自定义拦截器定义在独立文件/internal/middleware/exampleinterceptor.go,在main入口中引入,保证文件整洁
注册自定义中间件:
1 | s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { |
顺序如下:
MustNewServer创建服务对象- 在回调中注册业务服务和调试反射
- 在服务启动前,通过
AddUnaryInterceptors和AddStreamInterceptors注册自定义中间件 Start()后生效
定义自定义Unary中间件:
1 | func exampleUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { |
ctx:gRPC请求的上下文,可以读取metadatareq:已解码的请求消息对象info:包含当前请求对应的服务方法信息info.FullMethodhandler:业务处理函数,调用handler后会继续向后执行直到进入logic
定义自定义Stream中间件:
1 | func exampleStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { |
srv:当前服务实例,通常用server中的 handler 对象ss:服务端流对象,用于收发消息。info:方法信息handler:流式 RPC 的实际处理函数,需要你手动调用,期间可在外层包裹流程
例子:打印请求日志
1 | func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { |
调用顺序
注意,若有多个自定义拦截器,执行模式按照洋葱模型栈式调用:
1 | 请求进入 |
传递metadata
gozero中服务端的自定义拦截器会默认拿到ctx,所以读取metadata非常方便
1 | func exampleUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { |
然后可以通过s.AddUnaryInterceptors(exampleUnaryInterceptor)
注册到rpc服务端
如果是在客户端调用时携带metadata,只需要在传入的ctx上附加metadata即可
1 | // 方式1:推荐,追加 |
在logic层发送header/trailer可以直接使用grpc的方法:
1 | type PingLogic struct { |
拦截器
拦截器是 gRPC 中实现跨请求逻辑的关键机制,等价于 HTTP 中间件。每次 RPC 调用前后都会执行拦截器,可用于鉴权、记录指标或修改请求上下文
服务端
一元拦截器
以登录认证为例:
1 | func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, |
拦截器从rpc的ctx中取出metadata,检查是否有token字段,若没有则返回认证错误,若有则继续调用handler继续执行业务代码:
- ctx:上下文,包括传来的metadata等
- req:解码后的请求消息对象
- info:当前调用信息
- handler:拦截器上的下一环,最终指向业务handler
注意这里any是golang新版本中的interface{}别名
流式拦截器
1 | func loggingStreamInterceptor(srv any, ss grpc.ServerStream, |
流式 handler 不像一元 handler 那样直接调
handler(ctx, req),而是
handler(srv, ss)。业务代码通过 ss.Recv() 和
ss.Send() 来处理流中的多个消息
- srv:服务端实现对象
- ss:
grpc.ServerStream接口,对应流式调用的服务端流对象 - info:当前调用信息,包含流式RPC的方法流,是否是客户端流等信息
- handler:最终执行流处理的函数
注册方式
1 | server := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { |
也可以放到同一个AddUnaryInterceptors中
1 | server.AddUnaryInterceptors( |
执行顺序依据洋葱模型
客户端
有时候需要给所有发出去的RPC请求附加一些公共信息:比如Token认证、请求ID、业务租户ID或客户端来源/版本,客户端拦截器可以让统一在RPC调用前拦截并修改context/metadata,然后继续发送请求
一元拦截器
例如:在每次对外RPC请求中注入Token或传递请求头
1 | zrpc.WithUnaryClientInterceptor(func(ctx context.Context, method string, |
- ctx:当前调用上下文
- method:将要调用的gRPC方法名
- req:请求信息
- reply:响应信息
- cc:gRPC连接对象
- invoker:下一环节的调用函数,调用才能发送请求
- opts:调用选项
流程:
1 | 发起 RPC |
可以理解为:invoker 就是后续真正的 gRPC
调用链路,你只是“劫持”了它的 ctx 参数,替换成带 metadata
的ctx
流式拦截器
1 | zrpc.WithStreamClientInterceptor(func(ctx context.Context, desc *grpc.StreamDesc, |
- ctx:调用上下文
- desc:流描述,描述是客户端,服务端还是双向流
- cc:连接对象
- method:方法名
- streamer:真正的流创建函数,返回一个
grpc.ClientStream - opts:调用选项
一元拦截器是一次请求一次性处理,invoker完成整个调用,而流式流通过streamer创建流,后续还会通过
stream.Send、stream.Recv
等进行多次消息交互,因此:
- 在拦截器中,你会修改ctx,然后传给streamer,返回ClientStream
- 后续所有sendmsg,recvmsg都是用这个流,metadata已经在这个流理的context中固定了
因此流式拦截器适合在流建立前统一处理
注册方式
NewClient支持选项式注入:
1 | conn, err := zrpc.NewClient(c.GreeterRpc, |
MustNewClient需要先获取底层连接
1 | client, err := zrpc.MustNewClient(c.GreeterRpc, zrpc.WithUnaryClientInterceptor(...)) |
实例
Token注入
1 | func tokenClientInterceptor(ctx context.Context, method string, req, reply any, |
跨服务透传元数据拦截器
用于将上游metadata传给下游
1 | func forwardMetadataInterceptor(ctx context.Context, req any, |
- 首先从当前RPC的ctx中获得客户端传来的metadata
- 使用
metadata.Join合并传来的metadata和需要添加的内容 - 使用
NewOutgoingContext把合并后的metadata绑定到context上 - 将该ctx传入下游的handler,如果下游服务从这个ctx发起RPC服务,metadata就会被带过去
注意,这里Incoming的metadata是别人传给我的,而Outgoing的metadata是我传给别人的
日志拦截器
1 | func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, |
- 调用前记录start
- 执行业务handler
- 调用后记录方法名,耗时和错误信息
- 返回值透传,不影响业务
WithContext是go-zero的增强日志专用写法,可以将当前context的链路信息自动注入日志字段,方便采集到分布式链路系统,logx.Field可以创建键值结构
内置拦截器
go-zero 在每个 zrpc.Server 和 zrpc.Client
上自动注册以下拦截器,无需额外配置:
| 拦截器 | 功能 |
|---|---|
RecoverInterceptor |
将 panic 转换为 gRPC Internal 错误 |
PrometheusInterceptor |
记录 RPC 耗时与错误率指标 |
TracingInterceptor |
创建 OpenTelemetry span,传播 trace context |
BreakerInterceptor |
错误率超阈值时打开熔断器 |
SheddingInterceptor |
CPU 负载过高时主动降载 |
TimeoutInterceptor |
根据服务端配置执行每次 RPC 的超时限制 |