版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。
摘要:2026-11-10 在本机把一条 74 字节的 JSON 响应交给
gzip.compress,压完是 89 字节,比原样大 15 字节。同一份数据 1000 条各自压一遍,总字节从 74000 涨到 89000;把这 1000 条合并成一个数组只压一次,结果是 362 字节,比逐个压少 88638 字节。本文只算字节,不折算带宽费用,也不引用任何厂商报价。
先把事实钉死:74 字节的响应,压完是 89 字节
一条 74 字节的响应开了 gzip,出来的包是 89 字节,比不压多 15 字节。这不是参数填错,也不是压缩级别调低了——是 gzip 这个格式在每个压缩流上都要先收一笔固定费用,而这笔费用比它能压掉的内容还多。
74 字节是这么来的
响应体是这条记录序列化后的样子:{"id":0,"status":"ok","latency_ms":123,"cached":true,"region":"cn-east-1"}。五个字段,键名全拼,值都很短,separators=(",", ":") 已经把逗号冒号后面的空格去掉了,长度仍是 74 字节。
这个形状在智能体服务里密度很高:一次工具调用的回执、一次任务状态的轮询、一次缓存命中查询,都是「字段少、值短、一条就够」,没有必要翻页,也没有必要批量。本文刻意不改写这个形状(比如把键名压成单字母),因为压缩的收益取决于内容重复度,而真实接口的键名恰恰是最不重复的那部分——改小了输入,测出来的就不是线上那个问题。
三个数一口清:raw 74、gzip 89、deflate 71
脚本只有二十来行,输出直接给出 74 和 89,同时把 89 拆成「18 字节头尾 + 71 字节 deflate」。
🧪 实测环境:Python 3.13.12 / macOS / 标准库 zlib 与 gzip(gzip.compress 用 mtime=0 固定头)
import gzip
import json
N = 1000
REC = {"id": 0, "status": "ok", "latency_ms": 123, "cached": True, "region": "cn-east-1"}
raw = json.dumps(REC, separators=(",", ":")).encode()
one_gz = gzip.compress(raw, 6, mtime=0)
batch_raw = json.dumps([REC] * N, separators=(",", ":")).encode()
batch_gz = gzip.compress(batch_raw, 6, mtime=0)
print("per response raw : %d B" % len(raw))
print("per response gzip : %d B (18 B header/trailer + %d B deflate)" % (len(one_gz), len(one_gz) - 18))
print("%d separate, raw : %d B" % (N, len(raw) * N))
print("%d separate, gzip : %d B" % (N, len(one_gz) * N))
print("separate gzip delta : %+d B (%.1f%%)" % (
len(one_gz) * N - len(raw) * N, (len(one_gz) / len(raw) - 1) * 100))
print("one batch, raw : %d B" % len(batch_raw))
print("one batch, gzip : %d B" % len(batch_gz))
print("batch vs 1000 gzip : %d B saved" % (len(one_gz) * N - len(batch_gz)))
per response raw : 74 B
per response gzip : 89 B (18 B header/trailer + 71 B deflate)
1000 separate, raw : 74000 B
1000 separate, gzip : 89000 B
separate gzip delta : +15000 B (20.3%)
one batch, raw : 75001 B
one batch, gzip : 362 B
batch vs 1000 gzip : 88638 B saved
15 字节是净亏,不是误差
89 等于 18 加 71。71 是 deflate 压完的净长度,74 减 71 只省下 3 个字节;18 减 3 等于 15。这条账的关键不是「省得少」,而是「先付后省」:固定开销在压缩结果还没产生时就已经计入了。
把这条账按 1000 条摊开,就是输出里第 5 行的 separate gzip delta : +15000 B (20.3%)。注意里面的 20.3% 不是「压缩效率」的意思,是「总字节涨幅」——方向是涨,不是降。
那 18 字节的固定开销,就是阈值的来源
阈值不是一个拍出来的经验值,它是固定开销除以「压缩能省下的比例」:省得不够多,18 字节就摊不平。所以要先看清这 18 字节到底由什么组成。
头 10 字节、尾 8 字节,是照结构图数出来的
按 RFC 1952 §2.3 的结构图,gzip 成员的头部位固定排了 10 个字节格:ID1、ID2、CM、FLG、MTIME 占 4 字节、XFL、OS;尾部是 CRC-32 占 4 字节、ISIZE 占 4 字节,合计 8 字节。
这里必须把口径说清:规范正文并没有用文字写出「10 字节」「8 字节」「18 字节」这几个数,它们是照着结构图一格一格数出来的。所以准确的说法是「按 RFC 1952 §2.3 的结构图,头部固定字段 10 字节、尾部 8 字节」,而不是「规范规定 18 字节」。
各字段的人话版:ID1 和 ID2 是两个固定字节,解码方靠它一眼认出「这是 gzip 流」;CM 标明压缩方法,值 8 表示 deflate;FLG 是标志位,决定后面还跟不跟可选字段;MTIME 是 4 字节的修改时间;XFL 给压缩级别留个提示;OS 记录生成方。尾部的 CRC-32 是解压结果的校验和,ISIZE 是解压后的长度。前 10 个字节决定「怎么解」,后 8 个字节决定「解出来对不对」。
MTIME 写 0,只为让输出可复现
RFC 1952 §2.3.1 里有一句原文:MTIME = 0 means no time stamp is available. 意思是时间戳不可用时写 0。
这一项不影响长度,4 个字节该占还是占,它影响的是内容。Python 的 gzip.compress 文档字符串写着 The modification time is set to the current time by default.——默认塞当前时间,那么同一份数据隔一秒压两次,字节内容就不同。两个脚本都显式传 mtime=0,目的只有一个:让每次跑出来的 89 字节逐字一致,读数才可对照。这也是本文所有输出能贴出来核对的前提。
顺带把压缩级别也说清。gzip.compress 的签名是 compress(data, compresslevel=9, *, mtime=None),默认级别是 9,脚本里显式写 6。两个脚本用同一档,本文全部读数都在这一档下取得;跨档比较不在本文范围内,也不该拿这里的数字去对别的级别。
18 字节付完之后,剩下的才轮到压缩
把 74 字节拆成两段账:先付 18 字节,剩下的部分才有资格谈压缩。deflate 把这 74 字节压到 71 字节,省下的 3 个字节连固定开销的零头都不够。
为什么只能省 3 字节:deflate 靠两件事吃饭——「前面的重复子串」和「高频字节用短码」。74 字节里能重复的只有引号、逗号和几个键名前缀,样本太短,连统计表都还没建起来,deflate 自己的块头开销就把收益吃掉了大半。内容越短,可用于建模的样本越少,压缩比越接近 1,而固定开销不变。
九个档位摊平:从 65 字节到 6455 字节
九个档位跑完,结论只落在相邻两档之间:65 字节时压完是 79 字节,比原样大 14 字节、比例 1.215;120 字节时压完是 95 字节,比原样小 25 字节、比例 0.792。翻转就发生在 65 和 120 之间。
口径:target 是下限,raw 是真实长度
脚本按「至少 n 字节」的方式凑记录:不停追加五字段的记录,直到整体 JSON 超过 n 为止。所以表里的 target 是门槛,raw 才是真实长度——65、120、229 这些数都不是整数目标,原因就在这里。两者混着看,会得出「50 字节压完 79 字节」这种错结论。
🧪 实测环境:Python 3.13.12 / macOS / 标准库 zlib 与 gzip(gzip.compress 用 mtime=0 固定头)
import gzip
import json
SIZES = [50, 100, 200, 400, 800, 1200, 1600, 3200, 6400]
def body_at_least(n):
items = []
size = 2
i = 0
while size < n:
rec = {"id": i, "status": "ok", "latency_ms": 120 + i % 7, "cached": i % 2 == 0}
items.append(rec)
size += len(json.dumps(rec, separators=(",", ":"))) + 1
i += 1
return json.dumps({"items": items}, separators=(",", ":")).encode()
print("%8s %8s %8s %8s %8s" % ("target", "raw", "gzip", "delta", "ratio"))
for n in SIZES:
raw = body_at_least(n)
gz = gzip.compress(raw, 6, mtime=0)
print("%8d %8d %8d %+8d %8.3f" % (n, len(raw), len(gz), len(gz) - len(raw), len(gz) / len(raw)))
target raw gzip delta ratio
50 65 79 +14 1.215
100 120 95 -25 0.792
200 229 108 -121 0.472
400 447 134 -313 0.300
800 833 172 -661 0.206
1200 1222 206 -1016 0.169
1600 1610 236 -1374 0.147
3200 3220 361 -2859 0.112
6400 6455 581 -5874 0.090
九档摊平在一张表上,才好横向比:
| target(门槛) | raw(真实长度) | gzip 后 | delta | ratio |
|---|---|---|---|---|
| 50 | 65 | 79 | +14 | 1.215 |
| 100 | 120 | 95 | -25 | 0.792 |
| 200 | 229 | 108 | -121 | 0.472 |
| 400 | 447 | 134 | -313 | 0.300 |
| 800 | 833 | 172 | -661 | 0.206 |
| 1200 | 1222 | 206 | -1016 | 0.169 |
| 1600 | 1610 | 236 | -1374 | 0.147 |
| 3200 | 3220 | 361 | -2859 | 0.112 |
| 6400 | 6455 | 581 | -5874 | 0.090 |
口径:算的是「一份 JSON 响应体压成 gzip 成员后的字节数」,函数调用本身的开销、内存占用、CPU 时间都不在账内;不含 HTTP 头(Content-Encoding 等字段不参与比较);ratio 是 gzip 后除以 raw,小于 1 才算省。取数 2026-11-10,环境同上,数据形状为五字段小 JSON 记录。
固定成本的占比,从两成掉到三个百分点
用表里的 gzip 列减 18,得到 deflate 净长度:65 字节档是 79 − 18 = 61,120 字节档是 95 − 18 = 77,6455 字节档是 581 − 18 = 563。
再把 18 除以 gzip 列,得到固定开销在成品里的占比:65 字节档 22.8%,120 字节档 18.9%,6455 字节档 3.1%。同一个 18 字节,在 79 字节里是两成多,在 581 字节里只剩三个百分点。「该不该压」这件事的答案,本质上是被这个除法决定的。
反过来看更直观:120 字节那一档,raw 120 压成 95,省 25 字节;如果固定开销是 0,这 120 字节本该变成 77。也就是说这一档真正压掉的是 43 字节,其中 18 字节被头尾吃走了。
65 和 120 不是通用阈值
这两个数只在本文这一种数据形状下成立:五字段、键名英文全拼、值都是短标量、记录之间高度相似。换个形状,翻转点就会移——纯二进制载荷、已经压缩过的图片、键名极短的结构,位置都不在同一个地方。
所以本文不给「多少字节以上才压」的通用数字。给的是一个可复现的测法:拿你自己接口的真实响应,从 50 字节一路跑到 6400 字节,看哪一档 ratio 掉到 1 以下,那两档之间就是你的阈值。测法比重测一次结论更值钱,因为数据形状会随着接口版本变。
同样的活算一遍:1000 个响应逐个压,多花 15000 字节
1000 条响应各自压一遍,总字节从 74000 涨到 89000,多 15000 字节,涨幅 20.3%。这是第 1 章那段输出的第 3 到第 5 行,只是把「一条」换成了「一千条」。
18 乘 1000,加回来就是这 15000
固定开销 18 乘 1000,是 18000 字节;deflate 层每条省 3 字节,1000 条省 3000 字节;18000 减 3000 等于 15000。三个数完全对得上:74000 加 18000 减 3000 等于 89000。
这笔账的形状值得记住:固定开销是线性的、按条收费的,而压缩收益是跟着内容重复度走的。 当一条记录短到压不动时,前者就成了主项——条数越多,多出来的字节越多,比例却始终不动。
20.3% 是摊出来的,不是拍出来的
89 除以 74 等于 1.203,也就是涨幅 20.3%;89000 除以 74000 同样是 1.203。两个算法给出同一个数,说明这件事与条数无关:只要每条响应各自起一个 gzip 流,条数翻十倍,多出来的字节也翻十倍,比例一动不动。
| 做法 | 测得总字节 | 差额(对齐哪一行) |
|---|---|---|
| 1000 条不压,原样发 | 74000 B | 基准 |
| 1000 条各自压一遍 | 89000 B | 比基准多 15000 B(20.3%) |
| 1000 条合并成一个数组,不压 | 75001 B | 比基准多 1001 B |
| 1000 条合并成一个数组,压一次 | 362 B | 比「各自压」少 88638 B |
口径:四行用的是同一份记录(五字段、内容完全相同)、同一个压缩级别 6、同一个 mtime=0;只统计响应体字节,不含 HTTP 头、TLS 记录层开销和连接建立成本;「合并」指把 1000 条塞进一个 JSON 数组一次压完,不含分片与重组;第四行的差额对齐的是第二行,前三行对齐的是第一行。取数 2026-11-10,数字全部来自第 1 章那段脚本的输出。
这笔账不是 gzip 的锅,是「一条一个流」的锅
RFC 1952 定义的单位是「成员」(member),一个成员自带一套头尾。你发 1000 个响应,就是 1000 个成员,就是 1000 份 18 字节——这不是某个库的实现细节,是格式定义决定的:每个成员都必须能独立解压,所以每个成员都必须自带校验和与长度。
这也解释了为什么换压缩库、换语言、换级别都改不了这个结论:18 字节不是可调参数,它是成员结构的固定部分。
这份资料是什么:一份把 gzip 成员结构、deflate 输出与本文这套「按字节对账」的测法放在一起的整理,含九档梯子的复现脚本与口径说明。放在资料包里,扫码即可获取:
为什么合并成一个批能省 88638 字节
因为这 1000 条记录内容完全相同,合到一个流里之后,deflate 从第 2 条起几乎不用再写新内容,全是指回前面那一段;而头尾只付一次。89000 减 362 等于 88638 字节。
75001 减 74000:合并的代价只有 1001 字节
1000 条合成一个 JSON 数组,raw 从 74000 变成 75001。多出来的 1001 字节全是结构符:一对方括号 2 字节,999 个逗号 999 字节。记录本身一个字节没变——合并这件事几乎不要钱,它只是把 1000 份文档装进一个信封。
362 字节里,344 是 deflate
362 减 18 等于 344。75001 字节的内容压成 344 字节,占 0.46%。同样一份数据,逐个压是 89000 字节,合并压是 362 字节,差 88638 字节——后者的总量比前者的零头还小。
差距来自哪里:deflate 是「查前面的重复」。单条 74 字节的流,窗口里只有这 74 字节,第 1 个字节之前什么都没有,所以它只能做一次很勉强的字节频率统计;1000 条一样的记录排进同一个流,第 2 条开始每个字段名、每个值都能在前面找到一模一样的串,输出退化成一组「长度 + 距离」的指代。
一句话:压缩的收益来自「同一份东西出现第二次」,而「每条各自压」恰好把这个条件掐掉了。 18 字节只是把这件事的代价显性化,真正贵的是每次重开一个流,等于把已经学到的重复全部忘掉。
一条判断顺序:先合并,再压缩
顺序不能反。「先压再想合不合并」是最常见的错法:压缩是逐流生效的,先压了,重复就被切碎在各自的流里,事后再合并只能得到一堆已压缩的分片。
| 场景 | 做法 | 本文依据 |
|---|---|---|
| 单条响应不到 100 字节(健康检查、状态位、单条回读) | 不压,原样发 | 65 字节档压完 79,比原样大 14 |
| 单条响应 120 字节到 1KB(单条详情、列表首屏) | 压 | 120 字节档 95、447 字节档 134,都小于原长 |
| 同一批 1000 条以上、对端能一次收下 | 合并成一个响应再压 | 75001 压成 362,比逐个压少 88638 |
| 负载本身已压缩过(图片、归档包、已 gzip 的体) | 不再压 | 无重复可查,只剩 18 字节固定开销 |
口径:前三行的「依据」是本文脚本的实测读数;第四行是推论,本文脚本未覆盖已压缩载荷,列出来是因为它决定了判断顺序的第一道闸门。金额、带宽单价与 CPU 成本均不在账内,只算字节。取数 2026-11-10。
执行起来就三步:先问这个响应能不能装下一批——能,合并再压;不能,再看单条多大——不到 100 字节直接发,120 字节以上再压。停手的位置同样清楚:合并之后如果对端一次收不下,就按本文这一档切 1000 条,别把一个响应撑到几兆再交给网关,那时候省下来的字节会以超时和重传的形式还回去。
这份资料是什么:一份按「先合并、再压缩、最后看阈值」顺序整理的检查清单,附本文九档梯子的原始输出和复现命令。放在资料包里,扫码即可获取:
附表 A:本文引用事实与出处对照表
| 事实(含原文关键句) | 出处 | 本文位置 |
|---|---|---|
| gzip 成员结构:头部固定字段为 ID1、ID2、CM、FLG、MTIME(4 字节)、XFL、OS 共 10 个字节格;尾部为 CRC-32(4 字节)+ ISIZE(4 字节)= 8 字节。规范正文未以文字写出这三个数字,系按结构图清点 | RFC 1952(GZIP file format specification version 4.3)§2.3,https://www.rfc-editor.org/rfc/rfc1952#section-2.3 | 第 2 章 |
| 「MTIME = 0 means no time stamp is available.」 | RFC 1952 §2.3.1,https://www.rfc-editor.org/rfc/rfc1952#section-2.3.1 | 第 2 章 |
签名 compress(data, compresslevel=9, *, mtime=None);文档字符串「The modification time is set to the current time by default.」 | Python 3.13.12 标准库 gzip(2026-11-10 用 inspect.signature 读取),https://docs.python.org/3/library/gzip.html#gzip.compress | 第 2 章 |
| 单条响应 raw 74 B、gzip 89 B(18 B 头尾 + 71 B deflate);1000 条逐个压 74000 B → 89000 B,差 +15000 B(20.3%);合并一批 75001 B → 362 B,省 88638 B | 本文脚本 e04b_gzip_batch.py 输出(Python 3.13.12 / macOS,2026-11-10) | 第 1、4、5 章 |
| 九档梯子 65→79(1.215)、120→95(0.792)、229→108、447→134、833→172、1222→206、1610→236、3220→361、6455→581(0.090) | 本文脚本 e04a_gzip_floor.py 输出(同环境,2026-11-10) | 第 3 章 |
JSON 紧凑输出用 separators=(",", ":") 去掉分隔符后的空格 | Python 3.13 文档 json.dumps,https://docs.python.org/3/library/json.html#json.dumps | 第 1 章 |
| 「阈值落在 65 与 120 字节之间」及「不可外推到其它数据形状」属本文实测结论与判断,规范未定义压缩阈值 | 由本文 e04a_gzip_floor.py 读数推出,非来源原文 | 第 3 章 |
| 「已压缩载荷不必再压」属推论,本文脚本未覆盖 | 由第 1 条(固定开销结构)推出,非实测 | 第 5 章 |
写在最后:这篇用到的资料
写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:
- 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
- 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
- AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
- 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
- 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「AI」,优先通过。
资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2601_96189255/article/details/170402612










