bigywater.tech
gRPC · 29

gRPC-Gateway:把 gRPC 暴露成 REST 的转换成本

把 gRPC 服务暴露成 REST,中间那层 grpc-gateway 到底做了什么、花多少?错误码怎么翻译、头怎么传、超时和流怎么办?这篇用真实生成的 gateway 代码跑了一遍。

11 种 HTTP 状态
16 个 gRPC 状态码被 gateway 映射成 11 种不同的 HTTP 状态,多对一
5.0 倍
100 KiB 消息经 gateway 的 REST 相比直连 gRPC 的单次耗时倍数
x-custom 默认被丢掉
自定义 HTTP 头要用 WithIncomingHeaderMatcher 放行才会转成 metadata

它是什么

实测 · 本机执行输出
  POST /v1/echo → HTTP 200,{"id":"a","name":"alice","count":"123","payload":"aGVsbG8=","tags":["x","y"]}
  GET /v1/items/abc?name=bob → HTTP 200,{"id":"abc","name":"bob","count":"1","payload":"","tags":[]}
  GET /v1/items/abc?nope=1(未知查询参数)→ HTTP 200,{"id":"abc","name":"","count":"1","payload":"","tags":[]}
  GET /v1/nothing → HTTP 404,{"code":5,"message":"Not Found","details":[]}
  DELETE /v1/echo(路径存在但方法不对)→ HTTP 501,{"code":12,"message":"Method Not Allowed","details":[]}
  POST 非法 JSON → HTTP 400,{"code":3,"message":"unexpected EOF","details":[]}

从带 google.api.http 注解的 .proto 生成的反向代理:HTTP/JSON 请求进来,转成 gRPC 调用转发给后端,再把响应转回 JSON。

状态码映射

实测 · 本机执行输出
  gRPC Canceled           → HTTP 499
  gRPC Unknown            → HTTP 500
  gRPC InvalidArgument    → HTTP 400  {"code":3,"message":"failed with 3","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"BAD","domain":"demo.example","metadata":{}}]}
  gRPC DeadlineExceeded   → HTTP 504
  gRPC NotFound           → HTTP 404
  gRPC AlreadyExists      → HTTP 409
  gRPC PermissionDenied   → HTTP 403
  gRPC ResourceExhausted  → HTTP 429
  gRPC FailedPrecondition → HTTP 400
  gRPC Aborted            → HTTP 409
  gRPC OutOfRange         → HTTP 400
  gRPC Unimplemented      → HTTP 501
  gRPC Internal           → HTTP 500
  gRPC Unavailable        → HTTP 503
  gRPC DataLoss           → HTTP 500
  gRPC Unauthenticated    → HTTP 401

gRPC 的 16 种错误码映射到 HTTP 状态,是多对一的(400、500、409 各对应多个),响应体里保留原始的 code、message 和 details。

成本

实测 · 本机执行输出
  payload      0 字节:protobuf 26 字节,JSON 响应体 85 字节(3.27 倍);单次中位数 直接 gRPC 68 µs、经 gateway 的 REST 146 µs(2.1 倍)
  payload    100 字节:protobuf 128 字节,JSON 响应体 221 字节(1.73 倍);单次中位数 直接 gRPC 67 µs、经 gateway 的 REST 148 µs(2.2 倍)
  payload 102400 字节:protobuf 102430 字节,JSON 响应体 136621 字节(1.33 倍);单次中位数 直接 gRPC 246 µs、经 gateway 的 REST 1220 µs(5.0 倍)
  16 个并发、小消息:直接 gRPC 59336 请求/秒,经 gateway 的 REST 25095 请求/秒(2.4 倍)

JSON 比 protobuf 大(gateway 默认还输出零值字段,bytes 用 base64),多一跳转发加上 JSON 编解码,单次耗时和吞吐都不如直连 gRPC。本机上 HTTP/1.1 的 REST 客户端与 gRPC 的差别包含协议的差别。

头与 metadata

实测 · 本机执行输出
  默认:服务端 metadata(去掉标准头)= {"id":"","name":"","count":"0","payload":"","tags":["authorization=Bearer t1","foo=f1","grpcgateway-authorization=Bearer t1","grpcgateway-user-agent=Go-http-client/1.1","x-forwarded-for=127.0.0.1","x-forwarded-host=127.0.0.1:9807"]}
  WithIncomingHeaderMatcher 放行 X-Custom:{"id":"","name":"","count":"0","payload":"","tags":["authorization=Bearer t1","foo=f1","grpcgateway-authorization=Bearer t1","grpcgateway-user-agent=Go-http-client/1.1","x-custom=c1","x-forwarded-for=127.0.0.1","x-forwarded-host=127.0.0.1:9809"]}

Authorization 和 Grpc-Metadata- 前缀的头会被转发,其它自定义头默认不转发。

超时与取消

实测 · 本机执行输出
  HTTP 客户端超时(102.1 ms 后放弃,err 非空=true);后端 handler 的结果:"canceled"
  Grpc-Timeout: 300m:HTTP 504,303.2 ms 后返回;{"code":4,"message":"context deadline exceeded","details":[]}

HTTP 客户端断开会取消后端的调用;Grpc-Timeout 头可以设置后端的 deadline。

流

实测 · 本机执行输出
  HTTP 200,响应体共 3 行:
      {"result":{"id":"0","name":"","count":"0","payload":"","tags":[]}}
      {"result":{"id":"1","name":"","count":"0","payload":"","tags":[]}}
      {"result":{"id":"2","name":"","count":"0","payload":"","tags":[]}}

服务端流变成逐行 JSON(每行包一层 result)。客户端流和双向流默认不支持。

分步演示

gRPC-Gateway
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。本篇用 grpc-gateway v2.31.0(runtime 与 protoc-gen-grpc-gateway 都是这个版本),为此 go.mod 里 golang.org/x/net 升到 v0.59.0,genproto 等也相应升级(前面的文章用的是 x/net v0.57.0);google/api 的两个 .proto 是我按公开定义手写进 protoc 的 include 目录的最小版本;HTTP 侧用 Go 的 net/http(HTTP/1.1 keep-alive);耗时与吞吐是本机回环、同进程内的测量
    • 没有验证:OpenAPI 文档生成(protoc-gen-openapiv2);自定义 JSON 序列化选项(UseProtoNames、EmitUnpopulated 关闭);更复杂的路径模板与 body 绑定;双向流经 WebSocket 的转发;鉴权中间件与 gateway 的搭配;gateway 与后端分开部署时多出的网络跳数;其它语言的 gateway(如 Envoy 的 gRPC-JSON transcoder)的行为
    京ICP备2026067502号-1京公网安备11010502063487号