带 schema 的 Protobuf 和不带 schema 的 JSON、MessagePack 到底差在哪?这篇不只比体积:速度、校验、没有 schema 时能读出什么、数据损坏时的表现、内存占用,逐项量一遍,看有 schema 带来的好处有多少只是“不重复写键名”。
JSON(标准库) 编码 39.7 ms 解码 43.3 ms JSON(orjson) 编码 4.9 ms 解码 20.2 ms MessagePack 编码 13.0 ms 解码 30.8 ms Protobuf(从 dict 构造消息再序列化) 编码 42.4 ms Protobuf(仅解析,不访问字段) 编码 - 解码 3.8 ms Protobuf(解析并访问全部字段) 编码 - 解码 25.7 ms Protobuf 解析后转成 dict(MessageToDict) 解码 186.3 ms(转成 Python 字典的代价)
Protobuf 的解析本身很快(3.8 ms),逐个访问字段时才花时间(25.7 ms);但要得到 Python 字典(MessageToDict)要 186.3 ms。orjson 的解码是 20.2 ms,编码 4.9 ms。从 dict 构造 Protobuf 消息再序列化是 42.4 ms。
price 写成字符串 "abc" JSON 接受、MsgPack 接受、Protobuf 抛出 TypeError(must be real number, not str)
缺少 url 字段 JSON 接受、MsgPack 接受、Protobuf 接受,序列化 128 字节
多了一个未知字段 extra JSON 接受、MsgPack 接受、Protobuf 抛出 AttributeError(Protocol message Product has no "extra" )
stock 写成 1.5 JSON 接受、MsgPack 接受、Protobuf 抛出 TypeError('float' object cannot be interpreted as )
id 写成 2**70 JSON 接受、MsgPack 抛出 OverflowError、Protobuf 抛出 ValueError(Value out of range: 11805916207174113034)schema 在赋值时能拦住类型错误和未知字段(JSON、MessagePack 不会),但缺字段不会被拦,会用默认值。需要“必填”要自己检查。
JSON 前 80 字节:b'{"id":1000000,"url":"https://shop.example.com/item/1000000?ref=list&page=0","tit'
MessagePack 解成通用结构(不知道任何 schema):{'id': 1000000, 'url': 'https://shop.example.com/item/1000000?ref=list&page=0', 'title': 'wico biwa wasu kosekefo julome…
Protobuf 用 protoc --decode_raw 看到的前 8 行:1: 1000000 | 2: "https://shop.example.com/item/1000000?ref=list&page=0" | 3: "wico biwa wasu kosekefo julomesa zanisa wegiraka" | 4: 0x405c59999999999a | 5: "CNY" | 6: 249 | 7: 0x3ff199999999999a | 8: "tag-wizetesa"
歧义:把恰好是合法消息的字节(08 01 12 01 78)存进 title 字符串字段,decode_raw 显示为:3 { 1: 1 2: "x" }(没有 schema 分不清它是字符串还是嵌套消息)
浮点 price 在 decode_raw 里显示为 4: 0x405c59999999999a(只知道是 64 位,不知道是 double 还是 fixed64 还是别的)JSON 和 MessagePack 的数据里带着键名和类型。Protobuf 没有 schema 只能看到字段号和粗略类型,字符串与嵌套消息、double 与 fixed64 都分不清。调试时要保留 .proto。
表示 字节 zstd-3 JSON 对象(每条带键名) 7214161 1331194 JSON 数组 + 表头 5034293 1221884 MessagePack 对象 6357305 1413148 MessagePack 数组 + 表头 4717408 1307318 Protobuf 4914350 1378051
Protobuf 比 JSON 对象小很多,但把键名挪到表头的 JSON 数组(5034293 字节)只比 Protobuf(4914350)大 2.4%,zstd 压缩后还更小(1221884 对 1378051)。Protobuf 的优势里有一大块只是“不重复写键名”。
JSON 358 字节:抛异常 1116、解出相同 0、静默解出不同的值 1884(共 3000 次) MessagePack 316 字节:抛异常 595、解出相同 0、静默解出不同的值 2405(共 3000 次) Protobuf 241 字节:抛异常 551、解出相同 0、静默解出不同的值 2449(共 3000 次)
翻转 1 个字节:三种格式都有一半以上情况下静默解出不同的值,没有一次解出相同(因为总是改变了某个值)。要发现损坏得靠校验和(CRC、zstd 帧的校验等),格式本身不提供。
json 输入 6.9 MiB,解码后峰值内存增量 36 MiB(3 次:[36, 36, 37],macOS 的 ru_maxrss 单位是字节) orjson 输入 6.9 MiB,解码后峰值内存增量 50 MiB(3 次:[50, 50, 50],macOS 的 ru_maxrss 单位是字节) msgpack 输入 6.1 MiB,解码后峰值内存增量 29 MiB(3 次:[29, 29, 29],macOS 的 ru_maxrss 单位是字节) protobuf 输入 4.7 MiB,解码后峰值内存增量 11 MiB(3 次:[11, 11, 11],macOS 的 ru_maxrss 单位是字节)
解码 2 万条商品:Protobuf 11 MiB,msgpack 29 MiB,标准库 json 36 MiB,orjson 50 MiB。Python 对象本身的开销远大于数据大小;orjson 更快但峰值更高,原因我没有查。