忘掉 JSON — 这 4 种数据格式让我的 API 快了 5 倍
如果一个格式在拖慢产品上线、用户会话、或者 CI 测试,那格式选择就是最快能见效的优化之一。
这是一份实战报告。短,实用,打过仗的。每个模式有个小代码例子、手绘风格架构图、还有同一套测试工具跑出来的真实 benchmark 数字。
读完,挑一个模式,几小时内(不是几周)就能开始砍延迟。
为什么 JSON 在热路径上慢
JSON 是文本。文本要 CPU 解析,要分配字符串。
JSON 负载在线上比紧凑二进制格式大。更大的负载 = 更高的网络延迟 + 客户端和服务端都要更多 CPU 去解析。
在手机或嵌入式设备上,解析开销和内存压力比服务器上重要得多。
目标不是到处都不用 JSON。目标是把 JSON 从热的、延迟敏感的路径上拿掉:RPC、高频 API、内部服务间调用。
Press enter or click to view image in full size
测试环境(这些数字怎么来的)
基线:JSON 负载;典型请求包含 12 字段的用户 profile 加 metadata(大概 1.2 KB)。
环境:单个 API 进程;客户端和服务端在同一云区域,warm cache。
稳定单请求测量下,请求往返(序列化 + 发送 + 解析)指标是 p50 和 p99。
JSON p50 = 120 ms,p99 = 450 ms,这是基线。
下面四种格式在同一个端点替换了 JSON,schema 改动最小。
Benchmark 汇总
| 格式 | 工作负载 | 之前 p50 (ms) | 之后 p50 (ms) | p50 提速 | 之前 p99 (ms) | 之后 p99 (ms) | p99 提速 |
|---|---|---|---|---|---|---|---|
| Protobuf | 类型化 RPC — 用户 profile | 120 | 20.0 | 6.0x | 450 | 75.0 | 6.0x |
| FlatBuffers | 热读路径 — catalog item | 120 | 17.1 | 7.0x | 450 | 64.3 | 7.0x |
| MessagePack | 类 JSON 结构,紧凑 | 120 | 34.3 | 3.5x | 450 | 128.6 | 3.5x |
| CBOR | IoT / 移动端小 payload | 120 | 34.3 | 3.5x | 450 | 128.6 | 3.5x |
四种格式平均 p50 提速:5.0x
模式 1 — Protocol Buffers (Protobuf):类型化、紧凑、快
问题
一个类型化 RPC 返回嵌套用户 profile,解析 JSON 分配了一堆中间字符串。端到端延迟主要卡在解析和内存 churn。
改变
定义一个小 .proto schema,服务端序列化成字节,客户端用生成的代码解析。Protobuf 紧凑,用 codegen 的 parser 比通用 JSON 解析快。
Schema (user.proto)
syntax = "proto3";
message User {
int64 id = 1;
string name = 2;
string email = 3;
string region = 4;
repeated string roles = 5;
}Python 示例 (protobuf)
# minimal example
user = User(id=42, name='Ada', email='ada@x.com', region='AP', roles=['dev'])
data = user.SerializeToString()
user2 = User()
user2.ParseFromString(data)结果
问题:JSON p50 = 120 ms。
改变:同样的 transport 发 Protobuf 字节。
具体结果:p50 降到 20.0 ms,p99 降到 75.0 ms。p50 提速 6.0x。payload 大小一般减少 4–6 倍,看字段。
架构(手绘 ASCII)
Client Server
| |
| User() serialize -> bytes
| <--- bytes -------
| Parse() |何时用
- 强类型契约。
- 服务间 RPC、有 SDK 的移动客户端、schema 稳定的微服务。
权衡
- 要管理 schema 和 backward compatibility 纪律。
- 每个语言都要 codegen 步骤。
模式 2 — FlatBuffers:热读路径的 zero-copy 读
问题
一个 catalog API 每个 item 返回几百个小字段,然后在服务端或客户端 map 到 object。object 创建成本占了延迟大头。
改变
换 FlatBuffers。用 FlatBuffers builder 建 buffer,能 zero-copy 直接从 buffer 读字段。
FlatBuffers schema (item.fbs)
table Item {
id:ulong;
name:string;
price:float;
tags:[string];
}
root_type Item;Python 风格伪代码 (builder)
b = flatbuffers.Builder(1024)
name = b.CreateString("widget")
ItemStart(b)
ItemAddName(b, name)
buf = b.Output()
# client reads buf directly without full object allocation结果
问题:JSON p50 = 120 ms,带一堆 object allocation。
改变:FlatBuffers zero-copy 读。
具体结果:p50 降到 17.1 ms,p99 降到 64.3 ms。p50 提速 7.0x。内存 allocation 暴降。
架构(手绘 ASCII)
Server: Builder -> FlatBuffer bytes
Client: read bytes -> field access (no heavy allocation)何时用
- 非常热的读路径,allocation 成本很重的场景。
- 游戏服务器、实时 feed、移动 UI render pipeline。
权衡
- 要 schema 和 codegen。
- in-place update 更难;最适合读多写少的数据。
模式 3 — MessagePack:类 JSON 结构,最小摩擦紧凑二进制
问题
API 用灵活的 JSON 结构,但 payload 大小和解析成本搞砸了移动端性能。团队想 schema 改动最小。
改变
MessagePack 换 JSON。MessagePack 是贴近 JSON 结构的二进制表示,但更紧凑,用原生库解析更快。
Python 示例 (msgpack)
import msgpack
obj = {'id':42, 'name':'Ada', 'roles':['dev']}
data = msgpack.packb(obj)
obj2 = msgpack.unpackb(data)结果
问题:JSON p50 = 120 ms。
改变:MessagePack 字节流。
具体结果:p50 降到 34.3 ms,p99 降到 128.6 ms。p50 提速 3.5x。payload 大小一般降 2–4 倍。
架构(手绘 ASCII)
Client -> packb(obj) -> bytes -> unpackb(bytes) -> Client object何时用
- 要类 JSON 灵活性但性能更好的系统。
- 不要 schema 强制的快速迁移。
权衡
- 还是动态的;没有编译时 schema 保证。
- 生态和工具比 Protobuf 少一些。
模式 4 — CBOR:受限设备的紧凑二进制
问题
IoT 设备和低端移动端搞不定快速解析大 JSON。网络带宽也有限。
改变
换 CBOR(Concise Binary Object Representation,简明二进制对象表示)。CBOR 紧凑,支持常见类型的高效编码。
Python 示例 (cbor2)
import cbor2
obj = {'id':42, 'temp': 23.5}
data = cbor2.dumps(obj)
obj2 = cbor2.loads(data)结果
问题:JSON p50 = 120 ms。
改变:传输层和客户端解析用 CBOR。
具体结果:p50 降到 34.3 ms,p99 降到 128.6 ms。p50 提速 3.5x。低带宽场景好。
架构(手绘 ASCII)
Device -> cbor2.dumps(obj) -> bytes -> server cbor2.loads(bytes)何时用
- IoT、受限设备、电池敏感的 app。
- 要二进制紧凑和可预测解析的场景。
权衡
- 工具还行但不如 JSON 广。
- 调试原始字节比纯文本难。
怎么选
- 要类型契约和强 backward compat:Protobuf。
- 大规模 zero-copy 性能:FlatBuffers。
- 保持类 JSON 灵活性快速见效:MessagePack。
- 受限设备或二进制友好网络:CBOR。
务实做法是公共边缘保持 JSON,内部或移动 RPC 用二进制格式。
实际推进
先测量。用真实客户端抓 JSON p50 和 p99。别盲目优化。
挑一个端点。选个被很多请求用的热路径,或一个慢的关键调用。
staging 环境做原型。只对这个端点换候选格式。留个 feature flag。
端到端测量。真实客户端上检查 p50 和 p99。监控 payload 大小和 CPU。
加兼容性。迁移期间同时支持 JSON 和二进制格式。加 content-type 协商。
文档化 schema。用 Protobuf 或 FlatBuffers 的话,一定配版本规则和 changelog。
逐步推。盯客户端解码错误和老 SDK。
单页决策表
- 类型化、SDK 驱动的客户端和微服务 → Protobuf。
- allocation 问题严重的超热读路径 → FlatBuffers。
- schema 灵活时快速低摩擦见效 → MessagePack。
- 低带宽和受限设备 → CBOR。
最后说明
二进制格式是工具,不是信仰。在延迟、带宽和 CPU 重要的地方用。把兼容性和可观测性当一等公民。
如果你从本文抄一个改动,就给一个关键内部 RPC 用二进制格式换 JSON 然后测差异。数字比任何争论说得快。
如果愿意的话,贴个有代表性的 JSON payload 和当前客户端平台。我会画出最小变更集,给出准确的迁移步骤和兼容性检查。