如果一个格式在拖慢产品上线、用户会话、或者 CI 测试,那格式选择就是最快能见效的优化之一。
这是一份实战报告。短,实用,打过仗的。每个模式有个小代码例子、手绘风格架构图、还有同一套测试工具跑出来的真实 benchmark 数字。
读完,挑一个模式,几小时内(不是几周)就能开始砍延迟。
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 改动最小。
| 格式 | 工作负载 | 之前 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
一个类型化 RPC 返回嵌套用户 profile,解析 JSON 分配了一堆中间字符串。端到端延迟主要卡在解析和内存 churn。
定义一个小 .proto schema,服务端序列化成字节,客户端用生成的代码解析。Protobuf 紧凑,用 codegen 的 parser 比通用 JSON 解析快。
syntax = "proto3";
message User {
int64 id = 1;
string name = 2;
string email = 3;
string region = 4;
repeated string roles = 5;
}# 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 倍,看字段。
Client Server
| |
| User() serialize -> bytes
| <--- bytes -------
| Parse() |一个 catalog API 每个 item 返回几百个小字段,然后在服务端或客户端 map 到 object。object 创建成本占了延迟大头。
换 FlatBuffers。用 FlatBuffers builder 建 buffer,能 zero-copy 直接从 buffer 读字段。
table Item {
id:ulong;
name:string;
price:float;
tags:[string];
}
root_type Item;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 暴降。
Server: Builder -> FlatBuffer bytes
Client: read bytes -> field access (no heavy allocation)API 用灵活的 JSON 结构,但 payload 大小和解析成本搞砸了移动端性能。团队想 schema 改动最小。
MessagePack 换 JSON。MessagePack 是贴近 JSON 结构的二进制表示,但更紧凑,用原生库解析更快。
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 倍。
Client -> packb(obj) -> bytes -> unpackb(bytes) -> Client objectIoT 设备和低端移动端搞不定快速解析大 JSON。网络带宽也有限。
换 CBOR(Concise Binary Object Representation,简明二进制对象表示)。CBOR 紧凑,支持常见类型的高效编码。
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。低带宽场景好。
Device -> cbor2.dumps(obj) -> bytes -> server cbor2.loads(bytes)务实做法是公共边缘保持 JSON,内部或移动 RPC 用二进制格式。
先测量。用真实客户端抓 JSON p50 和 p99。别盲目优化。
挑一个端点。选个被很多请求用的热路径,或一个慢的关键调用。
staging 环境做原型。只对这个端点换候选格式。留个 feature flag。
端到端测量。真实客户端上检查 p50 和 p99。监控 payload 大小和 CPU。
加兼容性。迁移期间同时支持 JSON 和二进制格式。加 content-type 协商。
文档化 schema。用 Protobuf 或 FlatBuffers 的话,一定配版本规则和 changelog。
逐步推。盯客户端解码错误和老 SDK。
二进制格式是工具,不是信仰。在延迟、带宽和 CPU 重要的地方用。把兼容性和可观测性当一等公民。
如果你从本文抄一个改动,就给一个关键内部 RPC 用二进制格式换 JSON 然后测差异。数字比任何争论说得快。
如果愿意的话,贴个有代表性的 JSON payload 和当前客户端平台。我会画出最小变更集,给出准确的迁移步骤和兼容性检查。