忘掉 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 — 用户 profile12020.06.0x45075.06.0x
FlatBuffers热读路径 — catalog item12017.17.0x45064.37.0x
MessagePack类 JSON 结构,紧凑12034.33.5x450128.63.5x
CBORIoT / 移动端小 payload12034.33.5x450128.63.5x

四种格式平均 p50 提速:5.0x

模式 1 — Protocol Buffers (Protobuf):类型化、紧凑、快

问题

一个类型化 RPC 返回嵌套用户 profile,解析 JSON 分配了一堆中间字符串。端到端延迟主要卡在解析和内存 churn。

改变

定义一个小 .proto schema,服务端序列化成字节,客户端用生成的代码解析。Protobuf 紧凑,用 codegen 的 parser 比通用 JSON 解析快。

Schema (user.proto)

protobuf
syntax = "proto3";
message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  string region = 4;
  repeated string roles = 5;
}

Python 示例 (protobuf)

python
# 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)

python
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)

python
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)

python
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 用二进制格式。

实际推进

  1. 先测量。用真实客户端抓 JSON p50 和 p99。别盲目优化。

  2. 挑一个端点。选个被很多请求用的热路径,或一个慢的关键调用。

  3. staging 环境做原型。只对这个端点换候选格式。留个 feature flag。

  4. 端到端测量。真实客户端上检查 p50 和 p99。监控 payload 大小和 CPU。

  5. 加兼容性。迁移期间同时支持 JSON 和二进制格式。加 content-type 协商。

  6. 文档化 schema。用 Protobuf 或 FlatBuffers 的话,一定配版本规则和 changelog。

  7. 逐步推。盯客户端解码错误和老 SDK。

单页决策表

  • 类型化、SDK 驱动的客户端和微服务 → Protobuf
  • allocation 问题严重的超热读路径 → FlatBuffers
  • schema 灵活时快速低摩擦见效 → MessagePack
  • 低带宽和受限设备 → CBOR

最后说明

二进制格式是工具,不是信仰。在延迟、带宽和 CPU 重要的地方用。把兼容性和可观测性当一等公民。

如果你从本文抄一个改动,就给一个关键内部 RPC 用二进制格式换 JSON 然后测差异。数字比任何争论说得快。

如果愿意的话,贴个有代表性的 JSON payload 和当前客户端平台。我会画出最小变更集,给出准确的迁移步骤和兼容性检查。

本作品采用《CC 协议》,转载必须注明作者和本文链接