速查表: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 门禁;每次上线前把最后那份自查清单过一遍。

笔记加载中…