MessagePack 常被说成“更小更快的 JSON”。这篇用 4 个数据集量一量:未压缩和压缩之后各小多少,为什么有的数据几乎没变小,速度比谁快,类型保真和流式读取有什么不同。
整数 0 1 字节 00
整数 127 1 字节 7f
整数 128 2 字节 cc 80
整数 255 2 字节 cc ff
整数 256 3 字节 cd 01 00
整数 65535 3 字节 cd ff ff
整数 65536 5 字节 ce 00 01 00 00
整数 -1 1 字节 ff
整数 -32 1 字节 e0
整数 -33 2 字节 d0 df
整数 2**32 9 字节 cf 00 00 00 01 00 00 00 00
浮点 1.5 9 字节 cb 3f f8 00 00 00 00 00 00
浮点 0.1 9 字节 cb 3f b9 99 99 99 99 99 9a
True / None 3 字节 92 c3 c0
字符串 "a" 2 字节 a1 61
字符串 31 字节 32 字节 bf 78 78 78 78 78 78 78 78 78 78 78 …
字符串 32 字节 34 字节 d9 20 78 78 78 78 78 78 78 78 78 78 …
字符串 255 字节 257 字节 d9 ff 78 78 78 78 78 78 78 78 78 78 …
字符串 256 字节 259 字节 da 01 00 78 78 78 78 78 78 78 78 78 …
bytes 3 字节 5 字节 c4 03 61 62 63
{"a":1} 4 字节 81 a1 61 01
[1,2,3] 4 字节 93 01 02 03
JSON 对照:127 → 3 字节、65536 → 5 字节、"x"*31 → 33 字节、{"a":1} → 7 字节、0.1 → 3 字节小整数 1 字节,更大的整数 2、3、5、9 字节;字符串头 1 到 3 字节;浮点一律 9 字节;容器头 1 字节起。JSON 里短的浮点(0.1 只要 3 字节)和 {"a":1}(7 字节)对比 MessagePack 的 9 字节和 4 字节,小整数、短键名占优的是 MessagePack,短浮点占优的是 JSON。
数据集 JSON MsgPack 省下 压缩后 zstd-3: JSON MsgPack 差 gzip-6: JSON MsgPack lz4: JSON MsgPack products 7214163 6357308 11.9% 1331144 1413136 -6.2% 1484671 1496373 2314042 2385707 timeseries 2744543 2000003 27.1% 352531 298689 15.3% 313607 321012 649008 580243 nested 5582052 4062921 27.2% 660737 606310 8.2% 553565 546978 1117578 1047168 blobby 2224945 1652673 25.7% 1578156 1579529 -0.1% 1592993 1582706 2117830 1605618
未压缩时 MessagePack 小 11.9%(products)到 27.2%(nested)。但 zstd 压缩之后差距变了:products 上 MessagePack 反而大 6.2%,timeseries 小 15.3%,nested 小 8.2%,含随机二进制的 blobby 基本相同。
products:MessagePack 逐值统计合计 6357305 字节(整批打包 6357308 字节,差在数组头);字符串 64.8%;键名 25.5%;浮点 5.7%;整数 3.1%;容器头 0.9% timeseries:MessagePack 逐值统计合计 2000000 字节(整批打包 2000003 字节,差在数组头);键名 57.5%;浮点 22.5%;整数 17.5%;容器头 2.5% products:JSON 7214163 → MsgPack 6357308,省下 11.9%;timeseries:2744543 → 2000003,省下 27.1%
字符串多的数据(products 字符串占 64.8%),MessagePack 没什么可省;数值多的数据(timeseries 键名 57.5%、浮点和整数共 40%)省得多。数字越多,受益越大。
products:json.dumps 40.8 ms;orjson.dumps 5.0 ms;msgpack.packb 13.7 ms;json.loads 45.0 ms;orjson.loads 21.7 ms;msgpack.unpackb 32.9 ms timeseries:json.dumps 25.2 ms;orjson.dumps 3.3 ms;msgpack.packb 8.7 ms;json.loads 25.4 ms;orjson.loads 10.5 ms;msgpack.unpackb 16.3 ms
msgpack 比标准库 json 快(打包约 3 倍),但比 orjson 慢。“二进制就是快”不成立,要看对比的是哪个 JSON 实现。
tuple (1, 2) 3 字节 → [1, 2]
整数键 {1: "a"} 4 字节 → {1: 'a'}
bytes b"ab" 4 字节 → b'ab'
str "ab" 3 字节 → 'ab'
2**63 9 字节 → 9223372036854775808
2**64 打包抛出 OverflowError
float nan 9 字节 → nan
datetime 打包抛出 TypeError
set {1,2} 打包抛出 TypeError
use_bin_type=False(旧格式):bytes 和 str 打包成同一类型 → {'a': 'ab', 'b': 'ab'}
默认 strict_map_key=True 解包整数键:ValueErrorMessagePack 比 JSON 多了 bytes 和整数键(解包要 strict_map_key=False),tuple 会变成 list,2^64 溢出,datetime 和 set 要自己处理。bytes 和 str 的区分需要 use_bin_type=True。
MessagePack 流 637751 字节(没有分隔符),按 1000 字节切成 638 块喂 Unpacker:取出 2000/2000 条;JSON Lines 723399 字节(靠换行分隔) 把流截断在中间(多出半条):Unpacker 取出 1001 条完整记录,最后半条不会报错,只是留在缓冲里(再喂剩下的字节就能取出) JSON Lines 截断在同样位置:按行解析,1001/1002 行可解析(最后一行是半行,json.loads 抛出 JSONDecodeError)
每个 MessagePack 值自带长度,多条记录直接拼接,用 Unpacker 分块喂入就能取出(实验里 2000/2000)。JSON Lines 靠换行分隔。被截断的最后半条,Unpacker 不报错,只是还不能取出。
版本 JSON MsgPack zstd JSON zstd Msg MsgPack 比 JSON(压缩后)
原样(price/rating 是浮点) 7214163 6357308 1331144 1413136 +6.2%
price→整数分、rating→整数 7176128 6077249 1336116 1388457 +3.9%
去掉 price 和 rating 6675704 5737308 1208518 1280003 +5.9%
参考:1000 个 price 的 8 字节 float64 二进制(拼接)zstd-3 后 3732 字节(原 8000 字节);同样 1000 个 price 的文本(逗号分隔)zstd-3 后 2619 字节(原 5921 字节)
只看字符串字段(把某个字段的 2 万个值单独拿出来):
title JSON 数组 1223717 → zstd 413803;MsgPack 数组 1203160 → zstd 436587;换行分隔的纯文本 1183715 → zstd 409620
url JSON 数组 1135001 → zstd 11989;MsgPack 数组 1115003 → zstd 12386;换行分隔的纯文本 1094999 → zstd 12331
updated_at JSON 数组 460001 → zstd 100143;MsgPack 数组 420003 → zstd 99373;换行分隔的纯文本 419999 → zstd 99436
category JSON 数组 451749 → zstd 35091;MsgPack 数组 411751 → zstd 35938;换行分隔的纯文本 411747 → zstd 34720检验了两个猜想:浮点的 8 字节二进制压得差一些,但去掉浮点字段后 MessagePack 仍然大 5.9%,所以不是主因;长度不一的 title 字符串单独压缩时,MessagePack 数组比 JSON 数组大 5.5%,长度一致的 url 就没有差别。我的解释是字符串长度前缀是每条都要单独存的信息,而 JSON 的结束引号更好预测,这个解释没有拆到压缩器内部验证。