惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

雷峰网
雷峰网
D
Darknet – Hacking Tools, Hacker News & Cyber Security
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
C
Cisco Blogs
L
Lohrmann on Cybersecurity
宝玉的分享
宝玉的分享
I
Intezer
aimingoo的专栏
aimingoo的专栏
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
GbyAI
GbyAI
C
CERT Recently Published Vulnerability Notes
Apple Machine Learning Research
Apple Machine Learning Research
U
Unit 42
Cyberwarzone
Cyberwarzone
爱范儿
爱范儿
I
InfoQ
博客园_首页
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
P
Palo Alto Networks Blog
Microsoft Azure Blog
Microsoft Azure Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
腾讯CDC
阮一峰的网络日志
阮一峰的网络日志
NISL@THU
NISL@THU
T
Threatpost
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Schneier on Security
Security Latest
Security Latest
Martin Fowler
Martin Fowler
H
Help Net Security
小众软件
小众软件
The Hacker News
The Hacker News
Know Your Adversary
Know Your Adversary
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
Jina AI
Jina AI
Engineering at Meta
Engineering at Meta
The GitHub Blog
The GitHub Blog
P
Proofpoint News Feed
IT之家
IT之家
WordPress大学
WordPress大学
S
Securelist

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 【Linux 网络子系统深度拆解】内核网络调优方法论:从基准测试到生产验证 排序基准测试:用数据说话
【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡
Liao Tonglang · 2026-04-13 · via 土法炼钢兴趣小组的算法知识备份

你维护一个电商平台的订单服务。前端需要一个”订单详情”接口,返回订单基本信息、商品列表、物流状态、用户收货地址。后端团队设计了一个 REST 端点 GET /orders/{id},返回所有字段。上线三个月后问题来了:移动端只需要订单状态和物流信息,每次请求却要拉回 15KB 的完整 JSON;后台管理系统需要批量查询 50 个订单的汇总数据,只能循环调用 50 次接口;另一个内部服务需要实时订阅订单状态变更,只能靠轮询。

三个消费方,三种需求模式,一个 REST 端点全部无法优雅满足。

这不是 REST 的错。这是 API 设计中最常见的误判:把”选用哪种协议”等同于”做了 API 设计”。REST、GraphQL(图查询语言)、gRPC(Google 远程过程调用)是三种不同的 API 范式,各自有明确的适用边界。选错了范式,或者在正确的范式上犯了设计错误,代价都很高——接口膨胀、性能恶化、版本地狱、客户端与服务端深度耦合。

本文的目标不是做一个”哪个更好”的排名。它要回答三个具体问题:每种范式的设计哲学和核心约束是什么?在什么场景下它会成为最优选择?当你的系统需要多种通信模式时,如何组合使用它们?


在讨论具体范式之前,先明确 API 设计中反复出现的几类工程问题。

过度获取与欠获取(Over-fetching / Under-fetching):客户端只需要 3 个字段,接口返回了 30 个字段——这是过度获取。一个页面需要的数据分散在 5 个接口里,客户端必须串行或并行调用多次——这是欠获取。在 REST 架构中尤为突出,典型的应对是为不同客户端定制不同端点(/orders/{id}/summary/orders/{id}/detail),但这导致端点爆炸。

版本管理困境:API 发布后,消费方已经依赖了返回的字段结构。你想删除一个废弃字段或修改一个字段类型,要么发布新版本(v1、v2、v3 并行维护),要么冒着破坏客户端的风险强制变更。Netflix 在 2012 年公开讨论过这个问题:他们的 REST API 同时维护着多个版本,每个版本的废弃周期长达两年。

实时通信需求:HTTP 请求-响应模型是同步的、单向的。当业务需要服务端主动推送数据(实时报价、聊天消息、日志流),REST 只能退而求其次用长轮询(Long Polling)或 WebSocket。

序列化开销:在微服务架构中,服务间调用是高频操作。JSON 的序列化/反序列化 CPU 开销和网络传输体积在请求链路经过 8 个服务时会显著累加。

这四类问题,构成了”为什么不能只用 REST”的基本动机。


二、REST 深度解析

REST(Representational State Transfer,表述性状态转移)由 Roy Fielding 在 2000 年的博士论文中提出。REST 是一种架构风格(Architectural Style),不是协议,不是标准,不是规范。

2.1 Richardson 成熟度模型(Richardson Maturity Model)

Leonard Richardson 在 2008 年提出了一个四级模型,衡量 API 的 REST 成熟程度。这个模型揭示了一个现实:大多数自称”RESTful”的 API 只达到了 Level 1 或 Level 2。

graph TB
    L0["Level 0:HTTP 作为传输隧道<br/>POST /api,所有操作一个端点"]
    L1["Level 1:引入资源<br/>POST /orders,POST /users"]
    L2["Level 2:正确使用 HTTP 动词<br/>GET /orders/123,DELETE /orders/123"]
    L3["Level 3:HATEOAS<br/>响应中包含状态转移的超链接"]

    L0 --> L1
    L1 --> L2
    L2 --> L3

    style L0 fill:#f9d0c4,stroke:#e36209
    style L1 fill:#fef3c7,stroke:#f59e0b
    style L2 fill:#d1fae5,stroke:#10b981
    style L3 fill:#bfdbfe,stroke:#3b82f6

Level 0:The Swamp of POX。所有操作通过一个端点、一种 HTTP 方法完成,请求体包含操作类型和参数。本质是把 HTTP 当传输隧道。

// Level 0:所有操作走同一个端点
POST /api HTTP/1.1

{ "action": "getOrder", "orderId": "12345" }

Level 1:Resources。引入了资源概念,不同实体有不同端点,但 HTTP 方法使用不规范。

// Level 1:有资源概念,动词使用不规范
POST /orders/12345/query HTTP/1.1
POST /orders/12345/cancel HTTP/1.1

Level 2:HTTP Verbs。正确使用 HTTP 方法表达操作语义,正确使用状态码。

// Level 2:正确使用 HTTP 动词和状态码
GET    /orders/12345      // 200 OK
POST   /orders            // 201 Created
PUT    /orders/12345      // 200 OK
DELETE /orders/12345      // 204 No Content

大多数生产环境的 REST API 停在这个级别,Level 2 已足够解决 CRUD 场景。

Level 3:HATEOAS。HATEOAS(Hypermedia As The Engine Of Application State,超媒体作为应用状态引擎):响应体包含指向下一步操作的链接。

{
  "orderId": "12345",
  "status": "CONFIRMED",
  "total": 299.00,
  "_links": {
    "self": { "href": "/orders/12345" },
    "cancel": { "href": "/orders/12345/cancel", "method": "POST" },
    "payment": { "href": "/payments?orderId=12345", "method": "POST" }
  }
}

现实中 HATEOAS 采用率极低,原因:前端框架的路由驱动开发模式与运行时动态 URL 发现不兼容;链接描述标准碎片化(HAL、JSON-LD、Siren);每个响应附带大量元数据增加有效载荷(Payload)体积。

为什么停在 Level 2? Level 2 提供了足够的语义清晰度。开发者看到 GET /orders/123 就知道这是读取订单,HTTP 动词和状态码构成了简洁的接口合约。Level 3 的理论收益被前端开发模式的现实所抵消。

2.2 版本管理策略

维度 URI 路径 /api/v1/orders HTTP Header Accept: vnd.app.v2+json 查询参数 ?version=2
直观性
REST 纯粹性
缓存友好性
调试便利性
采用率 最高 最低

实践建议:对外 API 优先选择 URI 路径版本化,对第三方开发者最友好。GitHub、Twitter、Stripe 都采用这种方式。

2.3 资源命名与错误处理

# 好的命名:名词复数、层次清晰
GET    /users/{userId}/orders
GET    /users/{userId}/orders/{orderId}/items

# 坏的命名:动词化、层次混乱
GET    /getOrdersByUser?userId=123
POST   /createNewOrder

统一错误响应格式采用 RFC 7807(Problem Details for HTTP APIs):

{
  "type": "https://api.example.com/errors/insufficient-balance",
  "title": "余额不足",
  "status": 400,
  "detail": "账户余额 50.00 元,订单金额 299.00 元",
  "instance": "/orders/12345",
  "traceId": "abc-123-def-456"
}

2.4 分页模式

偏移量分页?offset=20&limit=10):简单但大 offset 性能差,并发写入时分页不稳定。

游标分页?cursor=eyJpZCI6MTIzNDV9&limit=10):性能稳定,不受并发写入影响,缺点是不能跳页。适合无限滚动。

键集分页?after_id=12345&limit=10):游标分页的显式版本,实现简单。

生产环境中,面向前端的列表接口建议游标分页,后台管理界面可用偏移量分页。


三、GraphQL 深度解析

GraphQL 由 Facebook 在 2012 年内部开发,2015 年开源。诞生动机:Facebook 移动应用需要从不同后端服务聚合数据,REST 接口导致严重的过度获取和欠获取问题。

3.1 Schema-First 设计

GraphQL 的核心是”客户端精确描述自己需要的数据”,通过强类型 Schema 和查询语言实现。

type Order {
  id: ID!
  status: OrderStatus!
  total: Float!
  createdAt: DateTime!
  items: [OrderItem!]!
  customer: User!
  shipping: ShippingInfo
}

type Query {
  order(id: ID!): Order
  orders(first: Int, after: String, status: OrderStatus): OrderConnection!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  cancelOrder(id: ID!): Order!
}

同一个 Schema,不同客户端按需取用:

# 移动端:只要状态和物流
query MobileOrderDetail($id: ID!) {
  order(id: $id) {
    id
    status
    shipping { carrier trackingNumber estimatedDelivery }
  }
}

# 后台管理:需要完整信息
query AdminOrderDetail($id: ID!) {
  order(id: $id) {
    id
    status
    total
    createdAt
    customer { name email address { city street } }
    items { product { name sku } quantity price }
  }
}

3.2 N+1 问题的工程解法

N+1 问题是 GraphQL 服务端最常见的性能陷阱。查询 10 个订单及其用户信息时,朴素实现会产生 11 次数据库查询(1 次查订单 + 10 次查用户)。

DataLoader 模式的核心是批处理:收集一个执行周期内的所有数据加载请求,合并为一次批量查询。

// 朴素实现:每个订单触发一次查询
func (r *orderResolver) Customer(ctx context.Context, obj *model.Order) (*model.User, error) {
    return r.db.GetUserByID(ctx, obj.CustomerID) // N 次调用
}

// DataLoader 实现:自动批处理
func (r *orderResolver) Customer(ctx context.Context, obj *model.Order) (*model.User, error) {
    return r.userLoader.Load(ctx, obj.CustomerID) // 合并为 1 次批量查询
}

DataLoader 的工作流程:

sequenceDiagram
    participant Client as 客户端
    participant GQL as GraphQL 引擎
    participant DL as DataLoader
    participant DB as 数据库

    Client->>GQL: query { orders { customer { name } } }
    GQL->>DB: SELECT * FROM orders LIMIT 10
    DB-->>GQL: 10 条订单

    loop 对每个订单解析 customer
        GQL->>DL: Load(customerID)
        Note over DL: 收集请求,不立即查询
    end

    Note over DL: 等待窗口结束(1-2ms)
    DL->>DB: SELECT * FROM users WHERE id IN (id1...id10)
    DB-->>DL: 10 条用户数据
    DL-->>GQL: 返回对应用户
    GQL-->>Client: 完整响应

DataLoader 的 Go 实现核心逻辑:在一个很短的时间窗口(通常 1-2 毫秒)内收集所有 Load 调用的 key,窗口结束后调用一次批量查询函数(fetch(keys []string)),将结果分发给各个等待者。原来的 N+1 次查询变成 2 次。

3.3 查询复杂度分析与深度限制

GraphQL 赋予客户端极大的查询自由度,也带来安全风险。恶意客户端可以构造深度嵌套的查询消耗服务端资源。

三层防护:

第一层:查询深度限制——限制查询嵌套不超过 7 层。

第二层:查询复杂度分析——为每个字段分配复杂度分值,列表字段的复杂度乘以请求数量,总复杂度不超过阈值。

func (e *ComplexityEstimator) Calculate(field string, childComplexity int, args map[string]interface{}) int {
    switch field {
    case "orders", "reviews":
        first, _ := args["first"].(int)
        if first == 0 { first = 10 }
        return first * (1 + childComplexity)
    default:
        return 1 + childComplexity
    }
}

第三层:持久化查询(Persisted Queries)——预注册查询并分配 ID,客户端只能通过 ID 执行已注册查询。完全消除恶意查询的可能性,但牺牲了自由查询的灵活性。

3.4 版本演进

GraphQL 没有显式版本号,Schema 演进通过字段废弃实现:

type User {
  id: ID!
  name: String!
  userName: String @deprecated(reason: "使用 name 字段替代,将在 2026-06 移除")
}

废弃字段仍可正常使用,开发工具会显示警告。服务端通过日志分析确认使用量后安全删除。这比 REST 的版本化优雅得多——不需要维护多个版本的端点。


四、gRPC 深度解析

gRPC 由 Google 在 2015 年开源,是其内部 RPC 框架 Stubby 的开源版本。Google 内部每天有超过 100 亿次 RPC 调用。

4.1 Protocol Buffers 与接口定义

gRPC 使用 Protocol Buffers(Protobuf,协议缓冲区)作为 IDL 和序列化格式。

syntax = "proto3";
package order.v1;

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (OrderResponse);
  rpc CreateOrder(CreateOrderRequest) returns (OrderResponse);
  rpc WatchOrderStatus(WatchOrderRequest) returns (stream OrderStatusEvent);
  rpc BatchImportOrders(stream ImportOrderRequest) returns (BatchImportResponse);
  rpc ProcessOrders(stream OrderCommand) returns (stream OrderResult);
}

message OrderResponse {
  string order_id = 1;
  OrderStatus status = 2;
  double total = 3;
  repeated OrderItem items = 4;
  CustomerInfo customer = 5;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
  ORDER_STATUS_CANCELLED = 5;
}

Protobuf 优势:二进制编码,体积约为 JSON 的 1/3 到 1/5;强类型,编译期检查错误;代码生成,Stub 代码自动生成。

4.2 四种通信模式

这是 gRPC 与 REST 最大的差异化优势。

一元 RPC(Unary RPC):等同于 HTTP 请求-响应。

func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.OrderResponse, error) {
    order, err := s.repo.FindByID(ctx, req.OrderId)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "订单 %s 不存在", req.OrderId)
    }
    return toOrderResponse(order), nil
}

服务端流式 RPC(Server Streaming):客户端发一个请求,服务端返回数据流。适合实时推送。

func (s *orderServer) WatchOrderStatus(
    req *pb.WatchOrderRequest,
    stream pb.OrderService_WatchOrderStatusServer,
) error {
    ch, err := s.eventBus.Subscribe(req.OrderId)
    if err != nil {
        return status.Errorf(codes.Internal, "订阅失败: %v", err)
    }
    defer s.eventBus.Unsubscribe(req.OrderId, ch)

    for {
        select {
        case event := <-ch:
            if err := stream.Send(&pb.OrderStatusEvent{
                OrderId:   req.OrderId,
                OldStatus: event.OldStatus,
                NewStatus: event.NewStatus,
            }); err != nil {
                return err
            }
        case <-stream.Context().Done():
            return nil
        }
    }
}

客户端流式 RPC(Client Streaming):客户端发送数据流,服务端在流结束后返回响应。适合批量上传。

func (s *orderServer) BatchImportOrders(
    stream pb.OrderService_BatchImportOrdersServer,
) error {
    var imported, failed int32
    for {
        req, err := stream.Recv()
        if err == io.EOF {
            return stream.SendAndClose(&pb.BatchImportResponse{
                ImportedCount: imported, FailedCount: failed,
            })
        }
        if err != nil { return err }

        if err := s.repo.Insert(stream.Context(), req.Order); err != nil {
            failed++
        } else {
            imported++
        }
    }
}

双向流式 RPC(Bidirectional Streaming):两端都可以随时发送数据。适合实时交互——聊天、协同编辑、游戏。

func (s *orderServer) ProcessOrders(
    stream pb.OrderService_ProcessOrdersServer,
) error {
    for {
        cmd, err := stream.Recv()
        if err == io.EOF { return nil }
        if err != nil { return err }

        result, procErr := s.processCommand(stream.Context(), cmd)
        if procErr != nil {
            stream.Send(&pb.OrderResult{CommandId: cmd.CommandId, Success: false, Error: procErr.Error()})
            continue
        }
        if err := stream.Send(result); err != nil { return err }
    }
}

4.3 拦截器(Interceptor)

gRPC 拦截器等同于 HTTP 中间件,用于横切关注点:认证、日志、限流、链路追踪。

func loggingUnaryInterceptor(
    ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
    start := time.Now()
    resp, err := handler(ctx, req)
    log.Printf("[gRPC] method=%s duration=%v err=%v", info.FullMethod, time.Since(start), err)
    return resp, err
}

server := grpc.NewServer(
    grpc.UnaryInterceptor(loggingUnaryInterceptor),
)

4.4 版本演进

Protobuf 天然支持前向和后向兼容(Forward/Backward Compatibility):新增字段旧客户端忽略;废弃字段用 reserved 标记编号;不能修改已有字段编号和类型。不兼容变更通过 package 版本隔离(order.v1 -> order.v2)。

message OrderResponse {
  string order_id = 1;
  OrderStatus status = 2;
  double total = 3;
  string currency = 7;  // 新增:旧客户端忽略

  reserved 4, 5;  // 废弃字段编号
  reserved "old_field_name";
}

五、三种范式的全面对比

5.1 核心特性对比

维度 REST GraphQL gRPC
协议基础 HTTP/1.1 或 HTTP/2 HTTP(通常 POST) HTTP/2
数据格式 JSON JSON Protobuf(二进制)
接口定义 OpenAPI(可选) Schema(强制) .proto 文件(强制)
类型系统 弱(依赖文档) 强类型 强类型
代码生成 可选 可选 内置
实时通信 需要额外方案 Subscription(WebSocket) 原生流式支持
浏览器支持 原生 原生 需要 gRPC-Web 代理
缓存 HTTP 缓存成熟 需要客户端缓存库 无标准缓存机制
学习曲线

5.2 性能对比

序列化性能(1000 条订单数据,Go 实现):

指标 JSON(REST/GraphQL) Protobuf(gRPC)
序列化耗时 12.3ms 2.1ms
反序列化耗时 15.7ms 3.4ms
序列化后体积 850KB 280KB
CPU 占用 基准 基准的 20%

GraphQL 传输格式也是 JSON,序列化性能与 REST 相当。其优势在于减少不必要的传输——客户端只请求 5 个字段时响应体积可能只有 REST 的 1/10。

延迟特性:

场景 REST GraphQL gRPC
单次简单查询 中(Schema 解析开销) 最低
多资源聚合 高(多次往返) 低(一次请求)
高并发 高(多路复用)
流式传输 不支持 Subscription 原生最优

5.3 适用场景

graph LR
    subgraph REST适用场景
        R1[公开 API]
        R2[CRUD 为主]
        R3[需要 HTTP 缓存]
    end

    subgraph GraphQL适用场景
        G1[多客户端不同需求]
        G2[数据聚合查询]
        G3[快速迭代的前端]
    end

    subgraph gRPC适用场景
        P1[微服务间通信]
        P2[高性能低延迟]
        P3[流式数据处理]
    end

    style REST适用场景 fill:#dbeafe,stroke:#3b82f6
    style GraphQL适用场景 fill:#fce7f3,stroke:#ec4899
    style gRPC适用场景 fill:#d1fae5,stroke:#10b981

六、工程案例:Airbnb 从 REST 到 GraphQL 的迁移

6.1 迁移背景

2017 年前,Airbnb 前端依赖大量 REST API。房源详情页需要调用 7 个以上端点——房源信息、价格、房东、评价、日期、设施、推荐。移动端下载了 3 倍于实际需要的数据。数百个 API 的 Swagger 文档经常与实际接口脱节。

6.2 迁移策略

Airbnb 采用渐进式迁移而非大爆炸:

graph TB
    subgraph 客户端
        Web[Web 应用]
        Mobile[移动应用]
    end

    subgraph GraphQL层
        GW[GraphQL 网关]
    end

    subgraph 后端服务
        UserAPI[用户服务 REST]
        ListingAPI[房源服务 REST]
        BookingAPI[预订服务 REST]
        ReviewAPI[评价服务 REST]
    end

    Web --> GW
    Mobile --> GW
    GW --> UserAPI
    GW --> ListingAPI
    GW --> BookingAPI
    GW --> ReviewAPI

    style GW fill:#fce7f3,stroke:#ec4899
    style UserAPI fill:#dbeafe,stroke:#3b82f6
    style ListingAPI fill:#dbeafe,stroke:#3b82f6
    style BookingAPI fill:#dbeafe,stroke:#3b82f6
    style ReviewAPI fill:#dbeafe,stroke:#3b82f6

第一阶段:在 REST 服务之上部署 GraphQL 网关,Resolver 调用底层 REST API,前端立即享受精确查询能力,后端无需修改。

第二阶段:后端逐步将数据接口从 REST 迁移到内部 RPC,减少序列化开销。

第三阶段:利用 GraphQL Schema 自动生成 TypeScript 类型定义,前端 API 调用获得完整类型检查。

6.3 迁移成果

经过约 18 个月:移动端数据传输量减少约 30%;页面加载时间降低 15%;新功能前端开发时间缩短 25%;接口相关线上 bug 减少 40%。

6.4 遇到的坑

N+1 问题:未引入 DataLoader 前,GraphQL 网关数据库查询次数反而更多,数据库负载增加 50%。

缓存失效:REST 可利用 HTTP 缓存和 CDN,GraphQL 的 POST 请求使 HTTP 缓存完全失效,需要实现应用级缓存。

查询复杂度控制:上线初期未做限制,一个误写的递归查询导致 OOM(Out Of Memory,内存溢出)。

监控盲区:REST 按端点监控,GraphQL 只有 /graphql 一个端点,需重建基于 operation name 的监控体系。

这些教训说明 GraphQL 不是银弹,它引入了新的运维和性能挑战,需要专门的基础设施支持。


七、混合架构:多种 API 范式共存

现实中的大型系统很少只使用一种范式。更常见的做法是根据通信场景选择最合适的范式。

7.1 典型混合架构

graph TB
    subgraph 外部客户端
        Browser[Web 浏览器]
        App[移动应用]
        ThirdParty[第三方开发者]
    end

    subgraph BFF层
        BFF_Web[Web BFF<br/>GraphQL]
        BFF_Mobile[Mobile BFF<br/>GraphQL]
        PublicAPI[Public API<br/>REST]
    end

    subgraph 微服务层
        OrderSvc[订单服务]
        UserSvc[用户服务]
        PaymentSvc[支付服务]
    end

    Browser --> BFF_Web
    App --> BFF_Mobile
    ThirdParty --> PublicAPI

    BFF_Web -.->|gRPC| OrderSvc
    BFF_Web -.->|gRPC| UserSvc
    BFF_Mobile -.->|gRPC| OrderSvc
    BFF_Mobile -.->|gRPC| PaymentSvc
    PublicAPI -.->|gRPC| OrderSvc
    OrderSvc -.->|gRPC| PaymentSvc

    style BFF_Web fill:#fce7f3,stroke:#ec4899
    style BFF_Mobile fill:#fce7f3,stroke:#ec4899
    style PublicAPI fill:#dbeafe,stroke:#3b82f6

分层逻辑:

  • 对外 API(第三方):REST——普及度最高,文档工具最成熟,接入门槛最低
  • 面向自有前端的 BFF(Backend For Frontend,前端专用后端):GraphQL——精确提供前端需要的数据结构
  • 微服务间通信:gRPC——Protobuf 的高效序列化和代码生成

7.2 BFF 模式实现

// Web BFF 的 GraphQL Resolver:聚合多个 gRPC 服务
func (r *queryResolver) Order(ctx context.Context, id string) (*model.Order, error) {
    orderResp, err := r.orderClient.GetOrder(ctx, &pb.GetOrderRequest{OrderId: id})
    if err != nil {
        return nil, err
    }

    userResp, err := r.userClient.GetUser(ctx, &pb.GetUserRequest{
        UserId: orderResp.Customer.UserId,
    })
    if err != nil {
        return nil, err
    }

    return &model.Order{
        ID:     orderResp.OrderId,
        Status: orderResp.Status.String(),
        Total:  orderResp.Total,
        Customer: &model.User{
            Name:  userResp.Name,
            Email: userResp.Email,
        },
    }, nil
}

八、选型决策框架

8.1 决策流程

flowchart TD
    Start[需要设计 API] --> Q1{消费方是谁?}

    Q1 -->|第三方开发者| REST[REST]
    Q1 -->|自有前端| Q2{多种客户端?}
    Q1 -->|内部微服务| Q3{需要流式?}

    Q2 -->|多种,需求差异大| GraphQL[GraphQL + BFF]
    Q2 -->|单一客户端| Q4{数据模型复杂?}

    Q4 -->|简单 CRUD| REST2[REST]
    Q4 -->|复杂聚合| GraphQL2[GraphQL]

    Q3 -->|是| gRPC1[gRPC]
    Q3 -->|否| Q5{性能是核心?}

    Q5 -->|是| gRPC2[gRPC]
    Q5 -->|否| REST3[REST]

    style REST fill:#dbeafe,stroke:#3b82f6
    style REST2 fill:#dbeafe,stroke:#3b82f6
    style REST3 fill:#dbeafe,stroke:#3b82f6
    style GraphQL fill:#fce7f3,stroke:#ec4899
    style GraphQL2 fill:#fce7f3,stroke:#ec4899
    style gRPC1 fill:#d1fae5,stroke:#10b981
    style gRPC2 fill:#d1fae5,stroke:#10b981

8.2 按维度评估

决策维度 选 REST 选 GraphQL 选 gRPC
消费方是第三方
需要 HTTP 缓存
多客户端差异化需求
前端快速迭代
微服务内部通信
需要流式传输
追求最低延迟
浏览器直接调用 需要代理

8.3 反模式警告

反模式一:用 GraphQL 做服务间通信。Schema 解析和 JSON 序列化是不必要的开销。

反模式二:用 gRPC 做公开 API。Stub 代码生成对第三方开发者门槛太高,浏览器无法直接调用。

反模式三:在简单 CRUD 上强推 GraphQL。只有一种 Web 客户端、数据模型简单的系统,REST 是最经济的选择。引入 GraphQL 的额外成本(Schema 管理、复杂度控制、缓存策略)收益有限。

反模式四:不做版本管理。无论哪种范式,版本演进策略都需要在第一天确定。

8.4 幂等性(Idempotency)设计

网络请求可能因超时重试而被重复发送。幂等性设计跨范式通用:

func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.OrderResponse, error) {
    md, _ := metadata.FromIncomingContext(ctx)
    keys := md.Get("x-idempotency-key")
    if len(keys) == 0 {
        return nil, status.Errorf(codes.InvalidArgument, "缺少幂等键")
    }

    // 检查是否已处理过
    if existing, err := s.idempotencyStore.Get(ctx, keys[0]); err == nil {
        return existing, nil
    }

    order, err := s.repo.Create(ctx, req)
    if err != nil { return nil, err }

    resp := toOrderResponse(order)
    s.idempotencyStore.Set(ctx, keys[0], resp, 48*time.Hour)
    return resp, nil
}

REST 中 GET、PUT、DELETE 天然幂等,POST 需要通过 Idempotency-Key 头部实现。Stripe 的 API 就采用了这种模式。

8.5 错误处理的统一原则

三种范式的错误表达方式不同,底层原则一致——好的错误响应应包含:错误类型标识(机器可读)、人类可读描述、上下文信息、链路追踪 ID。

REST 用 HTTP 状态码 + 结构化错误体;GraphQL HTTP 状态码始终 200,错误在 errors 数组中;gRPC 用状态码(codes.NotFound 等)+ Status Details。

// gRPC 错误示例
st := status.New(codes.NotFound, "订单不存在")
st, _ = st.WithDetails(&errdetails.ErrorInfo{
    Reason: "ORDER_NOT_FOUND",
    Domain: "order.example.com",
    Metadata: map[string]string{"orderId": "ORD-12345", "traceId": "trace-abc-123"},
})
return nil, st.Err()

九、总结

回到开头的问题:三种 API 范式各自的最佳场景是什么?

REST 适合公开 API、简单 CRUD 系统、需要 HTTP 缓存的场景。最大优势是普适性——每个开发者都理解 REST,每个工具链都支持 REST。核心限制:一个端点只能返回固定结构,面对多客户端差异化需求力不从心。

GraphQL 适合多种客户端有不同数据需求、前端快速迭代、数据关系复杂需要灵活聚合的场景。核心优势是”按需取用”,核心代价是需要 DataLoader、查询复杂度控制、缓存策略重设计、监控体系重建等额外基础设施。

gRPC 适合微服务间通信、流式传输、对延迟和吞吐量有极高要求的场景。Protobuf 的强类型和高效序列化在高频服务间调用中优势明显。核心限制是浏览器支持不友好、第三方接入门槛高。

为什么很多团队的 REST API 越用越痛?根本原因通常是两类错误:第一,在需要 GraphQL 或 gRPC 的场景里硬用 REST;第二,在 REST 范式内没有遵循基本设计原则。

工程中没有银弹。理解每种工具的适用边界,在正确的场景使用正确的工具。


参考资料

  1. Fielding, R. T. “Architectural Styles and the Design of Network-based Software Architectures.” Doctoral dissertation, University of California, Irvine, 2000.
  2. Richardson, L. “Justice Will Take Us Millions Of Intricate Moves.” QCon Talk, 2008.
  3. Facebook Engineering. “GraphQL: A data query language.” 2015.
  4. Google. “gRPC: A high performance, open-source universal RPC framework.” https://grpc.io/
  5. Airbnb Engineering. “Reconciling GraphQL and Thrift at Airbnb.” Airbnb 技术博客。
  6. Nottingham, M. and Wilde, E. “Problem Details for HTTP APIs.” RFC 7807, 2016.
  7. Netflix Technology Blog. “Embracing the Differences: Inside the Netflix API Redesign.” 2012.
  8. Stripe API Documentation. Idempotent Requests. https://stripe.com/docs/api/idempotent_requests
  9. Lee, B. and Vajgel, D. “Scaling the Facebook data warehouse to 300 PB.” 2014.
  10. OpenTelemetry Documentation. “Instrumenting gRPC.” https://opentelemetry.io/docs/

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计】什么是软件架构:从代码结构到系统决策

软件架构的定义至少有三种主流理解,每种指向不同的关注点。本文对比 Booch、Fowler、Johnson 三家定义,分析架构决策的不可逆性特征,拆解 Krutchten 4+1 视图模型,回答一个核心问题:架构和设计的边界到底在哪里。

2026-04-13 · architecture

【系统架构设计】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计】架构决策与 ADR:如何做出可追溯的技术决策

口头约定的架构决策会在人员流动中丢失,会在争论中反复翻车。ADR(Architecture Decision Records)用一种轻量的文档格式,把每一个关键技术决策的背景、选项、理由和代价写下来,跟着代码一起版本管理。本文从 ADR 的三种主流格式讲到 Git 仓库中的实操管理,再拆解 Spotify 和 Uber 的工业实践。