3. API 协议选择:RESTful vs gRPC vs GraphQL
说到特征平台的 Serving API,协议选型是个绕不开的坎。我见过不少团队,一上来就拍脑袋选 REST,结果线上延迟扛不住;也有人盲目追 gRPC,结果调试起来想骂娘。今天咱们就把这三个主流协议掰开揉碎,看看它们到底适合什么场景。
一句话总结:没有银弹。特征平台 Serving 的核心矛盾是「低延迟」和「灵活性」之间的取舍。选哪个,取决于你的特征量级、查询模式和团队运维能力。
3.1 RESTful:老牌劲旅,生态最成熟
RESTful API 说白了就是基于 HTTP 的 JSON 交互。你发一个 GET 或 POST,服务端返回 JSON。简单、直观、谁都能看懂。
优点:
- 调试友好:用 curl、Postman 就能测,浏览器直接看。我当年排查线上问题,全靠 curl 一把梭。
- 生态丰富:几乎所有语言都有 HTTP 客户端,SDK 随手就来。
- 防火墙友好:80/443 端口通行无阻,公司网络策略基本不拦。
缺点:
- 性能瓶颈:JSON 序列化/反序列化开销大,HTTP 头部冗余。单次请求几百字节的元数据,在高 QPS 下就是灾难。
- 无强类型约束:接口文档和实际返回可能不一致。我曾经被一个字段名拼写错误坑过,线上跑了三天才发现。
- 长连接支持弱:HTTP/1.1 的 keep-alive 有限,频繁建连消耗大。
我的建议:如果你的特征平台日均 QPS 低于 10 万,团队以业务开发为主,RESTful 是最稳妥的选择。别为了炫技上复杂协议,维护成本会吃掉你的幸福感。
3.2 gRPC:高性能利器,但门槛不低
gRPC 基于 HTTP/2 和 Protobuf,天生为高性能场景设计。它用二进制编码,支持双向流、多路复用,延迟比 REST 低一个数量级。
优点:
- 极致性能:Protobuf 序列化比 JSON 快 3-10 倍,体积小 60% 以上。我做过压测,同样 1000 个特征,gRPC 延迟只有 REST 的 1/3。
- 强类型契约:.proto 文件即接口文档,生成代码保证类型安全。再也不用担心字段名写错。
- 流式支持:适合批量拉取特征或实时推送更新。
缺点:
- 调试困难:二进制协议肉眼不可读,必须用 grpcurl 或专用工具。线上问题排查比 REST 麻烦得多。
- 浏览器支持差:需要 gRPC-Web 转译,多一层代理。
- 负载均衡复杂:gRPC 的长连接特性让传统 L4 负载均衡失效,需要客户端感知或 L7 代理。
避坑指南:我曾经在一个项目中,团队对 gRPC 不熟,结果 .proto 文件频繁变更,客户端和服务端版本对不上,线上报错查了半天。后来我们强制要求 .proto 文件必须经过 Code Review,并且用 buf 工具做兼容性检查。如果你团队没有 C++/Go 背景的 infra 工程师,慎选 gRPC。
3.3 GraphQL:灵活查询,但别滥用
GraphQL 允许客户端精确指定需要的字段,不多不少。这在特征维度爆炸的场景下很有吸引力——你想想看,一个推荐模型可能要几百个特征,但每次只用到其中几十个。
优点:
- 按需查询:客户端只取需要的特征,减少网络传输。对于移动端或弱网环境特别友好。
- 强类型 Schema:所有特征都有类型定义,查询前就能校验。
- 聚合能力:一次查询可以拉取多个实体的特征,减少请求次数。
缺点:
- 性能不可控:客户端可以发起深层嵌套查询,服务端可能被拖垮。N+1 问题是老生常谈。
- 缓存困难:REST 的 URL 级缓存策略在 GraphQL 下基本失效,所有 POST 请求都走同一个端点。
- 学习曲线陡:Resolver、DataLoader、Subscription……团队需要专门学习。
我的经验:GraphQL 更适合「特征探索」场景——分析师或算法工程师想临时看几个特征的值,用 GraphQL 灵活查询很方便。但线上 Serving 路径,我建议还是用 gRPC 或 REST,因为 GraphQL 的查询解析本身就有额外开销,高并发下扛不住。
3.4 核心对比:一张表说清楚
| 维度 | RESTful | gRPC | GraphQL |
|---|---|---|---|
| 序列化格式 | JSON(文本) | Protobuf(二进制) | JSON(文本) |
| 传输协议 | HTTP/1.1 或 HTTP/2 | HTTP/2 | HTTP/1.1 或 HTTP/2 |
| 延迟(P99) | 5-15ms | 1-5ms | 10-30ms |
| 吞吐量 | 中等 | 高 | 低-中等 |
| 调试难度 | 低 | 高 | 中 |
| 浏览器支持 | 原生 | 需 gRPC-Web | 原生 |
| 流式支持 | SSE / WebSocket | 原生双向流 | Subscription |
| 适用场景 | 中小规模、快速迭代 | 高并发、低延迟、微服务 | 灵活查询、特征探索 |
3.5 选型决策流程
说了这么多,到底怎么选?我一般按这个流程走:
- 先看延迟要求:P99 要求 < 5ms?直接上 gRPC。能接受 10ms+?REST 够用。
- 再看查询模式:每次请求固定取哪些特征?用 REST 或 gRPC。每次请求特征集变化大?考虑 GraphQL。
- 然后看团队能力:团队有 C++/Go 大佬?gRPC 随便玩。全是 Java/Python 业务开发?REST 更稳。
- 最后看运维设施:有没有 Service Mesh(如 Istio)?有的话 gRPC 的负载均衡问题就解决了。没有?REST 更省心。
我的推荐组合:线上 Serving 用 gRPC(性能优先),离线调试和特征探索用 GraphQL(灵活优先),管理面(如特征注册、元数据查询)用 REST(生态成熟)。三个协议各司其职,不冲突。
3.6 架构示意图
下面这张图展示了三种协议在特征平台中的典型位置:
3.7 代码示例:三种协议的调用对比
假设我们要查询用户 ID 为 12345 的 3 个特征(age、city、score),看看三种协议分别怎么写:
RESTful 调用:
GET /api/v1/features?user_id=12345&feature_names=age,city,score
Host: feature-platform.example.com
// 响应
{
"user_id": 12345,
"features": {
"age": 28,
"city": "Beijing",
"score": 0.95
},
"timestamp": 1712345678
}
gRPC 调用(Protobuf 定义):
// feature.proto
service FeatureService {
rpc GetFeatures(FeatureRequest) returns (FeatureResponse);
}
message FeatureRequest {
string user_id = 1;
repeated string feature_names = 2;
}
message FeatureResponse {
string user_id = 1;
map<string, FeatureValue> features = 2;
int64 timestamp = 3;
}
message FeatureValue {
oneof value {
int64 int_val = 1;
double double_val = 2;
string string_val = 3;
}
}
GraphQL 调用:
query {
user(id: "12345") {
age
city
score
}
}
// 响应
{
"data": {
"user": {
"age": 28,
"city": "Beijing",
"score": 0.95
}
}
}
小技巧:如果你决定用 gRPC,建议同时暴露一个 RESTful 的「健康检查」端点(比如 /healthz),方便 K8s 探活和运维人员手动排查。我见过太多 gRPC 服务挂了,运维连 curl 都测不了,只能干瞪眼。
3.8 我的最终建议
选协议这事儿,没有标准答案。但我可以给你一个「最小后悔」策略:
- 起步阶段:先用 RESTful 快速上线,把业务跑通。别在协议上纠结太久。
- 规模扩张:当 QPS 超过 5 万,延迟开始报警时,把核心 Serving 路径迁移到 gRPC。迁移成本不高,因为业务逻辑可以复用。
- 灵活需求:如果算法团队频繁要求「帮我查一下这个新特征的值」,给他们搭一个 GraphQL 网关,让他们自己玩去。
记住,协议只是工具,不是目的。你的目标是让特征 Serving 又快又稳,而不是证明自己会用某个协议。