速查表:gRPC 工程要点清单
这一章把前 15 章压成能贴在显示器旁边的表:定义与生成、调用模式、错误与超时、拦截器与鉴权、连接与流控、演进与观测,每项一句话。不需要通读,出问题时按表找对应的一条。
一、定义与生成
| 要点 | 一句话 |
|---|
| 传输层 | gRPC 跑在 HTTP/2 上,靠多路复用与二进制帧拿到低延迟,不要再用 Python 老客户端去压 |
| 编码 | protobuf 用字段编号编码、不带字段名,比 JSON 小 2~3 倍(小对象口径) |
| 字段编号 | 编号一旦发布就永久占用,删除字段必须 reserved 编号与名字 |
| 必填语义 | proto3 没有 required,靠校验规则(buf validate 或服务端校验)保证 |
| 零值语义 | 0、""、false 与「未设置」不可区分,需要区分就用 optional |
| 枚举 | 首值必须是 0 且表示 UNSPECIFIED,未知值必须走 default 分支 |
| 目录约定 | proto/<域>/<版本>/x.proto,gen/ 只放生成产物、不手工改 |
| 生成工具 | 小项目用 protoc,多人项目用 buf(lint、breaking、依赖管理一体化) |
| 生成命令 | buf generate 或 protoc --go_out --go-grpc_out --grpc-gateway_out,产物纳入 CI 比对 |
# buf.gen.yaml:一次生成 struct、服务骨架与 REST 代理
version: v2
plugins:
- {local: protoc-gen-go, out: gen, opt: paths=source_relative}
- {local: protoc-gen-go-grpc, out: gen, opt: paths=source_relative}
- {local: protoc-gen-grpc-gateway, out: gen, opt: paths=source_relative}
二、调用模式
| 模式 | 语法 | 适用与注意 |
|---|
| 一元(unary) | rpc Get(Req) returns (Resp) | 默认选择,请求响应一一对应 |
| 服务端流(server streaming) | returns (stream Resp) | 订阅、大结果集分页推送;客户端要能处理半关闭 |
| 客户端流(client streaming) | (stream Req) returns (Resp) | 上传、批量写入;服务端 Recv 到 io.EOF 才算结束 |
| 双向流(bidirectional streaming) | (stream Req) returns (stream Resp) | 长连接会话、实时 IM;必须处理两边的并发收发 |
| 生成骨架 | 服务端实现接口、客户端调 NewXxxClient | 客户端方法名与 proto 一一对应,别手写 HTTP |
| 流式控制器 | 服务端靠 Send 返回值判断对端是否已取消 | 客户端断开会得到非 nil 错误,要主动停止生产数据 |
三、错误与超时
| 要点 | 一句话 |
|---|
| 状态码 | 用 status.Error(codes.X, msg),不要用 errors.New 让调用方收到 Unknown |
| 常用码 | InvalidArgument 参数、NotFound 不存在、AlreadyExists 冲突、PermissionDenied 无权限 |
| 可重试码 | Unavailable、DeadlineExceeded、部分 ResourceExhausted,重试前先确认幂等 |
| 不可重试 | InvalidArgument、NotFound、PermissionDenied,重试只是浪费配额 |
| 错误详情 | 用 status.WithDetails(&errdetails.BadRequest{...}) 带上字段级信息 |
| 对外映射 | REST 出口把状态码翻成 HTTP 码,并统一返回 code/message/details 结构 |
| deadline | 每次调用都设超时:context.WithTimeout,并把 ctx 一路传下去 |
| 级联取消 | 上游 deadline 用尽会取消下游,服务端要监听 ctx.Done() 及时收工 |
| 幂等 | 写接口带 request_id,服务端做去重表,重试才真正安全 |
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
resp, err := client.GetUser(ctx, req)
if st, ok := status.FromError(err); ok && st.Code() == codes.DeadlineExceeded {
// 只能靠状态码判断超时,不要用错误字符串匹配
}
四、拦截器与鉴权
| 要点 | 一句话 |
|---|
| 服务端拦截器 | grpc.UnaryInterceptor / grpc.StreamInterceptor,做恢复、日志、指标、鉴权 |
| 顺序 | 多个拦截器用 grpc.ChainUnaryInterceptor(recover, log, auth),注册顺序即执行顺序 |
| 客户端拦截器 | 统一注入 token、traceId 与超时,避免每处调用手写 |
| 上下文透传 | metadata.NewOutgoingContext(ctx, md) 往下传,服务端 metadata.FromIncomingContext 取 |
| 键名规则 | metadata key 自动转小写,别用大写键去取;二进制键以 -bin 结尾 |
| 鉴权 | 令牌校验放拦截器,白名单方法(健康检查、登录)显式放行 |
| 日志 | 记方法名、耗时、状态码、traceId,不记敏感字段与整个 payload |
| 恢复 | defer recover() 把 panic 转成 codes.Internal,并打印堆栈,别让进程挂掉 |
func authUnary(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
md, _ := metadata.FromIncomingContext(ctx) // 白名单方法由 allowed 放行
if !allowed(info.FullMethod) && !validToken(md.Get("authorization")) {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
五、连接与流控
| 要点 | 一句话 |
|---|
| 连接复用 | 一个 *grpc.ClientConn 内部多路复用,全局复用一个即可,别每次调用都 Dial |
| 连接池 | 只有单连接打满(CPU/窗口)时才开小池(2~4 条),盲目开大会浪费内存 |
| 优雅关闭 | conn.Close() 放到进程退出流程,srv.GracefulStop() 等在途请求结束 |
| keepalive | 客户端 keepalive.ClientParameters{Time: 30s, Timeout: 10s},服务端配 EnforcementPolicy 相容 |
| 消息上限 | grpc.MaxRecvMsgSize(4<<20) 默认 4MB;大消息要走流式或改成对象存储直传 |
| 窗口 | 大流式吞吐靠 InitialWindowSize / InitialConnWindowSize,默认 64KB 起步偏小 |
| 负载均衡 | 客户端(round_robin + 服务发现)或服务端代理(Envoy)二选一,别两处都配 |
| 限流 | 服务端并发限额 + 令牌桶两件套,配合 ResourceExhausted 返回 Retry-After |
| 观测点 | 连接数、活跃流数、窗口余量、ResourceExhausted 次数四个指标必看;dns:///svc:9000 要多地址分流需配 round_robin |
srv := grpc.NewServer(
grpc.MaxRecvMsgSize(16<<20),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{MinTime: 10 * time.Second, PermitWithoutStream: true}),
grpc.ChainUnaryInterceptor(recoverUnary, logUnary, authUnary),
)
六、演进与观测
| 要点 | 一句话 |
|---|
| 新增字段 | 用全新编号,旧端忽略未知字段,最安全的演进方式 |
| 删除字段 | 先 reserved 编号与名字,再删代码,永远不要复用编号 |
| 改语义 | 字段含义变化必须换新字段名与编号,代码检查抓不到语义漂移 |
| oneof | 设一个字段会清空同组其它字段,做部分更新要用 FieldMask |
| 版本策略 | 能兼容新增就别开 v2;开了就双版本并行,以调用量归零作为下线依据 |
| 废弃流程 | 标记 deprecated → 双轨 → 停用 → reserved 并删除,四步不能跳 |
| 灰度 | 按 metadata 或流量权重分流,新旧版本必须能互读对方消息 |
| 链路追踪 | REST 路径与 gRPC 方法要能对上,traceId 由入口注入并全程透传 |
| 指标 | 每个方法记 QPS、P50/P90/P99、错误码分布、消息大小直方图 |
| 压测 | ghz 在分离机器上跑,报告写清并发数、连接数与消息大小 |
上线前 60 秒自查
- 超时都设了吗?客户端超时 < 网关超时 < 服务端 deadline?
- 写接口有幂等键吗?可重试码与幂等判定对上了吗?
- 大消息上限与 nginx
client_max_body_size 一致吗?拦截器里有同步阻塞或全量日志吗?
- 令牌与 traceId 能从 HTTP header 一路进到 metadata 吗?
buf breaking 通过了吗?
小结:把这一章当成整套教程的索引——定义侧守住「编号不复用、删除先 reserved、新增给默认语义」,调用侧记住四种模式与「一元优先」,错误侧统一用状态码加 deadline,拦截器只管横切关注点,连接侧复用一个 ClientConn 并按需开小池,演进侧坚持兼容新增加 buf breaking 门禁;每次上线前把最后那份自查清单过一遍。