小对象序列化体积膨胀主因是元数据占比过高,包括重复字段名、类型标记、结构分隔符及转义字符等;JSON因明文key和语法符号开销大,msgpack通过二进制编码压缩元数据,protobuf用schema定义字段ID进一步消除字符串key,struct.pack可实现零元数据定制序列化。
频繁传输小对象时,序列化后体积膨胀,并非因为“数据变多了”,而是元数据(metadata)在其中占了大头。小对象本身可能只有几个字节(比如
原始内存约 60–80 字节),但序列化后常达 200–500 字节——多出来的部分,基本是格式描述、类型标记、结构分隔符和冗余标识。
元数据到底包含什么
序列化不是简单地把字段值拼起来,它必须让反序列化端能准确还原对象结构。为此,每种格式都会嵌入额外信息:
字段名重复存储
:JSON 中每个 key(如
、
)都以明文字符串形式出现,每次序列化都完整写出,无法复用;而 Python 对象内部其实只存一份字段名映射(如
的键是共享的)。
类型与结构标记
:msgpack 用单字节前缀标识类型(如 0x82 表示 2 个键值对的 map),但 JSON 必须靠括号和引号推断结构;pickle 更甚,会写入模块路径、类名、实例标志等完整运行时上下文,哪怕对象只是
的两个字段。
空格、换行与转义字符
:默认 human-readable 格式(如 indent=2 的 JSON)会插入大量空白;即使不美化,字符串值中的引号、斜杠也要转义(
变成 5 字节),小字符串极易被“撑大”。
不同格式的元数据占比差异明显
同样一个
对象(两个 int 字段):
JSON:
→ 17 字节,但若字段名稍长或加空格,立刻翻倍;且无法表示 tuple、bytes 等原生类型,需额外约定。
msgpack:
(十六进制)→ 7 字节,靠紧凑二进制编码省去引号和冒号,但仍有 map 长度、key 长度等少量元数据。
pickle(协议 5):至少 30+ 字节,含协议头、类查找指令、字段名字符串对象、引用计数标记等——它设计目标是“全保真还原”,不是“轻量传输”。
为什么小对象特别吃亏
元数据开销通常是固定或弱增长的,而有效载荷(payload)很小,导致“固定成本占比极高”:
一个空 dict
:JSON 是
(2 字节),msgpack 是
(1 字节),看似还好;但一旦加一个字段,JSON 从 2→17 字节(+750%),msgpack 从 1→7 字节(+600%)。
对比大对象:10KB 的嵌套字典,JSON 元数据可能只占 3–5%,膨胀可接受;但 100 字节的对象,元数据轻松占掉 70% 以上。
高频调用下,这种浪费被放大:每秒传 1 万次小状态更新,JSON 多发 1.5MB/s 流量,msgpack 多发 0.3MB/s,而原始数据才 1MB/s。
怎么缓解元数据膨胀
核心思路是“减少重复描述,贴近二进制语义”:
禁用 JSON 美化(
),关闭 ensure_ascii(避免中文转 Unicode);
小对象优先用 msgpack 或 protobuf(定义 schema 后,字段 ID 替代字段名,彻底消除 key 字符串);
对固定结构的小对象(如传感器采样点),用 struct.pack 手写二进制布局,零元数据;
批量打包:把多个小对象合并为一个数组再序列化,摊薄头部开销(如 100 个
合并为 JSON 数组,比发 100 次单条省 60% 体积)。
{'id': 123, 'status': 'ok'}"id""status"__dict__dataclass"a\"b"Point(x=10, y=20){"x":10,"y":20}82 a1 78 0a a1 79 14{}{}80separators=(',', ':'){'t':123,'v':4.5}