Theory · Microservice · RPC

服务间通信与 RPC

把一次本地函数调用变成跨越网络的可靠请求 —— 桩生成 · 序列化 · 协议 · 传输 · 治理,五层都懂才敢说会 RPC

一句话本质

RPC = 掩盖网络细节的调用抽象:像调本地函数一样调远程,但网络不可靠,所以超时/重试/序列化一样都不能省

主战场

gRPC(HTTP/2 + Protobuf)已是微服务内部通信事实标准:多路复用、四种流模式、跨语言 IDL

本 deck 视角

聚焦架构与选型:协议底层(HTTP/2 帧格式)见 network deck,本 deck 讲通信模式与工程实践

这份 deck 回答"服务间怎么通信":从一次 RPC 的完整链路开始,到 gRPC 的设计与四种流模式,再到序列化选型、REST vs gRPC、治理集成(deadline/metadata/interceptor)、以及 gRPC 长连接在负载均衡上的经典坑。协议编码层(HTTP/2、Protobuf 字节格式)在 network deck 里有完整拆解,这里只讲要点并互链。

Why RPC Matters

先看现场:把服务拆开,最疼的是"调用"这件事

同一件事:拆之前一行,拆之后二十行

// 单体:同一进程内的一次普通函数调用
order := orderService.GetOrder(id)
// 纳秒级 · 同内存 · 参数直接传指针
// 不会失败(要么返回,要么 panic)
// 拆成两个服务后:手工发一次 HTTP 调用
b, _ := json.Marshal(req)
resp, err := http.Post(
  "http://10.0.3.7:8080/order/get", // 地址写死?
  "application/json", bytes.NewReader(b))
// 还要自己处理:超时 · 状态码 · 反序列化
// 重试 · 错误映射 · 日志埋点 · 链路追踪……
关键观察:IP 是写死的,但服务会重启、会扩缩容、会有 20 个实例——这行代码明天就错。更糟的是,这 20 行样板会在每一个调用点重复一遍:100 个调用点就是 2000 行,改一处约定要动 100 个地方。

拆开之后,一次调用多出来的五件事

对方在哪:IP 会变、实例有多个 → 需要服务发现 + 负载均衡;② 参数怎么过去:内存对象出了进程就没意义 → 必须序列化成字节;③ 它可能失败:丢包、超时、对端重启——本地调用没有这个状态,必须显式处理超时与重试;④ 错误语义变了:异常栈变成跨语言的状态码;⑤ 慢了几个数量级:纳秒 → 毫秒(约 1000 倍),延迟预算必须重新算。

RPC 框架换掉了什么

它没有"消灭"这五件事,而是把它们从每个调用点收拢到一处:用 IDL 生成桩函数让调用点回到一行 client.GetOrder(ctx, req),把超时、重试、鉴权、追踪收进拦截器统一配置,把"对方在哪"交给服务发现 + 负载均衡省掉的是重复的样板与漏改的风险,不是网络本身。

本 deck 的路线

先看一次 RPC 的完整链路(第 3 页)→ 拆开看框架的五层组件(第 4 页)→ 再深入 gRPC 依赖的 HTTP/2、四种流模式序列化选型 → 最后是治理三件套实战五坑。读完你应该能回答:"这两行代码之间,到底发生了什么"。

动机页:以同一段业务代码在单体/微服务下的两种写法对比开场,指出“地址写死 + 样板重复”是拆服务后最直接的痛。五件事(寻址/序列化/失败/错误语义/延迟量级)构成后面所有内容的索引。强调 RPC 是“收拢样板”而非“消灭网络”,为后面 deadline 与实战坑页埋伏笔。

Anatomy

一次 RPC 调用到底发生了什么

开场那二十行样板,展开来就是下面这张图——网络只是中间那一段,真正的复杂度在两端的处理

一次 gRPC 调用端到端流程 调用方从本地桩函数进入:经过拦截器链、编码器把请求序列化、按 HTTP/2 帧写出;网络上经过 DNS 与负载均衡选定服务端实例;服务端把帧解码、反序列化并执行业务方法,响应沿相同路径返回。深线标注了超时与重试发生的位置。 CLIENT PROCESS · caller 业务代码 client.GetOrder(req) 生成桩 Stub protoc 生成 · _pb.go 拦截器链 auth·log·tracing·retry 编码 Codec proto.Marshal(req) HTTP/2 传输层 HEADERS(+path/method) + DATA 帧 · 流多路复用 负载均衡 选实例 · 建/复用连接 超时/重试在这里生效 deadline 传播·重试预算 网络传输 SERVER PROCESS · callee HTTP/2 接入 · 解码 · 反序列化 帧解析 → proto.Unmarshal → 路由到方法 服务端拦截器 recover·限流·鉴权 业务实现 server.GetOrder(ctx, req) 编码响应 · HTTP/2 回传 grpc-status / grpc-message 尾帧携带错误 响应沿虚线路径原路返回到业务代码 错误以 status.Code 表达(Unavailable 等) 本地调用与 RPC 的本质差异都在这条链上暴露:延迟(网络 RTT + 序列化)、不可靠(超时/重试必须显式处理)、数据需序列化(不能传指针)、错误从异常变成 status 码。
这张图按请求路径走一遍 RPC 的解剖结构:业务代码调桩函数(protoc 生成的 _pb.go),桩进拦截器链(鉴权/日志/追踪/重试都挂这里),编码器序列化请求,HTTP/2 传输层切帧发出;负载均衡选实例、超时重试在传输层生效;服务端对称地解码、反序列化、路由到方法实现,响应原路返回,错误以 grpc-status 尾帧表达。收尾强调本地调用与 RPC 的四个本质差异。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
RPC
远程过程调用
像调用本地函数一样调用另一台机器上的函数——目标是"掩盖网络",代价是必须自己处理失败
IDL
接口描述语言
用一份中立文件(如 .proto)描述接口,工具据此生成各语言的调用代码

Stub
生成出来的"假函数":长得和真函数一样,内部其实在打包、发包、收包——调用方因此察觉不到网络
序列化 / Codec把内存对象变成能上线的字节(反向叫反序列化);指针与闭包传不过去。负责这一步的可替换组件叫 Codec
ProtobufGoogle 的二进制格式 + IDL:线上只传字段编号不传字段名,所以小且能演进
HTTP/2 Stream一条 TCP 连接上并发的多个逻辑通道;gRPC 的每个请求是一条 stream
Metadata跨服务传递的键值(走 HTTP/2 header):放 trace_id、鉴权 token、灰度标签——不放业务数据
Deadline
截止时间
一次调用的总时间预算,会随请求一跳一跳往下传(剩余时间),到点全链路一起放弃
拦截器
Interceptor
方法级的"洋葱模型"钩子:鉴权、日志、追踪、限流、重试全都挂在这里,不写进业务代码

如果这些概念还不熟,先看这三篇

微服务总览 → 为什么要拆服务(本 deck 一切问题的起点)
HTTP 与 Protobuf 底层原理 → HTTP/2 帧格式与 Protobuf 字节布局的完整拆解(本 deck 只讲架构,不重复底层)
服务注册与发现 → "对方在哪"这一问的完整答案

本 deck 怎么用这些词

统一直觉:一次 RPC = 把"函数名 + 参数"打包成字节 → 交给网络 → 对端拆包、找到那个函数、执行、再把"返回值或错误码"打包送回。不管什么框架,都是这条链路;差别只在于每一环用什么实现、各自的代价是什么

一个提前建立的直觉

后面的内容都能挂到这条链路上:怎么打包 → 序列化;怎么发 → HTTP/2 与流模式;发给谁 → 发现与负载均衡;出错怎么办 → 状态码、deadline、重试;横切需求 → 拦截器。听到新概念先问它属于哪一环。

阅读提示:术语不用背,忘了回来查这一页。真正要记住的只有一句:RPC 掩盖的是网络细节,不是网络风险——超时、重试、错误码一个都省不掉。
前置页:术语按“调用抽象 → 生成物 → 数据形态 → 传输 → 治理”排列,与第 4 页的链路图顺序一致。最小心智模型把 RPC 归纳为四个动作,并给出一张“新概念归到哪一环”的索引表,降低后面 10 页的认知负荷。

Framework Internals

RPC 框架的五层组件:任何框架都能装进这张图

用上一页的心智模型对照:打包=序列化层,怎么发=协议与传输层,发给谁=治理层的发现与负载均衡,横切需求=拦截器。

职责 · gRPC 对应物
IDL & 桩接口描述与代码生成(Protobuf IDL → protoc-gen-go-grpc 生成 stub)——契约先行
序列化对象 ⇄ 字节(Protobuf codec,可换 JSON/msgpack——gRPC 允许自定义 codec)
协议编码方法路由 + 消息分帧(gRPC over HTTP/2:/pkg.Svc/Method 路径 + LENGTH-PREFIXED 帧)
传输连接管理、多路复用、流控(HTTP/2 连接 + stream + WINDOW_UPDATE 流控)
治理拦截器、deadline 传播、重试策略、负载均衡、发现解析(Resolver/Balancer)

为什么 IDL 是核心(而不是"序列化快")

IDL 把接口契约变成可编译的产物:跨语言一致、变更可检(breaking change CI)、文档即代码。gRPC 的生态优势 80% 来自 Protobuf 生态(buf/breaking 检查/grpc-gateway),而不只是二进制编码的速度。

拦截器 = 微服务治理的挂载点

所有横切关注点(鉴权、日志、trace、限流、指标、重试)都应挂在 interceptor 而不是业务代码里。gRPC 拦截器分 unary/stream × client/server 四类,Go 里可链式组合——这是框架选型时"扩展性"的主要考察面。

gRPC 消息帧

1 字节压缩标志 + 4 字节长度前缀 + Protobuf payload——同一条流内按序传输

方法路径

/pkg.Service/Method 作为 :path 伪头——网关凭它路由,无需解析 body

错误模型

status.Code + message + details(结构化错误)——错误也是契约的一部分

五层组件是 RPC 领域的通用坐标系:IDL 与桩(契约先行)、序列化、协议编码(gRPC 用 HTTP/2 path 路由 + 长度前缀分帧)、传输(连接与流控)、治理(拦截器与 LB)。两个升华点:IDL 的真正价值是跨语言契约与变更检查,不只是编码快;拦截器是所有治理能力的挂载点。答框架对比题时逐层比较即可不乱。

Why HTTP/2

gRPC 为什么站在 HTTP/2 上

HTTP/2 给 gRPC 的四件礼物

  • 流多路复用:一个 TCP 连接上并行成百上千个 stream,没有队头阻塞在 stream 层(TCP 层仍有)——内部微服务高频小请求的最佳形态
  • 双向流:stream 天然双向,四种通信模式(下一页)因此成立
  • HPACK 头压缩:重复 header 编码开销低——但注意 gRPC 每请求仍带大量 metadata,跨服务串大字段要克制
  • 标准生态:TLS/代理/网关/云 LB 都认 HTTP/2,无需定制传输层

gRPC 在其上加的东西

  • 路径即方法::path = /pkg.Service/Method(强类型路由)
  • metadata:HTTP header 承载键值(trace id、鉴权 token、灰度标签)
  • 消息分帧:压缩位 + 4 字节长度前缀(length-prefixed message)
  • 尾帧错误:grpc-status/grpc-message 在响应尾部( trailers)——流式场景错误能"最后"通知
  • deadline:grpc-timeout 头(精度到毫秒,如 500m)跨服务传播

长连接的代价:连接粘滞

多路复用意味着一个 TCP 连接承载所有并发——LB 只在建连时分流一次,之后所有请求挤在同一连接(甚至同一后端 Pod)。这是 gRPC 在 K8s ClusterIP 下负载失衡的根源,解法在负载均衡 deck(headless + 客户端 LB / xDS / L7 LB)。

HTTP/2 层的 TCP 队头阻塞

stream 层复用解决了 HTTP/1.1 的队头阻塞,但一个 TCP 包丢失仍阻塞整条连接的所有 stream(传输层重传)。内网低丢包环境影响小;跨公网/高丢包时 QUIC(HTTP/3)用 UDP 多路复用根治——gRPC 社区已有 gRPC-over-HTTP/3 方案(gRFC)推进中。

HTTP/2 给 gRPC 四件礼物:流多路复用、双向流、HPACK 头压缩、标准生态。gRPC 在其上加:方法路径路由、metadata(header)、长度前缀分帧、trailers 错误模型、grpc-timeout 超时传播。两个必考代价:多路复用导致连接粘滞(LB 失衡根源),TCP 层队头阻塞仍在(HTTP/3/QUIC 是根治方向)。

Streaming Patterns

gRPC 四种通信模式:选型的第一维度

1 Unary 一元

rpc GetOrder(GetReq)
  returns (Order);

一问一答。90% 的业务接口;语义等同 HTTP 请求响应。

2 服务端流

rpc WatchUpdates(Query)
  returns (stream Event);

一问多答:订阅推送、大结果集分批(列表导出)、配置下发(long-lived stream)。

3 客户端流

rpc UploadLogs(stream Log)
  returns (Ack);

多问一答:批量上报、分片上传、聚合统计。

4 双向流

rpc Chat(stream Msg)
  returns (stream Msg);

实时对话:IM、实时转写、游戏状态同步。收发可异步交叠。

流式接口的工程要点

  • 心跳保活:长流要 Keepalive(gRPC keepalive.ClientTime)+ 应用层 ping,防 NAT/LB 空闲超时断流
  • 背压:HTTP/2 流控(WINDOW_UPDATE)天然提供端到端背压——服务端发太快会被流控窗口限速
  • 重连语义:流断了要恢复,应用层记录游标/序号(exactly-once 另议,见幂等 deck)
  • 超时语义:deadline 对流式 = 整条流的总预算;逐条超时要在应用层设计

选型话术

默认 Unary(简单、好治理、好监控);不要为了炫技用流——流式接口在网关/负载均衡/重试/监控上的治理成本数倍于 Unary。真实用例:LLM 流式输出(服务端流)、日志采集(客户端流)、实时协作(双向流)。

四种模式按"消息方向"记:一元一问一答、服务端流一问多答(订阅/LLM 输出)、客户端流多问一答(批量上报)、双向流实时对话。工程要点四条:心跳保活、HTTP/2 流控背压、断线重连要应用层游标、deadline 是整条流的总预算。选型话术:默认 Unary,流式由业务语义驱动而非性能驱动,治理成本高数倍。

Wire Format

Protobuf 编码要点:小、快、可演进

三个核心设计

  • Tag + 变长整数(varint):每个字段是 (field_number << 3) | wire_type 的 tag 加上变长值——小数字 1 字节,省 60-80% 体积
  • ZigZag 编码:负数在 varint 下会膨胀到 10 字节,sint32/sint64 用 ZigZag(0,1,-1,2,-2…)把负数拉回小值
  • 无字段名:线上只传字段编号——体积小且天然兼容:编号就是契约,删改编号=破坏兼容

兼容性规则(必须背)

  • 新增字段:老代码跳过未知字段(unknown fields),向前兼容 ✓
  • 删除字段:编号必须 reserved,否则未来复用编号 = 数据错乱
  • 改字段编号 = 删字段+加字段,同样破坏兼容
  • sint32 换 int32 等类型升级不兼容(wire type 变了)
  • 工具化保障:buf breaking 在 CI 里自动检测违规变更
3-10×相对 JSON 的体积压缩量级(数字多、字段多时更显著)
编号即契约proto 文件唯一的稳定承诺:注释可以改、名字可以改(仅编译期),编号不能动
深拆见 network deck字节布局、wire type 全表、性能对比 → HTTP 与 Protobuf 底层原理

面试陷阱题:"为什么 Protobuf 比 JSON 快?"——两个原因权重不同:体积小(网络/CPU 拷贝少)与解析省(不用词法分析字符串键,按 tag 直取)。但 JSON 也有 Protobuf 没有的:人类可读、动态 schema、浏览器原生——对外 API 仍是 JSON 的天下。

Protobuf 三核心:tag+varint 变长、ZigZag 处理负数、线上无字段名(编号即契约)。兼容性规则是必背区:新增字段安全、删除必须 reserved 编号、改编号等于删+加。快的原因权重:体积小 + 按编号直取无需词法分析。结尾反转:对外 API 仍是 JSON 天下(可读、动态 schema),内外分离是标准架构。

Serialization Landscape

序列化协议对比与选型

协议类型体积/速度跨语言可读性典型场景
JSON文本大 / 中(编码快,解析慢)天然✓ 人类可读对外 API、Web、调试友好场景
Protobuf二进制 + IDL小 / 快✓ 强(代码生成)✗(需工具)内部 RPC、存储序列化、K8s API
Thrift二进制 + IDL小 / 快✓ 强历史大厂体系(Meta/Hive)、Kitex 支持
MessagePack二进制较小 / 快✓(弱契约)无 IDL 的二进制 JSON 替代
Hessian二进制中 / 中Java 系为主Dubbo 老生态(自描述但 Java 耦合)
XML / SOAP文本很大 / 慢△ 冗长存量 SOA/金融协议,新项目不用

选型三问

  • 边界在哪:对外(可读/可调试 → JSON)还是内部(性能/契约 → Protobuf)
  • 有没有 IDL 需求:跨语言强契约 → 带 IDL 的协议;事件驱动消息体 → Protobuf 也用于 MQ payload
  • 演进要求:schema 变更频繁 → 选兼容性机制成熟的(Protobuf 规则明确)

架构上最常见的组合

外部 REST/JSON + 内部 gRPC/Protobuf:网关处协议转换(grpc-gateway / APISIX 插件 / 自研 gateway 层)。注意 MQ 场景建议消息体用 Protobuf + schema registry 管理(省体积 + 版本演进),HTTP 调试路径另配转换工具。

序列化对比表六行:JSON(对外)、Protobuf(内部标配)、Thrift(历史体系)、MessagePack(无 IDL 二进制)、Hessian(Dubbo 生态)、XML(存量)。选型三问:边界、契约、演进。落地组合就是"外 JSON 内 Protobuf + 网关转换",MQ 消息体也建议 Protobuf。答题先分层再给结论,别一上来就背表。

API Style

REST vs gRPC:不是替代关系,是分层关系

维度REST(HTTP/1.1 或 2 + JSON)gRPC(HTTP/2 + Protobuf)
契约OpenAPI 描述,约束弱(可选校验)Protobuf IDL 强契约,编译期检查 + breaking 检测
性能文本体积大、连接常复用不足(HTTP/1.1 队头阻塞)二进制小、多路复用、单连接高并发
流式SSE/WebSocket 另行实现四种流模式原生统一
生态浏览器/curl/CDN/缓存全兼容需要 stub 生成与工具链;浏览器需 gRPC-Web 代理
治理成熟(网关/缓存/CDN 直接可用)拦截器+xDS 体系;基础设施需认 HTTP/2

对外 → REST

移动端/Web/第三方:JSON 可读可调试、浏览器零成本、CDN 与 HTTP 缓存可用;REST 语义(GET 幂等、缓存控制)在公网才有价值

内部 → gRPC

服务间高频调用:强契约 + 低延迟 + 流式;K8s/Skaffold 生态(CRD 也是 Protobuf 系)全面原生

转换层

grpc-gateway(注解生成反向代理)、APISIX/Kong 的 gRPC 插件、Envoy gRPC-JSON transcoder——一份 proto 同时产出两套 API

追问"为什么内部不全用 REST"——高频内部调用里 JSON 的体积与解析开销、HTTP/1.1 连接低效、无统一流式语义;追问"为什么对外不全用 gRPC"——浏览器兼容、调试成本、公网中间盒(CDN/防火墙)对 HTTP/2 之外的扩展头支持差。两边都是"生态与可运营性"优先。

REST vs gRPC 的正解是分层:对外 REST(浏览器生态、可调试、CDN 缓存)、内部 gRPC(契约、性能、流式)。对比表五维:契约强度、性能、流式、生态、治理。转换层三选项:grpc-gateway、网关插件、Envoy transcoder。两个反问(为什么不内部全 REST/对外全 gRPC)准备好,这是追问的必经之路。

Governance Hooks

deadline 传播 · metadata · 拦截器:RPC 的治理三件套

1 Deadline 传播

入口设 500ms,每跳 RPC 自动继承剩余时间(grpc-timeout 头),任何一跳超时立即返回 DeadlineExceeded。

ctx, cancel := context.WithTimeout(
    ctx, 500*time.Millisecond)
defer cancel()
// 下游自动收到剩余 deadline
client.Get(ctx, req)

意义:防止上游已放弃、下游还在苦算——没有传播,超时只保护第一跳。

2 Metadata

跨服务的键值通道(HTTP/2 header):trace-id、auth token、灰度标签、租户 ID。

md := metadata.Pairs(
  "x-user-tag", "vip-a")
ctx = metadata.NewOutgoingContext(
  ctx, md)

约束:key 小写 ASCII、值不超 HTTP 头限制——别拿 metadata 传业务数据,大字段放 payload。

3 Interceptor

unary/stream × client/server 四类,链式组合——治理能力的唯一挂载点。

grpc.ChainUnaryInterceptor(
  otelInterceptor,   // trace
  authInterceptor,   // 鉴权
  ratelimitInterp,   // 限流
  retryInterceptor,  // 重试
)

顺序有讲究:trace 最外(全链路可见),重试内层(每次重试都带 trace)。

重试必须配幂等(详见容错与幂等 deck)

gRPC 服务端重试策略(service config / retryPolicy)自动重试 Unavailable;但 Unavailable 的请求可能已在服务端执行成功(响应丢失)——非幂等接口重试 = 重复下单。组合拳:只有幂等方法开启重试 + 重试预算(防止重试风暴)。

观测三件套的落地

OpenTelemetry 的 gRPC 拦截器自动注入 trace context(W3C traceparent 放在 metadata);Prometheus 拦截器统计每方法 QPS/延迟分布/错误率(RED 指标);日志拦截器打印 method/code/latency。三行配置获得全链路可观测——这是拦截器价值的最佳展示。

治理三件套:deadline 传播(grpc-timeout 头自动继承剩余预算,防止上游放弃下游苦算)、metadata(trace/鉴权/灰度标签的通道,别传业务大字段)、拦截器(四类拦截器链,trace 最外重试最内)。联动知识:gRPC 内置重试策略必须配幂等与重试预算(指向容错/幂等 deck);OTel/Prometheus 拦截器三行配置拿下可观测。

Landscape

RPC 框架版图:gRPC 之外还有什么

框架出品协议/序列化特点与定位
gRPCGoogle / CNCFHTTP/2 · Protobuf跨语言事实标准;生态最全(xDS/OTel/网关);Go 客户端 v1.83(2026-08)
ThriftMeta(Apache)TCP/多传输 · Thrift IDL老牌跨语言;传输层可换(socket/framed);存量系统多,新项目少选
Dubbo 3Apache(阿里)Triple(HTTP/2 兼容 gRPC)· 多序列化Java 系微服务全家桶;3.x 应用级服务发现 + Triple 协议向 gRPC 生态靠拢
Kitex字节 CloudWeGo多协议(Thrift/Protobuf/HTTP2)Go 高性能 RPC:强类型代码生成、内置治理、对 K8s/Mesh 友好;配套 Hertz(HTTP)
bRPC百度(Apache)baidu_std/HTTP/多种C++ 超高性能(单机百万 QPS 级);Go 系公司少用,面试属于"了解"级
tRPC-Go / Dubbo-go腾讯 / 社区各系协议公司内框架代表:自带治理与多环境体系——面试常问"你们框架封装了什么"

自研/公司框架的常见封装(答"你们用啥")

在 gRPC 之上包一层:统一 IDL 仓库 + CI 兼容检查、默认拦截器集(trace/log/recover/限流)、注册发现与配置接入、泛化调用(网关转发)、代码脚手架。框架的价值=把治理默认值做好,业务只写 handler。

Dubbo 3 的应用级发现(亮点概念)

2.x 按接口注册(实例数 × 接口数爆炸);3.x 改为应用级服务发现(对齐 K8s/K8s Service 模型,注册中心只存应用→实例映射,接口路由信息放到元数据中心)——回答"注册中心数据量怎么优化"时的现成案例。

RPC 版图按系记:Google 系 gRPC(跨语言标准)、Meta 系 Thrift(存量)、阿里系 Dubbo 3(应用级发现+Triple 协议)、字节系 Kitex(Go 高性能)。公司框架的价值在"治理默认值":统一 IDL 仓库、默认拦截器、泛化调用。Dubbo 3 应用级服务发现是回答"注册中心数据量优化"的现成案例,务必记住。

Field Notes

gRPC 实战五坑(面试讲这些 = 真用过)

坑 1:负载失衡(最高频)

现象:N 个 Pod,CPU 一个 90% 其余 10%。根因:HTTP/2 长连接 + 多路复用,LB 建连时一次分流,之后请求全挤一条连接。解法梯度:

  • headless service + 客户端 LB(gRPC 内置 rr/pickfirst 或 xDS)
  • L7 LB / Mesh(每个请求级分流)
  • 客户端 keepalive 周期重建连接(兜底缓解,不根治)

→ 详见 负载均衡 deck 的 gRPC 章节

坑 2:MaxRecvMsgSize 默认 4MB

大文件/大列表直接 ResourceExhausted。正解:分页 + 流式(服务端流或客户端流分片),而不是调大上限——调大上限是把 OOM 风险转移给服务端。4MB 是故意设计的"反模式警报"。

坑 3:keepalive 被代理掐断

长连接空闲时被 LB/NAT 静默断掉,客户端下一次请求报 Unavailable。配置双侧:客户端 PermitWithoutStream + Time 心跳,服务端 EnforcementPolicy 放行;中间 LB 的 idle timeout 要大于心跳间隔。

坑 4:错误被吞成 Unknown

handler panic 默认变 Unknown / 连接直接断。必须有 recover 拦截器:转成 Internal + 保留堆栈日志;跨服务错误要用 status.New(code, msg).WithDetails() 保留语义,别用裸 error 字符串穿透。

坑 5:deadline 没设或设太死

不设 deadline = 永远等(goroutine 泄漏放大);设太死 = 正常长查询被 DeadlineExceeded。实践:入口按 API 定 SLO 预算,逐跳继承;数据库查询超时必须 < RPC deadline,否则下游先挂上游才超时,时序错乱。

一句话总结

gRPC 的坑几乎都源于"长连接 + 多路复用 + 强类型"这三个特性:连接相关(失衡/keepalive)、契约相关(4MB/错误模型)、预算相关(deadline)。治理默认值配好,坑就填了 80%。

五个实战坑:负载失衡(长连接建连分流,解法梯度 headless+客户端 LB → L7/Mesh)、4MB 消息上限(正解是分页流式不是调大上限)、keepalive 被中间设备掐断(双侧配置)、panic 变 Unknown(recover 拦截器 + status.WithDetails)、deadline 缺失或与 DB 超时倒挂。总结规律:坑都来自长连接、多路复用、强类型三个特性。

Cheat Sheet

一页带走:链路 · 五层 · 选型 · 治理

① 与本地调用的四点本质差异(总纲)

延迟纳秒 → 毫秒(约 1000 倍):RTT + 序列化 + 排队
可靠性报文可丢可重排 → 超时、重试必须显式处理
数据只能传(序列化),指针与闭包跨进程失效
错误异常栈 → 跨语言状态码(gRPC 用 grpc-status 尾帧)

② 五层组件与 gRPC 对应物

IDL 与桩Protobuf IDL → protoc-gen-go-grpc 生成 stub(契约先行)
序列化Protobuf codec(可换 JSON)——编号即契约,删字段要 reserved
协议编码:path = /pkg.Service/Method + 长度前缀分帧(LENGTH-PREFIXED)
传输HTTP/2:一连接多 stream + WINDOW_UPDATE 流控(天然背压)
治理拦截器链、deadline 传播、重试策略、Resolver/Balancer

③ 选型:边界决定协议

对外REST + JSON:可读可调试、浏览器零成本、CDN 可用
内部gRPC + Protobuf:强契约、低延迟、流式原生
衔接grpc-gateway / Envoy transcoder:一份 proto 出两套 API

④ 四种流模式:由业务语义驱动

Unary(默认)一问一答;90% 的接口,治理成本最低
服务端流一问多答:订阅推送、LLM 流式输出、分批导出
客户端流多问一答:批量上报、分片上传
双向流实时对话:IM、实时协作、游戏状态同步
纪律别为了炫技用流——流在网关/重试/监控上的治理成本数倍于 Unary

⑤ 治理三件套

Deadline入口设预算(如 500ms),剩余时间逐跳传播;不传播 = 上游放弃、下游苦算
Metadatatrace / 鉴权 / 灰度标签的通道;key 小写、别传大字段
Interceptor四类(unary/stream × client/server)链式;trace 最外、重试最内

⑥ 五坑速记(都源于"长连接 + 复用 + 强类型")

· 负载失衡:LB 建连时分流一次 → headless + 客户端 LB / L7 LB
· 4MB 上限:正解是分页或流式,不是调大上限(那是把 OOM 转移给服务端)
· keepalive 被掐:双侧心跳,LB 的 idle timeout 要大于心跳间隔
· panic 变 Unknown:recover 拦截器 + status.WithDetails()deadline 倒挂:DB 超时必须 < RPC deadline
速查页:六块按理解顺序排(先差异总纲、再五层、再选型、再流模式、再治理、最后实战坑)。四差异回指第 2 页,五层回指第 4 页,选型回指第 9-10 页,治理与五坑回指第 11-12 页。

Interview QA · 1/2

高频 QA(一):原理与机制

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回上一页速查表。

Q1 RPC 和本地调用的本质区别?

四差异

① 延迟:网络 RTT + 序列化,量级从纳秒到毫秒;② 可靠性:报文可丢可重排,必须显式超时重试;③ 数据:只能传值(序列化),指针/闭包失效;④ 错误:从异常变成跨语言状态码。RPC 框架的全部设计都在消化这四个差异——"透明化"是目标,但假装没网络是灾难。

Q2 gRPC 的多路复用解决了什么?没解决什么?

stream 层复用TCP 层仍在

解决:HTTP/1.1 的请求级队头阻塞——一条连接并发数百 stream,高并发小请求不再排队建连。没解决:TCP 层队头阻塞(丢包重传阻塞整条连接),内网影响小、公网高丢包明显,根治要 HTTP/3/QUIC。另一面:复用让"连接"不再对应"请求",负载均衡必须转向请求级或客户端级。

Q3 Protobuf 为什么快?和 JSON 怎么选?

体积小按 tag 直取边界分流

快在两点:体积(varint + 无字段名,省 60-80%)与解析(按编号直取,无需字符串词法分析)。选型按边界:对外 API 要可读可调试 → JSON;内部 RPC 要契约与性能 → Protobuf;MQ 消息体建议 Protobuf + 版本演进规则。详见序列化对比页。

Q4 deadline 传播是怎么实现的?为什么重要?

grpc-timeout 头剩余预算

gRPC 把 context 的 deadline 换算成 grpc-timeout 头(如 300m)随请求传播,下游把"剩余时间"装回自己的 context,继续向更下游传播。重要性:没有传播,A 层超时放弃后 B/C/D 还在消耗资源——超时只是"客户端不再等",传播才让全链路"及时止损"。

Q5 HTTP/2 的流控(flow control)是什么?

WINDOW_UPDATE背压

HTTP/2 每条连接与每条 stream 各有接收窗口(默认 64KB),接收方通过 WINDOW_UPDATE 扩窗——发送方超窗必须暂停。对 gRPC 的意义:端到端背压,慢消费者拖慢快生产者而不是 OOM;流式接口大吞吐要调 BDP 优化窗口(gRPC-Go 的 initialWindowSize 选项)。

Q6 拦截器和 HTTP 中间件的区别?

类型化双向

语义同源(洋葱模型,前后都能插逻辑),差异在:拦截器是方法级(能拿到 method 名、req/resp 强类型、status code,便于按方法治理);中间件是路径级(HTTP 层)。gRPC 里两者并存:网关 HTTP 层做通用治理,RPC 层做业务治理。

上半场六题偏原理。Q1 四差异是总纲;Q2 多路复用要答"解决了 stream 层、没解决 TCP 层、改变了 LB 假设"三层;Q3 快在体积与按编号直取;Q4 deadline 传播的痛点是"上游放弃下游苦算";Q5 流控即 WINDOW_UPDATE 背压,流式大吞吐要调窗;Q6 拦截器与中间件同源不同层。

Interview QA · 2/2

高频 QA(二):选型与实战

同样建议先自答。这一页的题都要"结论 + 一句代价/边界"才完整——只答结论容易被追问。

Q7 内部服务调用选 REST 还是 gRPC?

内外分离网关转换

默认内部 gRPC:强契约(IDL+breaking 检查)、低延迟(二进制+复用)、流式原生。特例:调试工具链要求 HTTP+JSON 的团队、纯 CRUD 且变更频繁的领域(Protobuf 契约管理成本高于收益)。对外统一 REST,网关转换衔接。选型的决定性因素是团队工程能力而非纯性能。

Q8 接口要返回 100 万行数据怎么办?

分页流式异步导出

三选一按场景:① 分页(cursor 分页,用户交互场景);② 服务端流分批推送(程序消费、边生成边传);③ 异步导出:生成文件 → 对象存储 → 通知下载(报表场景)。反模式:调大 MaxRecvMsgSize 一次性拉全量——内存放大 + 超时 + 重试成本全炸。

Q9 gRPC 服务怎么做版本管理?

package 版本只加不删

Proto 文件按 package 版本化(order.v1 / order.v2),新旧并行、按消费者迁移后下线旧版;变更纪律:字段只加不删不编号复用、reserved 标记退役编号、CI 跑 buf breaking;兼容层:v2 handler 内组合调用 v1 逻辑,避免代码翻倍。

Q10 幂等性怎么和 RPC 重试配合?

只重试幂等重试预算

原则:只有幂等方法开启自动重试(GET/状态机类),非幂等(下单/支付)要么不重试、要么带幂等键(idempotency key 放 metadata,服务端去重)。gRPC 服务端重试要配 retryThrottling(预算),防止一个实例抖动引发全链路重试风暴。详见幂等 deck。

Q11 网关怎么转发 gRPC?

L7 透传transcoder

两条路:① 透传型(APISIX/Envoy 原生支持 HTTP/2 gRPC 代理,按 :path 路由到后端,性能最好);② 转换型(grpc-gateway / Envoy transcoder 把 JSON REST 翻译成 gRPC,代价是编解码开销)。公共治理(鉴权/限流)在网关层做,业务治理在拦截器层做,两层别重复。

Q12 服务间传大对象(图片/文件)走 RPC 合适吗?

数据面旁路

不合适。RPC 通道适合小消息高频调用;大文件走"控制面+数据面分离":RPC 传元数据与凭证 → 客户端直传对象存储(预签名 URL/分片)→ 传完回调或事件通知。收益:RPC 连接不被大流量占死(流控窗口不被单流吃光)、存储服务天然扩展。

下半场六题偏选型实战。Q7 决定性因素是团队工程能力;Q8 百万行三解:分页、服务端流、异步导出,反模式是调大消息上限;Q9 版本化三件套:package 版本、只加不删、buf breaking;Q10 重试配幂等键与预算;Q11 网关两条路:透传与转码,治理分两层别重复;Q12 大对象走数据面旁路(对象存储+预签名)。

Related & References

相关知识点与参考

本领域相关 deck

跨领域相关 deck

收尾链接:协议底层(HTTP/2 帧与 Protobuf 字节)在 network deck;领域内连接负载均衡(长连接失衡)、发现、容错、幂等、网关;跨领域对照 TCP 连接管理与 Kafka(请求响应 vs 事件驱动两极)。参考链接全部官方文档:gRPC 核心概念与协议规范、Protobuf encoding 与兼容规则、grpc-go service config 与 keepalive。