一条响应在字节、往返与处理器占用三个刻度上的反向走势,中间被几层缓存与代理隔开

一、三笔账方向不一致,那就先各给一个可测的量

把压缩当成“省流量的开关”,是这类决策最常见的错误起点。它同时改动三个量,而且这三个量并不朝同一个方向走。要判断开不开,先把三笔账各变成一个能测的东西。

字节账测的是同一批响应在开启前后的传输字节差,而且必须按接口分组,不能只看出口总带宽。总带宽里混着访问量结构,一个被砍掉一半体积的高频小接口,和一个几乎压不动的大文件下载,在总数上会被抹平成同一条曲线。

往返账测的是连接与请求次数的变化,具体落到三个读数:新建连接的次数、握手开销落在哪些请求上、以及每个响应实际占用的报文段个数。这一笔容易被忽略,因为压缩看起来只改体积;但体积一旦少跨过一个报文段的边界,链路侧的发送轮次就真的变少了。

CPU 账测的是服务端每单位请求多花的处理器时间,以及它转化为响应延迟(尤其是尾延迟)的幅度。这一笔方向永远是恶化的,只是幅度随级别和负载形状变化。

三笔账里,字节与往返常常一起改善,CPU 与延迟一起恶化,看上去像是“用算力换带宽”的干净交易。麻烦在于往返这一笔在高并发短连接下会反向:压缩需要缓冲,缓冲会把首字节时间推后,连接被占用得更久,连接池的有效容量变小,新建连接反而增多。这时三笔账里有两笔在变差,汇报时如果只说“体积小了”,结论就会被单口径误导。这也是我们要求把三笔账分开记账、分开签字的原因。

二、字节账的断点:以单个报文段为界

收益要成立,省下的字节必须真的减少传输轮次。把最大报文段按约 1400 字节这个口径举例来看:一个不足一个报文段的响应,一次往返就能带完,压缩后省下那点字节不会减少报文段个数,也不会减少往返次数,但压缩的固定开销照付。小于这个量级,基本等于白做。

必须说清一点:这个断点的物理含义不是某个“最优字节数”,而是“报文段个数是否因为压缩而减少”。1400 只是用来讲清口径的举例,本篇不把它写成通用阈值,也不提供任何“压缩一定省多少”的外推。

响应体在阈值附近抖动时,要有明确的判定策略,而不是交给实现临时决定。可执行的做法是按长度阈值跳过压缩:长度已知且落在阈值之下的,直接以未压缩形态返回,并把“未压缩”定义成合法输出形态之一,而不是异常路径。

这个策略会把分支测试成本买回来。同一个接口从此有两种合法输出,客户端、中间代理与缓存三条链路都要各自覆盖两种形态;用例至少按三类构造:刚好在阈值之下的、刚好在阈值之上的、以及长度未知的。第三类最常被跳过——流式响应在开始时没有总长度,只能先缓冲或者先放弃压缩,这是阈值策略在落地时最容易被悄悄改掉的一处。

最后,阈值绑在链路单元上,链路变了就得重算。启用更高的最大报文段、换网络栈、或者中间多了一层封装导致可用载荷变小,断点都会跟着移动。阈值不重算,就等于把别人机器上量出来的数抄进自己的容量表。

三、往返账与 CPU 账:什么形状下“体积变小”而整体变差

跨地域链路上,带宽受限、往返代价高,省字节能直接换回传输时间,这一笔通常划得来;前提是链路的瓶颈确实在带宽。如果瓶颈从来不是带宽,而是握手、鉴权或者后端计算,省下来的字节只是账面好看,客户端感知不到。

高并发场景里方向就换了。处理器是共享且稀缺的资源,压缩把单笔请求的占用时间抬高,同样的机器容量下降;等量流量下尾延迟上升,很容易出现“平均每字节成本降了、单位请求成本升了、总吞吐掉了”的组合。这也是为什么只看带宽曲线的复盘会得出完全相反的判断。

要重测的负载形状有三件事必须还原:响应长度的分布(看分位点,不看平均值,平均值会被少数大响应拉走)、并发复用程度(每个连接上跑多少请求、连接是复用还是频繁新建)、以及处理器余量的时间分布(峰值时段是否还有空闲核)。三件里任何一件和线上不一致,测出来的收益都不可信。

四、按数据形状分流的四类判据与各自落点

来源那篇实测记录的核心方法主张是按数据形状分流,而不是给全站一个统一开关。落到工程上,判据是四类。

第一类,已经是压缩格式的内容不再压。图片、音频、视频、已经打包归档的对象,再压一遍几乎没有字节收益,却要付完整的 CPU。判据要看内容本身是不是已编码形态,而不是看响应长度。落点应当放在产出处或者应用侧——只有它知道内容的真实形态;网关侧没有这个信息,只能在类型信息被明确传递上来之后做兜底跳过。

第二类,体积过小的不压,也就是上一节的阈值判断。它需要总长度作为输入。长度已经确定的响应,网关侧就能判;长度要等生成完才知道的,判据就回到了应用侧。同一条判据在两侧实现,成本完全不同,这一点要在设计评审时就定下来。

第三类,可流式输出与需要缓冲之间的取舍。压缩需要缓冲窗口,而流式的价值恰恰在于不等待。大文件导出、报表流、事件推送这三类接口,要分别决定是允许首字节变慢、还是明确不参与压缩。这条判据属于接口语义,只能在应用侧定,网关侧无法反推。

第四类,缓存友好内容的压缩时机。静态与可预生成的资源,可以在构建或发布环节压好,运行时的 CPU 归零,代价是存储与缓存键必须能区分两种编码形态;动态内容只能在响应时压,时机上没有任何选择。把这两类混在一个开关下,是静态资源白耗服务器算力、动态资源又拿不到收益的常见起因。

五、压缩级别要落成一张表

级别不是一个“越高越省”的单调选项,它同时改三个量,所以要按列摊开。下表的收益与代价都是方向描述,具体幅度必须由自己的负载量出来,不能引用别处的比值。

级别档位 字节收益 CPU 增量 延迟增量
低档 有限,但报文段数可能已经减少 最平缓 首字节几乎不动
中档 与低档相比继续变好 上升但仍可预算 需要缓冲,可感知
高档 边际增幅开始递减 明显变陡 流式接口首字节被推后
最高档 相比高档收益不多 最陡,峰值时段挤占处理能力 尾延迟恶化最直接

“最高压缩比级别”不能默认用于在线接口,原因就在最后两列:字节收益的边际增幅已经在递减,而 CPU 和尾延迟这两笔还在按陡坡上升,在线交互的付费方恰恰是尾延迟。它在两类形态上才成立:离线导出(对端不等待交互响应,占用时间不构成用户体验成本)和批量下发(一次生成、多次分发,CPU 成本被分发次数摊薄)。带宽按量计费或者出口受限的场合,也值得单独算一次字节账再决定。

六、三个响应头字段各防一种错配,以及收益验证的口径

三项约束必须同时处理正确,任何一项漏掉,省下的字节都会被缓存与代理层的错配吃掉;而这三类错配各防各的,得分开说。

内容编码标识防的是投错内容。标识与实际编码不一致,或者与自己声明的请求协商结果不一致时,中间层会把已编码的内容当成原文投给不支持的客户端,或者反过来把原文当成编码内容去解。这一类错配在直连时完全看不出来。

随请求头变化的缓存键,防的是互相污染。如果压缩与否取决于请求侧的某个维度,而缓存键没把这个维度带上,压缩版本和非压缩版本就会落在同一个键下互相覆盖,结果是同一份资源先后被不同形态命中。这一项比编码标识更容易长期潜伏,因为命中率曲线看着一切正常。

响应长度防的是长度与实际传输内容不一致。压缩改变了真正传输的字节数,长度字段如果照搬原始体积,断点续传的偏移计算和下载进度显示都会被破坏,客户端还可能在收满之前判定响应截断。

灰度期间的复测要三项一起做,而不是只验编码标识。做法是同一份内容分别用三类请求打:声明支持编码的、声明不支持的、声明支持但中间层会改写请求的;每类都在缓存命中与未命中两种状态下各取一次;再带中间代理层与绕过代理层各取一次。每次比对的结论只有三条:内容能不能正确解出、实际字节数与长度字段是否自洽、带偏移的续传请求能不能正确衔接。

收益验证的口径落在三个指标上:按接口分组的出口字节、尾延迟、单位请求的处理器时间。只在压测环境做一次采样会得出错误结论,原因有三层:压测用的响应体通常是少数几个模板,长度分布比生产更集中,阈值判据几乎不被触发;并发复用率与生产不同,往返那笔账的符号可能就变了;缓存命中率不同,压缩时机那笔账被测成了另一件事。正确的做法是在灰度期间用同一时间轴做对照,至少覆盖峰值与低峰两种负载形状,让三笔账各有一行读数,再决定全量与否。洹拓在给客户做传输优化评审时,要求表上必须三笔账齐全、三个字段自洽,缺任何一项就先补齐证据再谈收益。