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

【系统架构设计百科】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡

文章导航

分类入口
architecture
标签入口
#API-design#REST#GraphQL#gRPC#Richardson-maturity-model

目录

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

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

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

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


一、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

分层逻辑:

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

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

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

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .