关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

压缩到底是省带宽还是费CPU:gzip、brotli、zstd的级别该怎么定

发布时间:2026-09-24

出网流量降了 70%,机器却先撑不住了

先讲一件运维群里隔三差五就会冒出来的事。有人给一台跑 API 的机器开了 gzip,级别拉到 9,上线第二天兴冲冲地去看出网流量图——确实跌了七成,效果立竿见影。但同一个页面上,CPU 使用率从原来的三成爬到了接近满载,load 翻倍,P99 响应时间从 40 毫秒涨到 300 多毫秒,业务方开始报超时。最后他做的是把压缩级别从 9 降回 4,出网流量只比 9 级多了几个百分点,CPU 直接掉回去一半。

这个反差里藏着一件很多人没算过账的事:压缩不产生价值,它只是把一种资源换成另一种资源。省下来的是出网字节,花掉的是 CPU 时间和首字节延迟。既然是交换,那它就一定存在"划算"和"亏本"两种情况,而不是"开了就好"。

所以本文要立的判断很明确——问题从来不是"要不要开压缩",而是"对什么内容开、开到几级、在哪一层开"。这笔交换划不划算,取决于两件事:内容本身的可压缩性,以及这条链路上真正稀缺的是带宽还是 CPU。判断标准也很直接:出网带宽是瓶颈的时候(海外节点、按流量计费、移动端用户),压缩稳赚;CPU 已经是瓶颈的时候(高 QPS 的 API、已经跑在七八成 CPU 的机器),再往上拉压缩级别就是自杀。

  • 压缩是一种资源兑换,不是性能优化。它把带宽账单换成 CPU 账单,两边都是成本。看到出网流量下降就觉得赚了,是只看了账本的一面。
  • 级别不是越高越好。从 1 级往上,体积收益是递减的,CPU 开销是递增的,某些算法还是超线性递增。绝大多数场景的最优解在中低档:gzip 4–6 级、brotli 4–5 级。
  • 已经压缩过的格式再压一遍,等于纯烧 CPU。JPEG、PNG、WebP、MP4、woff2、zip、gz,这些占了多数站点出网流量的大头,压它们体积几乎不掉。这条最容易被忽略,也最容易白烧钱。
  • 静态资源和动态响应要分开对待。静态资源在构建期预压缩、上最高等级;动态响应在运行时压、必须限制级别并设最小阈值。混为一谈是绝大多数配置问题的根源。
  • 重复压缩是纯浪费。CDN 边缘压过了,源站就别再压;应用框架压过了,Nginx 就别再压。多层都开,CPU 花两遍,延迟叠两次。
  • 有一条不能碰的红线。响应里同时包含敏感信息和攻击者可控输入时,不要开压缩。这不是性能问题,是安全问题。

压缩换的是什么:省下出网字节,花掉 CPU 时间和首字节延迟

要把这笔账算清楚,得先把"换"的两端摆明白。压缩带来的变化体现在三个量上,两个是付出,一个是收益。

收益端:出网字节数

收益很好算,就是响应体压缩前后之差乘以请求数。不同内容类型的压缩比差异极大,下面是行业里常见的参考区间,仅供建立量级感,你自己的数据必须实测

  • HTML:标签重复度极高,压缩比通常很可观,压到原体积的两成上下是常见量级。
  • CSS / JS(已经过 minify 的):minify 去掉了空白和注释,剩下的字符分布仍然高度偏斜,通常还能再压掉六七成。
  • JSON / XML API 响应:字段名大量重复时收益很好;但如果响应体里塞满了随机 ID、一次性 token、Base64 片段,收益会明显下降。所以"API 压了没效果"这种抱怨,往往不是压缩没生效,而是内容本身就接近随机。
  • 纯文本日志、内部 RPC 报文:重复度高,压缩比通常很好。
  • 图片、视频、字体、归档包:接近 0,理由下一节详细讲。

这里有个很容易被算漏的点:收益是"每次传输"都在省,而压缩的 CPU 成本在静态预压缩场景下只付一次。一个 200KB 的 JS 文件,预压一次花掉几十毫秒 CPU,之后几万次下载都不再花 CPU,净赚。而一个每次请求都要重新生成的动态响应,压缩成本是每一次请求都要付一遍的。这两件事的账本结构完全不同,所以处理方式也必然不同——后面讲静态预压与动态压缩那一节会展开。

付出端之一:CPU 时间

CPU 这一侧的口径要稍微绕一点。常见的定性算法是:压缩一段数据花掉的 CPU 时间 ≈ 原始字节数 ÷ 该算法在该级别下的压缩吞吐速度。压缩吞吐速度通常用 MB/s 表示,随算法、级别、数据内容、CPU 型号变化很大,从几十 MB/s 到几百 MB/s 都有,这个数字只能实测,不要套用别人的跑分

有了这个口径,就能把"每 GB 出网流量烧多少 CPU"估出来。举个假设性的算法演示(以下为典型部署思路的算账示例,并非特指某一真实客户,数值均为假设):某站点每天出网 500GB 原始文本,压缩后降到 150GB,省下 350GB 传输;压缩这 500GB 原始数据的 CPU 成本,按单核 100MB/s 的压缩吞吐粗估,大约需要单核跑 5000 秒,摊到一天 86400 秒里大约是单核 6% 的占用。看起来不多,很划算。

但如果这 500GB 是由 2000 QPS 的小响应堆出来的(平均每个响应只有几 KB),情况就变了:小响应上压缩的单位成本更高——每个响应都要付出算法初始化、字典准备、块头写入这些固定开销,而这些开销摊在几 KB 的数据上就非常显眼。加上高 QPS 本身就把 CPU 吃得七七八八,这时候压缩就是压垮骆驼的那根稻草。这也是为什么"大文件压缩很划算,小响应压缩要谨慎"会成为一条经验。

付出端之二:TTFB 与首字节延迟

第三个量最容易被忽略:压缩会影响首字节时间。动态压缩不是"拿到完整响应再压一遍"这么简单,它要在响应生成过程中边生成边压,压缩器内部有缓冲区,数据要攒够一定量才会吐出一段输出。这带来两个后果:一是首字节要等第一个压缩块产出;二是如果响应体小于缓冲区阈值,可能根本压不出来任何东西,白等一场。

压缩和分块传输(chunked)配合时,服务端的行为、缓冲区刷新的时机,都会影响客户端拿到第一个字节的时间。对首屏敏感的页面来说,压缩省下的传输时间和它加上的编码时间,两者并不总是前者赢——尤其是文本只有几 KB、而客户端和服务器之间延迟很低(比如同机房、同地域)的情况下,省下的那点传输时间可能还不如压缩花掉的时间多。

还有一个连带影响在缓存层:压缩会改变响应体的字节长度,因此会影响 ETag、Content-Length,也会影响 CDN 和共享缓存的缓存键。这些地方处理不当,会出现缓存命中率下降或者缓存串味的问题,Vary 那一节会讲到。

这笔交换什么时候稳赚,什么时候是自杀

既然是交换,判断标准就只有一个:这条链路上真正稀缺的是带宽还是 CPU。稀缺的那一项,压缩才有价值;已经富余的那一项,压了也是白压。

带宽稀缺时:压缩几乎稳赚

  • 按流量计费的计费模式。出网字节直接换算成钱,压缩比几乎等于折扣率,这时的账最好算。
  • 海外或跨地域节点。跨地域链路的带宽单价通常高于同地域,而且延迟高、BDP 大,传输时间本身就更长,压缩省下的时间也更明显。
  • 移动端用户占比高。移动网络下多传 100KB 和多传 30KB,在弱网环境里的体感差距比宽带环境大得多,而且用户的流量是真金白银。
  • 回源链路紧张。CDN 回源走的是源站的出网带宽,源站压一份或者 CDN 压一份,都能显著降低回源压力。
  • 峰值计费模式。按 95 计费或者按峰值计费的带宽,削峰本身就是价值,压缩把峰值压下去比把均值压下去更值钱。

CPU 稀缺时:压缩是自杀

  • CPU 已经跑到七八成。这时候任何额外的 CPU 开销都会直接反映到排队延迟上,指数级恶化响应时间。先把 CPU 降下来,再谈压缩。
  • 高 QPS 的小型 API 响应。每个响应几 KB,但每秒几千次请求,压缩的固定开销乘以请求数之后非常可观。
  • 响应体本身接近随机。加密内容、随机 token、已压缩的二进制,压缩比接近 1,CPU 全白烧。
  • 同机房、低延迟的内网调用。链路延迟本来就在亚毫秒级,压缩省下的传输时间几乎为零,花掉的编码时间却是实打实的。

还有一种容易被误判的情况:带宽包年是固定额度且长期用不满。这时候压缩的收益只是"更不容易顶到峰值",并不产生实际的成本节省,而 CPU 开销是实打实的。这种情况下,压缩的级别就应该往低调,甚至对动态内容直接不开。

反过来,如果机器上 CPU 大量空闲、带宽却吃紧,那把压缩级别往上调一档就是白捡的钱。判断的动作其实很简单:看监控上 CPU 和出网带宽这两条曲线,谁先接近天花板,就为谁做优化。

gzip、brotli、zstd 的性格差异,和它们各自该待的位置

三种算法不是"新的一定比旧的好"的关系,它们的性格差别很大,适合待的位置也不一样。挑错位置,比挑错级别更浪费。

gzip:兼容性最好,负责兜底

gzip(底层是 DEFLATE)的最大优势不是压缩比,而是任何客户端都支持。从二十年前的浏览器到现在的嵌入式设备、爬虫、命令行工具,Accept-Encoding 里带 gzip 是常态。这决定了它的位置:兜底。当客户端不支持 brotli 时,退回 gzip;当 CDN 或者中间设备对 brotli 支持不确定时,用 gzip。

级别范围通常是 1 到 9。中等级别(4–6)的性价比最高:1 级太快但压缩比损失明显,9 级的 CPU 开销涨得厉害而体积收益很小。默认级别通常在 1 或 6 附近(不同实现默认值不同,需确认你自己用的版本),生产环境我一般建议显式写出来,别依赖默认值。

brotli:静态字典大,吃的是文本,高等级只能预压

brotli 相对 gzip 的核心差异是内置了一个体积很大的静态字典,里面预置了大量 Web 领域常见的文本片段(标签名、常见 CSS 属性与关键字、常见 JS 惯用语等)。这意味着两件事:

  • 对 HTML/CSS/JS 这类文本,brotli 的压缩比通常优于 gzip,尤其在小文件上优势更明显——因为小文件里"字典里本来就有的片段"占比更高,几乎不用编码就能引用。
  • 对非文本内容(图片、视频、已压缩数据),brotli 相对 gzip 基本没有优势,因为字典根本派不上用场。压这些东西用哪个算法都一样亏。

brotli 的级别范围是 0 到 11,这是它最容易踩坑的地方。高等级(9–11)使用的是非常大的滑动窗口和极其昂贵的匹配搜索,CPU 和内存开销会急剧上升——11 级压缩单个大文件时占用几百 MB 内存是常见的量级描述(需实测确认)。这个开销对于"构建期跑一次"完全可接受,对于"每个请求跑一次"就是灾难。

所以 brotli 的位置很清晰:静态资源用最高等级预压缩,动态响应用 4–5 级。把 brotli 11 级用在动态内容上,是我在生产环境里见过的最典型的压缩误用之一。

zstd:同压缩比下更快,解压极快,主要待在不直接面对浏览器的地方

zstd 的设计目标是在相近的压缩比下提供明显更快的压缩速度,以及非常快的解压速度。它的级别范围很宽(通常 1 到 19,更高还有带额外内存开销的超高档位),低档位速度快,高档位压缩比好,选择空间比 gzip 大得多。它还支持用业务样本训练专用字典,对结构固定的短报文(比如固定 schema 的 JSON、日志行)效果很好。

解压快这件事在什么地方值钱?在解压次数远多于压缩次数的场景:一份日志压一次,之后被查询和分析解压很多次;一份数据在对象存储里压一次,被读取很多次。这类场景里解压成本占比很高,zstd 的优势就会非常明显。

但 zstd 有个硬约束:浏览器生态对它的支持度远不如 gzip 和 brotli。客户端在 Accept-Encoding 里声明支持 zstd 的比例、以及各浏览器与各版本的支持情况都在变化,需要你自己去确认目标用户群的实际情况。因此 zstd 的现实位置主要是:

  • 内部服务之间的 RPC 与消息队列载荷压缩;
  • 日志、快照、备份的落盘压缩;
  • API 网关到内部服务这一段链路(网关对外仍用 gzip/brotli,对内可以换成 zstd);
  • 数据库、对象存储、大数据组件的列存/块压缩。

说白了就是一句:面向浏览器用 gzip 兜底 + brotli 提效,不面对浏览器的链路上 zstd 更划算。

内容协商与 Vary:最常见的坑在这里

算法是通过内容协商选出来的:客户端在请求头 Accept-Encoding 里声明自己支持哪些编码(比如 gzip, deflate, br),服务端挑一个返回,并在响应头里用 Content-Encoding 标明实际用了哪个。

坑在于缓存。同一个 URL 现在有了多种不同的字节表示——不压的版本、gzip 的版本、brotli 的版本。如果服务端没有在响应里带上 Vary: Accept-Encoding,中间的 CDN 和共享缓存就不知道这个响应是依赖于请求头的,它会拿一个 URL 对应的缓存条目去服务所有请求。

后果有两种,一种轻一种重:

  • 轻的:把 gzip 版本发给了其实支持 brotli 的客户端。功能正常,只是白白浪费了压缩比。
  • 重的:把 brotli 版本发给了不支持 brotli 的客户端。客户端解不开,直接乱码或者报错。这种故障的表现非常迷惑——同一个 URL,某些客户端正常、某些客户端炸,而且复现率跟客户端类型强相关。排查的时候如果没想到缓存层,会怀疑半天应用层。

所以 Vary: Accept-Encoding 必须设,而且要在所有可能被缓存的压缩响应上都设。同时要注意,带上了 Vary 之后缓存键会变多,同一个资源会在缓存里存多份,这是必须接受的成本。还有一个连带检查项:ETag。有些实现会在压缩响应上把 ETag 标记为弱校验或者改写后缀,以区分不同编码版本,具体行为取决于你用的服务端版本,需要实测确认,别想当然。

哪些内容压了等于白压,甚至比不压更糟

这一节是全文最应该记住的部分。前面算的所有账,都建立在"内容可压缩"这个前提上。而现实中很多站点的出网流量大头,恰恰是不可压缩的内容。

已经压缩过的格式:再压一遍是纯烧 CPU

JPEG、PNG、WebP、AVIF、MP4/H.264/H.265、ZIP、GZ、7z、以及 PDF 内部已经压缩过的流,这些格式的共同点是:它们在自己的编码过程中已经做过熵编码,输出的字节分布已经接近随机。而任何通用压缩算法(DEFLATE、brotli、zstd)的收益来源都是"找到并消除统计冗余",面对已经接近随机的数据,它们找不到冗余,输出体积几乎不掉,但整个编码流程的 CPU 是一点没少花。

折算下来就是:体积收益 0%–2%(行业参考,需实测),CPU 成本 100%。这不是低效,这是纯亏。而且这类内容往往是大文件,压一个大文件花掉的 CPU 时间比压一百个小文本还多。

几个特别容易漏掉的:

  • 字体 woff2:woff2 格式内部就是用 brotli 压过的,外层再套一层 brotli 或 gzip 基本无效。早期的 woff(不带 2)是可以压的,别搞混。
  • PDF:PDF 里的文本流和图像流通常已经分别压缩过,整体再压收益极低,除非是那种未压缩生成的特殊情况。
  • Base64 内联的图片:这个有意思——Base64 编码本身把 3 字节变成 4 字节,膨胀了约三分之一,而膨胀后的文本是可压缩的,所以压它往往能省回一部分。但这属于"先犯了个错再补救",正解是别用 Base64 内联图片,直接用二进制图片资源。

小响应:可能越压越大

压缩格式本身是有头的。gzip 有固定长度的文件头、deflate 块头、末尾还有校验和与长度字段,加起来是几十字节的固定开销;brotli、zstd 也各有自己的头部开销。同时还有算法初始化、字典准备这类固定成本。

当响应体只有几百字节时,这些固定开销很容易超过压缩省下来的字节。极端情况下压缩后的响应比原文还大。即便没有变大,赚到的那十几个字节也完全抵不过 CPU 启动成本。所以几乎所有服务端实现都提供了最小压缩阈值这个配置项(常见的默认值在几百字节到一千多字节之间,各实现不同),务必设上。

加密内容与实时流

加密内容的输出在设计上就必须接近随机(否则会泄露明文信息),因此不可压缩。端到端加密的报文、密文落盘的数据,开压缩没有任何意义。

流式与实时音视频则要反过来考虑延迟:压缩器需要缓冲才能产出输出块,这个缓冲会直接叠加到端到端延迟上。对直播、实时通话这类场景,为了省一点带宽而引入几十到几百毫秒的缓冲,是完全不可接受的取舍。

一张表对照清楚

内容类型 典型可压缩性(行业参考,需实测) 值不值得压 推荐算法与级别 说明
HTML / CSS / JS(已 minify) 高,通常可压到原体积的两成上下 非常值得 静态:brotli 9–11 预压缩;动态:brotli 4–5 或 gzip 4–6 标签与关键字重复度高,大字典收益明显;静态走构建期预压缩,运行时零开销
JSON / XML API 响应 中高,通常两成到三成半,视字段重复度而定 值得,但要先算 CPU 动态 gzip 4–6;内部链路可 zstd 3–5 字段名重复多则收益好;塞满随机 ID 与一次性 token 时收益明显下降,高 QPS 场景需实测
图片(JPEG / PNG / WebP / AVIF) 极低,接近 0%–2% 不值得 不压缩,在配置里按类型排除 格式内部已做熵编码,再压体积不掉、CPU 照烧;这是被忽略最多的浪费源
视频(MP4 / H.264 / H.265 / 切片) 基本为 0 不值得 不压缩;实时流尤其不能压 码流本身已压缩;对流式内容开压缩还会引入缓冲与额外延迟
字体 woff2 几乎为 0 不值得 不压缩(早期 woff 可压) woff2 内部已用 brotli 压过,外层再套一层基本无效
已压缩归档(zip / gz / 7z)与 PDF 内已压缩流 接近 0,个别情况略微变大 不值得 不压缩 二次压缩没有冗余可消;下载站、软件分发类业务要重点排除这一类
小于 1KB 的小响应 不确定,存在变大的可能 通常不值得 设最小压缩阈值(常见 1KB 上下,需实测定档) 头开销与算法初始化成本可能超过省下的字节;高 QPS 下这一点会被放大
含敏感信息且含攻击者可控输入的响应 不论压缩比,安全优先 不该压 对该路径单独关闭压缩 BREACH 类压缩侧信道可通过响应长度推断敏感内容,属硬性红线
纯文本日志 / 内部 RPC 体 / 落盘数据 值得 zstd 3–8,可用业务样本训练字典 不直接面向浏览器,解压极快是核心优势;落盘场景压一次解压多次,收益清晰

静态预压高等级、动态压低等级:这个组合为什么才是正解

前面反复提到的"静态"和"动态",其实是两种完全不同的成本结构,处理方式必须分开。

静态预压缩:把 CPU 成本挪到构建期

静态资源(JS、CSS、HTML 模板产物、SVG、大型 JSON 配置)在构建阶段就能确定内容。那就在构建流程里直接生成压缩产物——比如同时产出 app.jsapp.js.gzapp.js.br——服务端运行时直接把预压好的文件发出去,CPU 开销为零。主流 Web 服务器都支持这种行为(Nginx 的 gzip_static 与 brotli 静态模块这类机制),命中预压文件时就跳过运行时压缩。

这样做最大的好处是:既然 CPU 只在构建期花一次,那就可以毫无顾忌地用最高等级。brotli 11 级压一个大 JS 文件可能要几秒甚至更久、占几百 MB 内存,但这是一次性的、离线的、可以慢慢跑的成本,换来的却是这个资源每一次下载都能少传几个百分点的字节。这笔账在静态资源上永远划算。

代价也很清楚,有三条:

  • 构建时间变长。高等级压缩会让构建流水线明显变慢,需要在流水线里预留时间,或者做增量构建只压变化的文件。
  • 存储多份。同一个资源要存原文件加两种压缩产物,存储占用翻倍不止,需要做清理与版本管理。
  • 一致性风险。这是个真事故源:源文件改了但压缩产物没重新生成,线上就会一直发旧内容,而且表现非常诡异——直接访问源文件是新的,走压缩响应的客户端拿到旧的。所以预压产物必须进构建流程并强制校验,不能靠人工手动跑一次。

动态压缩:必须限制级别,必须设阈值

API 响应、SSR 页面、个性化内容,这些每次请求的内容都不一样,只能在运行时压。这时候纪律只有两条:级别压住,阈值设上。

级别为什么必须压住,下一节讲。阈值为什么必须设上,前面已经讲过——小响应的收益抵不过固定开销。两个配在一起,动态压缩的成本才是可控的。

还有一条容易忽略:动态压缩要按内容类型做排除。不要写一个"所有响应都压缩"的规则,而是要写成"对文本类响应压缩、对图片视频字体归档类排除"。很多默认配置的压缩类型列表里已经带了 text/html 一类,但有些实现会默认包含 * 或者把 image/* 也带上,这种默认值要自己核一遍。

级别不是越高越好:收益递减,开销递增

这是本文第二个必须立的判断。压缩级别从 1 级往上走,体积收益是递减的,而 CPU 开销是递增的,有的算法还是超线性递增。

具体量级(行业参考,不同内容与实现差异极大,必须实测):

  • gzip 从 1 级到 9 级:体积大概还能再降几个百分点到十几个百分点,而压缩耗时可能涨到两三倍甚至更多。前几级的性价比极高,后几级明显不划算。
  • brotli 从 4 级到 11 级:体积收益通常也只是几个百分点到十几个百分点,但耗时可能涨到十倍甚至几十倍,内存占用也大幅上升。这就是"11 级只能用于预压缩"的原因,不是保守,是算术。
  • zstd 的级别跨度更宽:低档位(1–3)非常快,中档位(3–8)在速度和压缩比之间比较平衡,再往上压缩比的边际收益同样快速衰减。

所以定档的建议是直接给的:动态内容 gzip 4–6 级、brotli 4–5 级、zstd 3–5 级;静态预压缩 brotli 9–11 级、gzip 9 级。把级别从 4 拉到 9,多花的那部分 CPU 换不回等价的流量——这才是"级别拉满"真正的代价。

要强调的是,这些档位是建立在"内容是可压缩文本"这个前提上的。如果内容本身压不动,级别调到几都是白搭,先把内容类型筛对,再谈级别。

怎么从监控上判断压缩已经吃掉 CPU

判断压缩是不是已经变成瓶颈,不需要什么特殊工具,看几个现有指标的对应关系就够了。

三个现象同时出现,基本就是它

  • CPU 使用率随 QPS 线性上升,但出网带宽没跟着涨。这是最有指向性的一条。正常情况下流量涨了带宽也该涨,如果带宽被压着(因为压缩掉了)而 CPU 一路爬升,说明多出来的 CPU 大部分花在编码上,而不是在服务更多请求。
  • TTFB 与 P99 变长,而上游响应时间没变。在反向代理层做压缩时,上游返回的时间是固定的,多出来的那部分时间就是压缩花掉的。对比请求总耗时与上游耗时的差值变化,能直接把压缩的成本单独剥出来。
  • load 升高、上下文切换变多。CPU 被吃满之后请求开始排队,load 会先于响应时间反映出来。

怎么拿到"每 GB 出网流量烧多少 CPU"这个数

给一个可以直接落地的定性口径,所有数字都要用你自己的环境实测

  • 第一步,取两个监控量:一段时间窗口内的出网字节数(或原始响应体字节数,压前的更准)与CPU 使用秒数
  • 第二步,做一次开关对比:在完全相同的流量模型下,分别记录开启压缩与关闭压缩(或改不同级别)时的这两个量,算差值。
  • 第三步,把差值相除,得到"每 GB 出网流量对应的 CPU 秒数"。这个数本身就是决策依据——它可以和你的带宽单价直接换算成钱,也可以和 CPU 余量直接对照。

行业参考的量级感是:中等级别压缩的吞吐通常在单核几十 MB/s 到一两百 MB/s 之间(需实测确认,不同算法、级别、数据内容、CPU 型号差异巨大),折算下来每 GB 原始数据的压缩时间在单核几秒到十几秒这个量级。这个数看着不大,但乘以每天几百 GB 的出网量、再叠加高 QPS 下的小响应固定开销,就完全可能吃掉一整台机器的余量。

确认到底是不是压缩吃的

上面的现象只是嫌疑,确认还要再走一步。最省事的办法是采样:在压测或者高峰时段抓一份 CPU 采样的火焰图或 top 函数排名,看排在前面的函数里有没有压缩库的编码函数(zlib 的 deflate 系列、brotli 的编码入口、zstd 的压缩入口)。有,就坐实了。没有,就说明 CPU 是别的地方吃的,别调优错了方向。

还有一条:看压缩吃的是哪个核。Web 服务器通常是多 worker 进程模型,压缩发生在处理请求的那个 worker 所在的核上。核数少、worker 少的时候,压缩会把某几个核打满而其他核闲着,看起来整机 CPU 不高但实际已经在排队。这类分布问题跟软中断集中在单核上是同一个道理,判断时要按核看,不要只看整机平均值。

不能碰的红线:压缩侧信道,以及到底该在哪一层开

BREACH 这类攻击的基本思路

压缩会泄露信息,这不是理论问题。BREACH、CRIME 这一类攻击利用的都是同一件事:压缩后的长度依赖于内容,而内容中的重复会缩短长度。

推理过程是这样的。假设一个响应里既包含用户的 CSRF token(秘密),又包含攻击者能控制的输入(比如 URL 参数回显到页面上)。攻击者在自己的输入里放一段猜测字符串,如果这段猜测恰好和秘密的某个部分相同,那么响应里就出现了重复内容,压缩器会把它消掉,响应长度就会变短。攻击者观察响应长度,逐字节试,就能把秘密一个字节一个字节地试出来。

它成立需要几个条件同时满足:响应里包含攻击者想拿到的秘密;响应里同时包含攻击者可控的输入;秘密在响应中重复出现(否则压缩无从谈起);攻击者能反复发起请求并观测到压缩后的响应长度。这四个条件凑齐的场景比你想象的要多——带 token 的表单页、带回显的搜索结果页、把用户标识渲染进 HTML 的页面,都在这个范围内。

所以结论是硬性的:对同时包含敏感信息与攻击者可控输入的响应,不要开压缩。如果业务上必须压,就要做针对性缓解,比如让 token 每次请求都随机化(这样秘密不再以固定形式重复出现)、或者把秘密移出会被回显的上下文、或者对这类路径单独关闭压缩并配合其他防护手段。至于给响应加随机填充来掩盖长度这种做法,会破坏长度语义、影响面广,是否采用要具体评估,别当成默认方案。

顺带说一句:TLS 层压缩(CRIME 的直接目标)在现代实现中基本已经默认关闭,但 HTTP 层的响应压缩是另一回事,它依然可能成为侧信道,两者不要混为一谈。

在哪一层开:只开一处,别重复

压缩可以开在三个位置,各有各的适用面:

  • CDN 边缘。边缘节点压一次,之后所有命中缓存的请求都不再需要压,这是最省的一种方式。而且边缘离用户近,压缩后的传输段最长,收益最大。
  • 应用层反向代理(Nginx / Apache / Envoy 这类)。最常见的位置,可控性最好,能按路径、按类型精细配置,也方便做预压缩。动态内容基本都在这里压。
  • 应用框架中间件。在业务进程里压,好处是能拿到业务语义(比如知道这个响应含不含敏感信息),坏处是占用业务进程的 CPU,而且和反向代理层容易重复。

原则是:一条链路上只压一次。重复压缩的表现是 CPU 花两遍、延迟叠两次,而收益为零——因为第二个压缩器面对的是已经被压过的数据,压不动了。

最常见的重复场景是 CDN 加源站:CDN 已经压过了,源站就不需要再压。回源请求可以让 CDN 用 identity 回源(源站只出不压的版本,由边缘统一压),或者明确约定由谁负责。反过来,如果源站压了而 CDN 也开了压缩,CDN 通常不会因为已经压缩过就重复压,但配置不清晰时就容易出现两边都压或者两边都不压的情况,需要实测确认。

同样的道理适用于反向代理加应用框架:如果 Nginx 已经开了 gzip,应用里的压缩中间件就该关掉,或者反过来。选一层的标准是"哪一层更容易拿到业务语义、哪一层更容易做缓存",通常选反向代理层,敏感路径的例外单独在业务层关闭。

落地检查清单:八个开关各自该拧到哪

把前面的判断落到配置上,就是这八项。每一项都要有人确认过,不能靠默认值。

  • 一、哪些路径开。按 location / 路由级别列出启用压缩的路径清单,而不是全局一刀切。动态接口、静态资源目录分开写。
  • 二、哪些类型排除。明确排除图片、视频、字体(尤其 woff2)、归档包、PDF 这类已压缩格式。不要出现 image/* 或通配类型被包含进去的情况,默认值要逐条核。
  • 三、最小阈值设多少。设一个最小响应长度阈值,行业常见的默认值在一千字节上下(各实现不同),小响应占比高的业务可以适当上调,具体档位需实测。
  • 四、级别定几。动态内容 gzip 4–6 级、brotli 4–5 级、zstd 3–5 级;静态预压缩 brotli 9–11 级、gzip 9 级。显式写进配置,不要依赖默认值——不同版本默认值不一样,升级时可能被悄悄改掉。
  • 五、Vary 有没有设。所有可能被 CDN 或共享缓存接管的压缩响应,都必须带 Vary: Accept-Encoding。用 curl 带不同 Accept-Encoding 各请求一次,看响应头和缓存行为是否符合预期。
  • 六、敏感响应有没有单独处理。把"响应含敏感信息且含攻击者可控输入"的路径挑出来,单独关闭压缩或做 token 随机化。这一项要有明确的路径清单,不能靠印象。
  • 七、预压缩产物有没有进构建流程。静态资源的 .gz / .br 必须由构建流水线生成并随产物一起发布,要有源文件与产物一致性的校验,避免改了源文件忘了重压。
  • 八、链路上有没有重复。确认 CDN、反向代理、应用框架三处只有一处开着。用 curl 逐级测(直连源站、过 CDN)看 Content-Encoding 出现几次、响应体有多大。

改完之后做一轮验证:用同样的流量模型压一次,记录开关前后的 CPU、出网带宽、TTFB 与 P99 四个数。只有这四个数的变化都符合预期,这次调整才算做完。只看出网流量降了就收工,是开头那个故事重演的开始。

这些数字的边界在哪:为什么说压缩比必须实测

整篇文章里出现的所有压缩比、吞吐、耗时倍数,都是"行业参考 / 通常会出现"的口径,不是任何实测数据,也不构成任何性能承诺。原因不是谦虚,是这些数字的离散度实在太大:

  • 内容差异主导一切。同样是 JSON,字段规整的列表和塞满随机字符串的响应,压缩比可以差出好几倍。同样是 HTML,模板化程度不同结果也不同。
  • 实现与版本差异。不同压缩库的版本、不同服务端的模块版本,在同一级别下的表现并不一致,字典、窗口大小、默认参数都可能不同。
  • 硬件差异。CPU 的指令集、主频、缓存大小、是否开启相关加速,都会显著影响压缩吞吐。同一份配置在不同机型上的 CPU 成本完全不可比。
  • 客户端构成差异。支持 brotli 的客户端占比、移动端占比、跨地域用户占比,直接决定压缩的实际收益。

所以正确的做法是:拿你自己的真实响应样本,在你的目标机型上,用你准备上线的级别,跑一遍压缩吞吐与压缩比,再拿这两个数去算账。样本要覆盖你业务里的主要响应类型,挑几个有代表性的路径分别测,别用一个平均值代表全部。

承载这些 Web 与 API 的服务器环境本身也在这个账里。压缩吃的是 CPU 核,核数与主频直接决定你能不能扛住压;出网带宽的计费方式(固定带宽还是按流量)决定压缩的收益能不能换成钱。这两件事在选机器形态时就要想清楚——需要自己决定核数与压缩策略、不想受虚拟化层干扰的,我一般会建议直接看裸金属形态;弹性需求强、流量有波动的,云主机形态更灵活。一万网络深耕 IDC 19 年(成立于 2007 年),这类承载 Web 与 API 的服务器环境从一万云 ¥25 元/月起,到华南 ¥799 元/月起、华东 ¥699 元/月起、华北 ¥899 元/月起的大陆节点,再到裸金属 E5-2620 / 32G / 1T ¥999 元/月起、双路 E5-2698v4×2 / 32G / 1T ¥3999 元/月的档位都有覆盖(以上起价档通常对应最低配置与特定付款方式,实际成交价以下单时核算为准,以官网实时价为准)。核数够不够扛住你要开的压缩级别,是选型时可以直接拿来问的一个具体问题。

调压缩之前,这几个问题最常被问到

开了压缩,首屏会不会反而更慢

有可能,取决于内容大小和客户端链路。压缩省的是传输时间,花的是编码时间加首块缓冲时间。当响应体很小(几 KB)而客户端与服务器之间延迟很低(同地域、同机房)时,省下的传输时间可能是几毫秒,而编码加缓冲的时间与之相当甚至更多,净结果就是变慢。反过来,跨地域链路、移动端弱网、大体积文本,压缩省下的传输时间远大于编码时间,就是明显变快。判断方法很直接:同一条路径开关压缩各测一次,比较 TTFB 与完整下载完成时间的 P50 和 P99。首屏的关键资源记得走静态预压缩,运行时零开销,就不存在这个权衡了。

CDN 已经压过了,源站还要不要压

通常不需要,而且不应该。CDN 边缘压一次之后,缓存命中的请求都不再产生压缩 CPU,这是最省的结构;源站再压一遍,CPU 花两遍而收益为零,因为第二遍面对的数据已经压不动了。合理的分工是:源站只出不压的版本(回源用 identity),由边缘统一压;或者明确约定由源站压、边缘不重复处理。关键是要实测确认——直连源站和过 CDN 各用 curl 测一次,看 Content-Encoding 出现几次、响应体字节数是多少,别靠配置文件的字面意思下结论。就算 CDN 压了,源站的静态预压缩产物仍然有价值:回源时把预压好的版本给 CDN,能省回源带宽。

brotli 11 级到底能不能用在动态响应上

不能,这是明确的。11 级用的是极大的滑动窗口和极其昂贵的匹配搜索,单文件的内存占用可以到几百 MB 量级,压缩耗时相对中等等级可能涨到十倍甚至几十倍(行业参考,需实测)。这个成本放在构建期跑一次完全没问题,放在每个请求跑一次就是灾难——请求一多,内存和 CPU 同时告急,响应时间直接崩。动态响应用 brotli 就老老实实用 4–5 级,想要更高压缩比就走静态预压缩这条路,别在运行时硬扛。顺带提醒:有些实现的 brotli 默认级别就偏高,上线前务必把级别显式写出来。

为什么开了压缩,出网流量一点没降

先按可能性从高到低排查。第一,你的出网流量大头本来就是不可压缩的内容——图片、视频、字体、下载包。很多站点这两类内容占出网流量的八成以上,把文本全压掉对总量影响也有限,这种情况下先去优化图片格式和视频码率,收益比调压缩大得多。第二,压缩没真正生效:Content-Encoding 响应头有没有出现?用 curl -H 'Accept-Encoding: gzip' -I 看一眼,没有就是没生效。第三,内容本身接近随机:响应体里塞满随机 ID、token、Base64 片段,压不动。第四,小响应占比太高,被最小阈值挡掉了大部分请求。这四个查完,原因基本就出来了。

Vary 没设会出什么事,具体表现是什么

表现是缓存串味,而且症状很迷惑。轻的一种:支持 brotli 的客户端拿到了 gzip 版本的缓存,功能正常,只是白白浪费了压缩比,看不出来。重的一种:不支持 brotli 的客户端拿到了 brotli 版本的缓存,解不开,直接乱码或报错。症状是同一个 URL 在某些客户端正常、某些客户端炸,而且跟浏览器版本强相关。排查时如果你只盯应用层,会怀疑很久——因为应用层的逻辑完全没问题,问题出在中间缓存把"依赖于请求头的不同响应"当成了同一个响应。所以规则是死的:只要响应内容随 Accept-Encoding 变化,就必须带 Vary: Accept-Encoding。带上了之后缓存条目会变多,这是必须接受的代价。

敏感接口到底要不要单独关压缩

判定条件是四个同时成立:响应里含攻击者想拿的秘密(token、用户标识、会话信息一类);响应里同时含攻击者可控输入(URL 参数回显、搜索词回显、用户名一类的反射内容);这个秘密在响应中重复出现;攻击者能反复请求并观测到压缩后的长度。四个都满足,就必须关——或者用每次请求随机化 token 的方式让秘密不再以固定形式重复。不要觉得"我的 token 藏在很深的地方就没事",压缩器看到的是字节流,它不管语义。落地方式是按路径列清单单独处理,而不是全局关压缩——全局关了,你为了一小部分路径牺牲了整站的带宽账。

怎么知道压缩到底吃了多少 CPU

三个办法,从粗到细。最粗的是开关对比:同样的流量模型,开与关各跑一段,直接比 CPU 使用率的差值,再除以这段时间的出网字节数,就得到"每 GB 出网流量的 CPU 成本"这个可以直接拿来做决策的数。中等精度的是看耗时拆分:在反向代理层比较请求总耗时与上游耗时的差值,压缩就发生在这一段里,差值随级别变化的情况就是压缩的成本曲线。最细的是采样:抓一份 CPU 采样数据,看压缩库的编码函数在排名里占多少百分比,同时按核看分布,确认压缩是不是把某几个核打满了。三个办法里第一个最实用,因为它直接给出能拿去和带宽单价对比的量。

小响应要不要设阈值,设多少

要设,这是几乎必开的一项。理由是压缩格式有固定头部开销(gzip 的头、块头、尾部校验与长度字段加起来几十字节),还有算法初始化与字典准备的固定成本,这些成本摊在几百字节的数据上非常显眼,极端情况下压缩后反而更大。行业常见的默认值在一千字节上下(不同实现默认值不同),可以以此为起点:小响应占比高、QPS 高的业务往上调;大响应为主的静态资源站可以维持默认或下调。具体档位要拿你自己的响应体大小分布来定——先把响应大小的 P50、P90 统计出来,再决定阈值卡在哪,比拍脑袋强得多。

这些数字我是对着什么写的

文中出现的所有压缩比区间、压缩吞吐与耗时倍数的量级描述,均为行业公开经验的参考口径,不构成任何实测数据、跑分结果或性能承诺。三种算法(gzip/DEFLATE、brotli、zstd)的级别范围、典型性格与适用位置,属于公开技术文档的通用描述,具体行为随压缩库版本、服务端模块版本与实际配置而异;BREACH/CRIME 一类压缩侧信道的描述为公开的攻击原理性介绍,实际风险取决于你自己的响应结构与防护配置,请以实际安全评估结论为准。所有数值请以你自己的真实响应样本在目标机型上实测的结果为准。

三种算法的公开资料来自各自官方文档与 IETF RFC 相关条目,压缩比与吞吐区间属行业公开经验值,不指向任何一次具体测量。服务器与云主机的档位描述(一万云 ¥25 元/月起、裸金属 E5-2620 / 32G / 1T ¥999 元/月起等)来自一万网络官网 https://www.idc10000.net/ 的产品页面,未做任何外推。具体以签约时最新报价与合同为准,起价档通常对应最低配置与特定付款方式,实际成交价请以下单时核算结果为准。文中出现的部署思路与算账示例均为典型场景假设,并非特指某一真实客户或真实案例


上一篇:危地马拉云整页都写"元起/月":拿到起价之后还要问清哪几件事?

下一篇:迁移不是搬完就完:双写期的订单数据该用什么方法做一致性校验