Apache 的 mod_deflate 在压缩响应数据时并不是边收边发。它先把内容写入内部缓冲区,等一个合适的时机再把压缩结果交出去。这个时机直接决定了后端脚本的 flush 是否真的能把字节立刻送到客户端。很多流式接口看似调用了 flush,结果浏览器仍要等几秒才收到数据,问题往往出在压缩过滤器这一层。

压缩缓冲器如何暂存响应数据
Apache 启用 mod_deflate 后,响应内容不会从应用层直接到达客户端,而是先进入输出过滤器链。mod_deflate 在这个链上注册了一个输出过滤器,它维护一块用 DeflateBufferSize 控制大小的内存缓冲区。每次后端模块或 CGI 产生一批数据,这些数据以 bucket 的形式进入过滤器。mod_deflate 先把数据复制到缓冲区,并不立即调用 zlib 压缩。这样做的原因在于压缩率:zlib 需要积累足够的相似数据,才能识别重复模式,给出更有价值的压缩窗口。如果来一个字节就压一个字节,DEFLATE 流的每个同步块都会额外写入块边界,最终体积反而变大,CPU 开销也会增加。
DeflateBufferSize 的默认值是 8096 字节。这意味着在通常情况下,Apache 会至少积攒 8 KB 左右的原始响应数据,才进行一次真正的压缩并向下游发送。对于小于 8 KB 的完整响应,压缩和发送会推迟到响应结束阶段。对于动态生成的页面,后端可能已经执行完毕,数据却仍然滞留在过滤器缓冲区中。理解了这一点,就很容易解释为什么某些接口即使关闭了输出缓冲,响应依然不是即时到达。
这个缓冲过程和 HTTP 的 Transfer-Encoding: chunked 并不冲突。压缩数据本身可以作为分块传输发送,但 chunked 的分块边界由过滤器交出数据的时机决定,而不是由应用层原始数据边界决定。也就是说,后端脚本把数据交给 Apache,Apache 再决定何时压缩和输出。脚本看到的 flush 只是应用层缓冲的清理,真正到达网络层还需要经过压缩过滤器这一关。
触发 Flush 的三种典型时机
第一种时机是压缩缓冲区达到 DeflateBufferSize 阈值。当缓冲区积攒的数据达到设定字节数时,mod_deflate 会把缓冲区内容送入 zlib 进行压缩,并使用 Z_SYNC_FLUSH 方式同步刷新,生成可以立即解码的压缩数据块。这个同步刷新不会结束整个压缩流,但会强制 zlib 输出当前可用的压缩字节。之后缓冲区清空,开始下一轮积累。需要注意,同步刷新会略微降低压缩率,因为每个同步块都需要额外的标记和窗口对齐。
第二种时机是上层过滤器或内容生成器明确传递了 FLUSH bucket。比如 CGI、FastCGI 或者代理模块在处理流式数据时,可能把一个 FLUSH 标记插入到输出队列。mod_deflate 检测到这个标记后,不会再等待缓冲区填满,而是立即压缩当前缓冲区并使用 Z_SYNC_FLUSH 输出。这也是为什么有些 PHP 的 flush 调用在 Apache + mod_deflate 下要经过 CGI 或 FastCGI 层才能表现为实时输出。如果上层模块没有把 FLUSH 标记传递到 Apache 的核心输出链,压缩过滤器就感知不到这个刷新请求。
第三种时机是响应结束。当 mod_deflate 收到 EOS 桶,也就是响应流的结束标记,它会将剩余缓冲区数据交给 zlib,并调用 Z_FINISH 完成整个 DEFLATE 流的收尾。Z_FINISH 会输出流结尾标记,保证客户端可以正确解压并识别数据结束。任何还留在 zlib 内部缓冲区的压缩数据也会在这一步被全部送出。因此,即使响应体远小于 DeflateBufferSize,客户端在响应结束时仍然能收到完整的压缩结果。
实际环境中还可能存在连接中断或内部错误导致的提前清理,但这种情况下压缩数据通常不能正常收尾,客户端可能收到不完整的流。所以排查压缩响应异常时,先要判断是正常的同步刷新还是流结束刷新。
调整 DeflateBufferSize 与性能权衡
DeflateBufferSize 不是越大越好,也不是越小越好。增大缓冲可以提升压缩率,因为同一段窗口内的数据更多,zlib 能找到更多重复序列。对于大体积静态文件或整页 HTML,把缓冲从默认的 8096 提高到 16384 或 32768,通常可以减少最终传输字节数,但会增加首字节响应时间。对于普通网页来说,8 KB 到 16 KB 是比较折中的区间。对于已经压缩的图片、视频或音频文件,不应再启用 mod_deflate,因为压缩收益极低,白白消耗 CPU。
如果系统主要提供 SSE、长轮询或实时日志这类需要连续推送的响应,建议把 DeflateBufferSize 设置成较小的值,甚至直接设置为 0。当值为 0 时,mod_deflate 不再等待缓冲区积累,每次收到数据就立即执行压缩和同步刷新。这样做会让每个小片段都带上 DEFLATE 同步块开销,可能导致总传输体积比不压缩还要大,尤其是数据本身不具备重复特征的场景。不过对于文本型实时消息,2 KB 以上的片段仍然有不错的压缩空间。
下面是一个针对流式文本接口的配置示例,将缓冲区调小以降低发送延迟:
<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
DeflateBufferSize 1024
DeflateCompressionLevel 1
AddOutputFilterByType DEFLATE text/event-stream text/plain
</IfModule>
设置完成后,可以通过 curl 的 -N 参数观察响应是否分段到达。需要查看响应头是否包含 Content-Encoding: gzip 和 Transfer-Encoding: chunked。如果压缩过滤器仍在等待大缓冲,即使 chunked 存在,分段也不明显。验证时可以在后端循环中每次输出一条日志并 sleep,对比修改 DeflateBufferSize 前后的到达间隔。
另一个容易忽略的点是压缩级别。DeflateCompressionLevel 越高,zlib 内部计算越复杂,压缩块生成速度越慢。对于实时场景,设置 1 级或 2 级往往比 6 级更合适,因为 CPU 时间缩短,压缩缓冲刷新的整体延迟也会下降。压缩级别与缓冲大小共同决定响应延迟,不能只调整其中一个。
总结起来,Apache 压缩缓冲的 Flush 并非由某个单一开关控制,而是由缓冲区阈值、FLUSH 标记和响应结束三者共同作用。理解了这些触发时机,就能根据具体应用选择合适配置,避免在流式场景中盲目依赖后端 flush 或直接关闭压缩。