Theory · Microservice · RPC
把一次本地函数调用变成跨越网络的可靠请求 —— 桩生成 · 序列化 · 协议 · 传输 · 治理,五层都懂才敢说会 RPC
RPC = 掩盖网络细节的调用抽象:像调本地函数一样调远程,但网络不可靠,所以超时/重试/序列化一样都不能省
gRPC(HTTP/2 + Protobuf)已是微服务内部通信事实标准:多路复用、四种流模式、跨语言 IDL
聚焦架构与选型:协议底层(HTTP/2 帧格式)见 network deck,本 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 会变、实例有多个 → 需要服务发现 + 负载均衡;② 参数怎么过去:内存对象出了进程就没意义 → 必须序列化成字节;③ 它可能失败:丢包、超时、对端重启——本地调用没有这个状态,必须显式处理超时与重试;④ 错误语义变了:异常栈变成跨语言的状态码;⑤ 慢了几个数量级:纳秒 → 毫秒(约 1000 倍),延迟预算必须重新算。
它没有"消灭"这五件事,而是把它们从每个调用点收拢到一处:用 IDL 生成桩函数让调用点回到一行 client.GetOrder(ctx, req),把超时、重试、鉴权、追踪收进拦截器统一配置,把"对方在哪"交给服务发现 + 负载均衡。省掉的是重复的样板与漏改的风险,不是网络本身。
先看一次 RPC 的完整链路(第 3 页)→ 拆开看框架的五层组件(第 4 页)→ 再深入 gRPC 依赖的 HTTP/2、四种流模式、序列化与选型 → 最后是治理三件套与实战五坑。读完你应该能回答:"这两行代码之间,到底发生了什么"。
Anatomy
开场那二十行样板,展开来就是下面这张图——网络只是中间那一段,真正的复杂度在两端的处理。
Prerequisites & Glossary
| 术语 | 一句话理解(先记住这个,细节后面展开) |
|---|---|
| RPC 远程过程调用 | 像调用本地函数一样调用另一台机器上的函数——目标是"掩盖网络",代价是必须自己处理失败 |
| IDL 接口描述语言 | 用一份中立文件(如 .proto)描述接口,工具据此生成各语言的调用代码 |
| 桩 Stub | 生成出来的"假函数":长得和真函数一样,内部其实在打包、发包、收包——调用方因此察觉不到网络 |
| 序列化 / Codec | 把内存对象变成能上线的字节(反向叫反序列化);指针与闭包传不过去。负责这一步的可替换组件叫 Codec |
| Protobuf | Google 的二进制格式 + IDL:线上只传字段编号不传字段名,所以小且能演进 |
| HTTP/2 Stream | 一条 TCP 连接上并发的多个逻辑通道;gRPC 的每个请求是一条 stream |
| Metadata | 跨服务传递的键值(走 HTTP/2 header):放 trace_id、鉴权 token、灰度标签——不放业务数据 |
| Deadline 截止时间 | 一次调用的总时间预算,会随请求一跳一跳往下传(剩余时间),到点全链路一起放弃 |
| 拦截器 Interceptor | 方法级的"洋葱模型"钩子:鉴权、日志、追踪、限流、重试全都挂在这里,不写进业务代码 |
微服务总览 → 为什么要拆服务(本 deck 一切问题的起点)
HTTP 与 Protobuf 底层原理 → HTTP/2 帧格式与 Protobuf 字节布局的完整拆解(本 deck 只讲架构,不重复底层)
服务注册与发现 → "对方在哪"这一问的完整答案
统一直觉:一次 RPC = 把"函数名 + 参数"打包成字节 → 交给网络 → 对端拆包、找到那个函数、执行、再把"返回值或错误码"打包送回。不管什么框架,都是这条链路;差别只在于每一环用什么实现、各自的代价是什么。
后面的内容都能挂到这条链路上:怎么打包 → 序列化;怎么发 → HTTP/2 与流模式;发给谁 → 发现与负载均衡;出错怎么办 → 状态码、deadline、重试;横切需求 → 拦截器。听到新概念先问它属于哪一环。
Framework Internals
用上一页的心智模型对照:打包=序列化层,怎么发=协议与传输层,发给谁=治理层的发现与负载均衡,横切需求=拦截器。
| 层 | 职责 · 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 把接口契约变成可编译的产物:跨语言一致、变更可检(breaking change CI)、文档即代码。gRPC 的生态优势 80% 来自 Protobuf 生态(buf/breaking 检查/grpc-gateway),而不只是二进制编码的速度。
所有横切关注点(鉴权、日志、trace、限流、指标、重试)都应挂在 interceptor 而不是业务代码里。gRPC 拦截器分 unary/stream × client/server 四类,Go 里可链式组合——这是框架选型时"扩展性"的主要考察面。
1 字节压缩标志 + 4 字节长度前缀 + Protobuf payload——同一条流内按序传输
/pkg.Service/Method 作为 :path 伪头——网关凭它路由,无需解析 body
status.Code + message + details(结构化错误)——错误也是契约的一部分
Why HTTP/2
:path = /pkg.Service/Method(强类型路由)500m)跨服务传播多路复用意味着一个 TCP 连接承载所有并发——LB 只在建连时分流一次,之后所有请求挤在同一连接(甚至同一后端 Pod)。这是 gRPC 在 K8s ClusterIP 下负载失衡的根源,解法在负载均衡 deck(headless + 客户端 LB / xDS / L7 LB)。
stream 层复用解决了 HTTP/1.1 的队头阻塞,但一个 TCP 包丢失仍阻塞整条连接的所有 stream(传输层重传)。内网低丢包环境影响小;跨公网/高丢包时 QUIC(HTTP/3)用 UDP 多路复用根治——gRPC 社区已有 gRPC-over-HTTP/3 方案(gRFC)推进中。
Streaming Patterns
rpc GetOrder(GetReq) returns (Order);
一问一答。90% 的业务接口;语义等同 HTTP 请求响应。
rpc WatchUpdates(Query) returns (stream Event);
一问多答:订阅推送、大结果集分批(列表导出)、配置下发(long-lived stream)。
rpc UploadLogs(stream Log) returns (Ack);
多问一答:批量上报、分片上传、聚合统计。
rpc Chat(stream Msg) returns (stream Msg);
实时对话:IM、实时转写、游戏状态同步。收发可异步交叠。
keepalive.ClientTime)+ 应用层 ping,防 NAT/LB 空闲超时断流默认 Unary(简单、好治理、好监控);不要为了炫技用流——流式接口在网关/负载均衡/重试/监控上的治理成本数倍于 Unary。真实用例:LLM 流式输出(服务端流)、日志采集(客户端流)、实时协作(双向流)。
Wire Format
(field_number << 3) | wire_type 的 tag 加上变长值——小数字 1 字节,省 60-80% 体积sint32/sint64 用 ZigZag(0,1,-1,2,-2…)把负数拉回小值buf breaking 在 CI 里自动检测违规变更面试陷阱题:"为什么 Protobuf 比 JSON 快?"——两个原因权重不同:体积小(网络/CPU 拷贝少)与解析省(不用词法分析字符串键,按 tag 直取)。但 JSON 也有 Protobuf 没有的:人类可读、动态 schema、浏览器原生——对外 API 仍是 JSON 的天下。
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/金融协议,新项目不用 |
外部 REST/JSON + 内部 gRPC/Protobuf:网关处协议转换(grpc-gateway / APISIX 插件 / 自研 gateway 层)。注意 MQ 场景建议消息体用 Protobuf + schema registry 管理(省体积 + 版本演进),HTTP 调试路径另配转换工具。
API Style
| 维度 | 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 |
移动端/Web/第三方:JSON 可读可调试、浏览器零成本、CDN 与 HTTP 缓存可用;REST 语义(GET 幂等、缓存控制)在公网才有价值
服务间高频调用:强契约 + 低延迟 + 流式;K8s/Skaffold 生态(CRD 也是 Protobuf 系)全面原生
grpc-gateway(注解生成反向代理)、APISIX/Kong 的 gRPC 插件、Envoy gRPC-JSON transcoder——一份 proto 同时产出两套 API
追问"为什么内部不全用 REST"——高频内部调用里 JSON 的体积与解析开销、HTTP/1.1 连接低效、无统一流式语义;追问"为什么对外不全用 gRPC"——浏览器兼容、调试成本、公网中间盒(CDN/防火墙)对 HTTP/2 之外的扩展头支持差。两边都是"生态与可运营性"优先。
Governance Hooks
入口设 500ms,每跳 RPC 自动继承剩余时间(grpc-timeout 头),任何一跳超时立即返回 DeadlineExceeded。
ctx, cancel := context.WithTimeout(
ctx, 500*time.Millisecond)
defer cancel()
// 下游自动收到剩余 deadline
client.Get(ctx, req)
意义:防止上游已放弃、下游还在苦算——没有传播,超时只保护第一跳。
跨服务的键值通道(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。
unary/stream × client/server 四类,链式组合——治理能力的唯一挂载点。
grpc.ChainUnaryInterceptor( otelInterceptor, // trace authInterceptor, // 鉴权 ratelimitInterp, // 限流 retryInterceptor, // 重试 )
顺序有讲究:trace 最外(全链路可见),重试内层(每次重试都带 trace)。
gRPC 服务端重试策略(service config / retryPolicy)自动重试 Unavailable;但 Unavailable 的请求可能已在服务端执行成功(响应丢失)——非幂等接口重试 = 重复下单。组合拳:只有幂等方法开启重试 + 重试预算(防止重试风暴)。
OpenTelemetry 的 gRPC 拦截器自动注入 trace context(W3C traceparent 放在 metadata);Prometheus 拦截器统计每方法 QPS/延迟分布/错误率(RED 指标);日志拦截器打印 method/code/latency。三行配置获得全链路可观测——这是拦截器价值的最佳展示。
Landscape
| 框架 | 出品 | 协议/序列化 | 特点与定位 |
|---|---|---|---|
| gRPC | Google / CNCF | HTTP/2 · Protobuf | 跨语言事实标准;生态最全(xDS/OTel/网关);Go 客户端 v1.83(2026-08) |
| Thrift | Meta(Apache) | TCP/多传输 · Thrift IDL | 老牌跨语言;传输层可换(socket/framed);存量系统多,新项目少选 |
| Dubbo 3 | Apache(阿里) | 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。
2.x 按接口注册(实例数 × 接口数爆炸);3.x 改为应用级服务发现(对齐 K8s/K8s Service 模型,注册中心只存应用→实例映射,接口路由信息放到元数据中心)——回答"注册中心数据量怎么优化"时的现成案例。
Field Notes
现象:N 个 Pod,CPU 一个 90% 其余 10%。根因:HTTP/2 长连接 + 多路复用,LB 建连时一次分流,之后请求全挤一条连接。解法梯度:
→ 详见 负载均衡 deck 的 gRPC 章节
大文件/大列表直接 ResourceExhausted。正解:分页 + 流式(服务端流或客户端流分片),而不是调大上限——调大上限是把 OOM 风险转移给服务端。4MB 是故意设计的"反模式警报"。
长连接空闲时被 LB/NAT 静默断掉,客户端下一次请求报 Unavailable。配置双侧:客户端 PermitWithoutStream + Time 心跳,服务端 EnforcementPolicy 放行;中间 LB 的 idle timeout 要大于心跳间隔。
handler panic 默认变 Unknown / 连接直接断。必须有 recover 拦截器:转成 Internal + 保留堆栈日志;跨服务错误要用 status.New(code, msg).WithDetails() 保留语义,别用裸 error 字符串穿透。
不设 deadline = 永远等(goroutine 泄漏放大);设太死 = 正常长查询被 DeadlineExceeded。实践:入口按 API 定 SLO 预算,逐跳继承;数据库查询超时必须 < RPC deadline,否则下游先挂上游才超时,时序错乱。
gRPC 的坑几乎都源于"长连接 + 多路复用 + 强类型"这三个特性:连接相关(失衡/keepalive)、契约相关(4MB/错误模型)、预算相关(deadline)。治理默认值配好,坑就填了 80%。
Cheat Sheet
| 延迟 | 纳秒 → 毫秒(约 1000 倍):RTT + 序列化 + 排队 |
| 可靠性 | 报文可丢可重排 → 超时、重试必须显式处理 |
| 数据 | 只能传值(序列化),指针与闭包跨进程失效 |
| 错误 | 异常栈 → 跨语言状态码(gRPC 用 grpc-status 尾帧) |
| 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),剩余时间逐跳传播;不传播 = 上游放弃、下游苦算 |
| Metadata | trace / 鉴权 / 灰度标签的通道;key 小写、别传大字段 |
| Interceptor | 四类(unary/stream × client/server)链式;trace 最外、重试最内 |
status.WithDetails();deadline 倒挂:DB 超时必须 < RPC deadline
Interview QA · 1/2
先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的直接翻回上一页速查表。
① 延迟:网络 RTT + 序列化,量级从纳秒到毫秒;② 可靠性:报文可丢可重排,必须显式超时重试;③ 数据:只能传值(序列化),指针/闭包失效;④ 错误:从异常变成跨语言状态码。RPC 框架的全部设计都在消化这四个差异——"透明化"是目标,但假装没网络是灾难。
解决:HTTP/1.1 的请求级队头阻塞——一条连接并发数百 stream,高并发小请求不再排队建连。没解决:TCP 层队头阻塞(丢包重传阻塞整条连接),内网影响小、公网高丢包明显,根治要 HTTP/3/QUIC。另一面:复用让"连接"不再对应"请求",负载均衡必须转向请求级或客户端级。
快在两点:体积(varint + 无字段名,省 60-80%)与解析(按编号直取,无需字符串词法分析)。选型按边界:对外 API 要可读可调试 → JSON;内部 RPC 要契约与性能 → Protobuf;MQ 消息体建议 Protobuf + 版本演进规则。详见序列化对比页。
gRPC 把 context 的 deadline 换算成 grpc-timeout 头(如 300m)随请求传播,下游把"剩余时间"装回自己的 context,继续向更下游传播。重要性:没有传播,A 层超时放弃后 B/C/D 还在消耗资源——超时只是"客户端不再等",传播才让全链路"及时止损"。
HTTP/2 每条连接与每条 stream 各有接收窗口(默认 64KB),接收方通过 WINDOW_UPDATE 扩窗——发送方超窗必须暂停。对 gRPC 的意义:端到端背压,慢消费者拖慢快生产者而不是 OOM;流式接口大吞吐要调 BDP 优化窗口(gRPC-Go 的 initialWindowSize 选项)。
语义同源(洋葱模型,前后都能插逻辑),差异在:拦截器是方法级(能拿到 method 名、req/resp 强类型、status code,便于按方法治理);中间件是路径级(HTTP 层)。gRPC 里两者并存:网关 HTTP 层做通用治理,RPC 层做业务治理。
Interview QA · 2/2
同样建议先自答。这一页的题都要"结论 + 一句代价/边界"才完整——只答结论容易被追问。
默认内部 gRPC:强契约(IDL+breaking 检查)、低延迟(二进制+复用)、流式原生。特例:调试工具链要求 HTTP+JSON 的团队、纯 CRUD 且变更频繁的领域(Protobuf 契约管理成本高于收益)。对外统一 REST,网关转换衔接。选型的决定性因素是团队工程能力而非纯性能。
三选一按场景:① 分页(cursor 分页,用户交互场景);② 服务端流分批推送(程序消费、边生成边传);③ 异步导出:生成文件 → 对象存储 → 通知下载(报表场景)。反模式:调大 MaxRecvMsgSize 一次性拉全量——内存放大 + 超时 + 重试成本全炸。
Proto 文件按 package 版本化(order.v1 / order.v2),新旧并行、按消费者迁移后下线旧版;变更纪律:字段只加不删不编号复用、reserved 标记退役编号、CI 跑 buf breaking;兼容层:v2 handler 内组合调用 v1 逻辑,避免代码翻倍。
原则:只有幂等方法开启自动重试(GET/状态机类),非幂等(下单/支付)要么不重试、要么带幂等键(idempotency key 放 metadata,服务端去重)。gRPC 服务端重试要配 retryThrottling(预算),防止一个实例抖动引发全链路重试风暴。详见幂等 deck。
两条路:① 透传型(APISIX/Envoy 原生支持 HTTP/2 gRPC 代理,按 :path 路由到后端,性能最好);② 转换型(grpc-gateway / Envoy transcoder 把 JSON REST 翻译成 gRPC,代价是编解码开销)。公共治理(鉴权/限流)在网关层做,业务治理在拦截器层做,两层别重复。
不合适。RPC 通道适合小消息高频调用;大文件走"控制面+数据面分离":RPC 传元数据与凭证 → 客户端直传对象存储(预签名 URL/分片)→ 传完回调或事件通知。收益:RPC 连接不被大流量占死(流控窗口不被单流吃光)、存储服务天然扩展。
Related & References