题干与适用场景
一个平台有三个 API 边界。第一类由浏览器、移动应用和外部合作伙伴调用,需要容易接入、调试和长期演进。第二类是可控数据中心内的服务间调用,峰值为每秒 20,000 次,既有普通请求—响应,也有服务端持续推送结果的需求。第三类接收第三方系统发来的 webhook 回调。
每秒 20,000 次是面试假设,不是通用性能分界线。候选人需要为每个边界选择 REST 风格的 HTTP+JSON API、原生 gRPC,或有明确理由的组合,并解释迁移和验证。REST 是一种架构风格,不绑定 JSON 或 HTTP/1.1;这里写成“REST/HTTP+JSON”,只是固定本题中被比较的常见实现。gRPC 也不是一种自动获得低延迟、幂等和可靠性的开关。
这道题归入 backend,因为核心工作是服务接口、协议语义、客户端契约和运行治理。答案不需要设计整个业务系统,也不能只背“gRPC 更快、REST 更通用”的表格。
面试官考察点
第一项信号是先识别消费者和网络边界。公共浏览器与合作伙伴生态重视通用 HTTP 工具、可读载荷、缓存语义和低接入成本;可控的内部调用方更容易统一 .proto、生成代码、代理和负载均衡。协议选择跟着边界走,不必让整个平台只保留一种接口。
第二项信号是区分抽象与实现。REST 借助资源、HTTP 方法、状态码和缓存表达语义,可以使用 HTTP/2 或 HTTP/3;gRPC 以服务和方法为中心,默认用 Protocol Buffers 描述接口与消息,并提供 unary、客户端流、服务端流和双向流。把“REST 只能 HTTP/1.1”或“REST 不能流式返回”当作结论,说明概念没有分清。
第三项信号是把契约和失败语义写完整。OpenAPI 同样能为 HTTP API 提供机器可读契约和代码生成;Protobuf 的二进制兼容也不等于业务兼容。无论选择哪一边,都要定义截止时间、取消、幂等、可重试错误、认证授权、版本演进和可观测字段。
最后看证据。强回答会用代表性载荷、并发、压缩、长连接和故障模式做基准测试,再用 canary 验证尾延迟与错误率。只凭“二进制一定更快”做平台级迁移,缺少可复现依据。
回答前需要澄清的问题
- 谁控制客户端和升级节奏? 同一组织内可同步升级的服务适合生成客户端;无法强制更新的合作伙伴需要更稳定、更容易独立消费的边界。
- 通信是 unary、流式还是异步通知? 普通 CRUD 不会因为使用 gRPC 自动获益;长连接上的有序流可能让原生 gRPC 更自然。第三方 webhook 由对方主动发起,通常必须遵循其公开 HTTP 契约。
- 浏览器是否必须直接调用? 浏览器不能直接使用原生 gRPC 所需的全部 HTTP/2 控制能力,需要 gRPC-Web、JSON 转码或 BFF。这个额外层会改变调试、流式能力和运维成本。
- 性能问题具体是什么? 若瓶颈在数据库、下游扇出或超大查询,替换序列化协议不会消除它。需要给出载荷大小、QPS、并发、p95/p99、CPU 和网络预算。
- 现有网关与可观测工具支持什么? 代理是否理解 gRPC 状态、流和健康检查,日志与追踪是否能关联一次 RPC,会直接影响上线风险。
- 接口如何演进? 公共 API 需要兼容政策;内部 Protobuf 需要字段编号和混合版本规则。若没有多版本测试,强类型契约仍会在滚动发布中失败。
30 秒回答框架
“我会按消费者边界选择,而不是全局二选一。公共浏览器、移动端和合作伙伴 API 先用 REST 风格的 HTTP+JSON,并用 OpenAPI、HTTP 语义和兼容政策约束它;第三方 webhook 也沿用公开 HTTP 契约。内部每秒 20,000 次的可控服务调用,如果基准测试证明序列化或连接成本重要,而且确实需要服务端流,我会选择原生 gRPC。它用 .proto 生成客户端,但仍要显式设置截止时间、传播取消、只重试安全操作并维护字段兼容。边缘可以用 JSON 转码或薄网关复用一份领域实现。上线前用真实载荷比较端到端 p99、CPU、带宽和错误恢复,再灰度迁移;协议本身不替代认证、幂等和可观测性。”
分步骤深入解答
第一步:给三个边界分别做决定
公共边界选择 REST 风格的 HTTP+JSON。资源 URI、方法、状态码、条件请求和缓存能被浏览器、CDN、命令行工具与合作伙伴普遍理解。OpenAPI 作为契约真源,可生成文档和 SDK,因此不能把 REST 描述成“没有类型、全靠手写”。代价是团队必须主动治理错误模型、分页、枚举开放性和文档漂移。
内部边界先满足两个门槛再选 gRPC:调用方和服务端能统一生成代码与运行组件;代表性基准证明收益足以支付代理、调试和混合版本成本。本题还明确需要服务端流,gRPC 的流式方法、每次调用的 metadata、status 和 deadline 能给出一致模型。因此内部接口采用 gRPC 合理,但 20,000 QPS 本身不是决定依据。
第三方回调采用 HTTP webhook。协议由外部发送方控制,入口需要公开 TLS、签名验证、快速确认和异步处理。把接收端改成 gRPC,无法让一个只会发送 HTTP POST 的合作伙伴调用。若内部处理链使用 gRPC,webhook 接入层验证、持久化后再调用内部服务。
| 边界 | 初始选择 | 主因 | 主要代价 |
|---|---|---|---|
| 浏览器、移动端、合作伙伴 | REST/HTTP+JSON | 广泛兼容、HTTP 语义、低接入成本 | 需治理契约与客户端差异 |
| 可控内部服务 | gRPC | 生成契约、流式方法、紧凑消息 | 工具链、代理和滚动升级复杂度 |
| 第三方回调入口 | HTTP webhook | 发送方契约和公网互操作性 | 必须自行做签名、去重和异步隔离 |
第二步:分别写出可执行契约
公共读接口使用资源和 HTTP 语义:
GET /v1/orders/ord_123
If-None-Match: "order-v7"
200 OK
ETag: "order-v7"
Content-Type: application/json内部接口以动作和消息定义:
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrder(WatchOrderRequest) returns (stream OrderEvent);
}
message GetOrderRequest {
string order_id = 1;
}这两份契约都要覆盖认证、授权、错误、分页或流结束条件、大小上限和审计字段。gRPC 的方法名可以隐藏网络成本,调用者仍要把它当成会延迟、超时、部分失败的远程调用。REST 的 POST 也不会自动幂等;创建类操作要使用稳定幂等键或业务唯一约束。
第三步:设计失败语义
gRPC 客户端默认可能没有截止时间,应按端到端预算显式设置。deadline 到期会让调用方停止等待,但服务端应用仍负责停止已经派生的工作;跨服务传播也要按所用语言和框架验证。HTTP 客户端同样需要连接、响应和总预算,不能把 socket 默认值当业务 SLO。
重试由业务语义决定。HTTP 的 GET、HEAD、PUT、DELETE 在规范中具有安全或幂等属性,但实现仍必须符合方法语义;POST 只有在幂等键等机制保证结果时才可安全重试。gRPC 方法名没有自动幂等性,必须在契约中标明哪些状态与操作可重试。两者都要限制尝试次数、遵守剩余截止时间并避免网关、SDK 和业务层叠加重试。
流式 RPC 还要处理慢消费者、背压、单条消息上限、断线后的恢复位置和重复事件。若消费者必须从游标恢复,事件需要稳定 ID 或序号;仅仅重新建立 stream 不能证明不漏不重。
第四步:保证契约能滚动演进
REST/JSON 的兼容要同时检查结构和语义。新增响应字段只在客户端允许未知字段时安全;改变默认分页、排序或枚举含义可能保持 JSON 可解析却破坏业务。用 OpenAPI diff、最后一个公开 SDK、录制请求回放和端到端断言做门禁。
Protobuf 添加字段通常保持二进制线兼容,旧读取方会忽略未知字段,但应用代码仍可能因新枚举或默认值出错。不得改变既有字段编号;删除字段后保留编号和名称,防止未来复用。在滚动发布中同时测试旧客户端—新服务端和新客户端—旧服务端,不能只让同一版本互测。
如果边缘和内部都需要同一能力,优先共用领域逻辑和一份明确的契约真源,再通过 JSON 转码或薄适配层暴露 HTTP。适配层要显式映射 HTTP 状态、gRPC status、headers/metadata、流式限制和字段名称。维护两份人工同步的业务实现会产生行为漂移。
第五步:用数据决定迁移
基准测试使用真实分布的载荷和方法比例,而非只序列化一个小对象。对相同业务逻辑测端到端吞吐、p50/p95/p99、客户端与服务端 CPU、传输字节、连接数和内存;分别覆盖 unary、小消息、大消息、压缩、服务端流、慢消费者和跨可用区网络。数据库和下游调用保持一致,才能把协议影响分离出来。
先让一个低风险内部方法双栈运行,按调用方灰度。比较业务结果、错误分类、deadline exceeded、取消后仍运行的工作、重试放大和追踪完整率。达到预先写下的收益与可靠性门槛才扩大;没有显著收益时保留 HTTP API也是有效结论。
高质量示范回答
“我不会给整个平台统一贴 REST 或 gRPC 标签。先看谁调用、谁控制升级,以及通信模式。
公共浏览器、移动端和合作伙伴接口我会用 REST 风格的 HTTP+JSON。它能直接使用浏览器和通用 HTTP 生态,也能用 OpenAPI 做机器可读契约、SDK 和兼容检查。第三方 webhook 继续收 HTTP POST,因为发送方的协议是外部约束;入口验签、用事件 ID 去重、先持久化再异步处理。
内部接口中,本题有每秒 20,000 次调用和服务端流。我会用真实载荷做基准。如果调用方都能使用生成客户端,代理、负载均衡和监控也支持 gRPC,而且 p99、CPU或带宽收益达到门槛,我会给这个边界采用 gRPC。unary 与 stream 都在 .proto 中定义,但每次调用仍显式设置 deadline,取消后服务端停止派生工作,重试只用于契约声明为安全的操作。
演进方面,HTTP API 用 OpenAPI diff、旧 SDK 和语义回放防止分页或枚举破坏;Protobuf 不改字段编号,删除字段后 reserve,并做新旧客户端交叉测试。若公共和内部要复用能力,我会让同一领域实现分别由 HTTP 适配器和 gRPC 服务调用,或在确认映射完整时使用 JSON 转码。
最后按调用方灰度,观察端到端 p99、CPU、字节数、状态映射、重试放大和追踪完整率。若收益只存在于微基准,生产瓶颈仍在数据库,我不会为协议一致性扩大迁移。”
常见错误
- 把 REST 等同于 HTTP/1.1 → HTTP 语义与传输版本被混为一谈 → 明确 REST API 也可运行在 HTTP/2 或 HTTP/3。
- 宣布 gRPC 永远更快 → 真实瓶颈可能是存储、业务逻辑或代理 → 用同一业务逻辑和真实载荷测端到端尾延迟、CPU 与带宽。
- 认为 REST 没有强契约 → 忽略 OpenAPI 的描述、代码生成和测试生态 → 比较实际契约流程,不比较团队疏于维护后的结果。
- 让浏览器直接调用原生 gRPC → 浏览器缺少原生 gRPC 所需控制能力 → 选择 gRPC-Web、JSON 转码或 BFF,并计入功能限制。
- 未设置 gRPC deadline → 客户端可能无限等待并占用资源 → 从端到端预算推导 deadline,并验证取消传播。
- 把 Protobuf 线兼容当业务兼容 → 新枚举、默认值和字段含义仍会破坏旧代码 → 做新旧版本交叉测试并保留字段编号。
- 所有层都自动重试 → 故障时请求成倍放大,写操作还可能重复 → 集中重试,限定安全操作、剩余预算和尝试次数。
- 为了统一只保留一种协议 → 公共接入成本或内部流式需求被牺牲 → 允许边缘 HTTP、内部 gRPC 的明确边界。
追问及应对
追问一:内部接口只有 500 QPS,还应该使用 gRPC 吗?
QPS 不能单独决定。若已有成熟 gRPC 平台、跨语言生成契约和流式需求,500 QPS 仍可能适合;若团队只有一个简单 CRUD 服务、HTTP 工具成熟且性能充足,REST/HTTP+JSON 的运维成本更低。先写出要改善的指标,无法证明收益就不迁移。
追问二:公共移动端可以使用生成的 gRPC 客户端,能否直接对外提供 gRPC?
可以作为受控客户端的候选,但仍要检查代理、企业网络、调试工具、版本兼容、证书与发布节奏。合作伙伴和浏览器可能仍需要 HTTP API,因此公共 gRPC 不会自动消除双协议成本。用调用方分组而非“公网”标签做决定。
追问三:怎样从一份 .proto 同时提供 JSON API?
可以为方法添加 HTTP 映射并使用 JSON 转码或网关。上线前逐项验证字段命名、空值与默认值、HTTP 状态和 gRPC status、metadata 与 headers、认证、缓存以及 streaming 限制。把 .proto 作为契约真源能减少重复,但适配语义仍需测试。
追问四:gRPC stream 断开后如何做到不丢事件?
协议只提供流和单次 RPC 内的消息顺序,不自动提供业务级持久订阅。事件应有稳定序号,服务端保留可重放日志,客户端提交或携带已消费游标,重连时从游标继续;再定义保留期、过期处理和去重。若这些要求更像消息系统,应比较队列或日志而非强行延长 RPC。
追问五:基准测试显示 gRPC p99 更低,但线上没有改善,怎么排查?
先分解客户端排队、DNS/连接、代理、序列化、服务业务逻辑、数据库和下游耗时。检查线上载荷分布、压缩、连接复用、TLS、跨区路径和隐藏重试是否与基准一致。若协议只占总延迟的一小部分,应优化主导瓶颈,而不是继续扩大迁移。