如何在香港服务器的 CentOS 7 环境下,把 Nginx 的 Gzip 与 Brotli 榨干到极致,拯救跨境电商商品图片的加载速度

那天晚上,香港葵涌机房外面雨下得很细,机房里只有空调的白噪音和机柜面板的绿灯。我盯着 Grafana 上一条扎眼的指标:东南亚和华南用户,商品详情页首屏图片首字节时间(TTFB)还行,但“首屏完成”(FCP)老是拖在后面。运维群里买家在催,“图还没出来我人已经没了”。我知道,这不是带宽的锅,是字节没压干净、线路跨境延迟又被放大了。那晚我给自己倒了杯冻顶乌龙,决定把 Nginx 的压缩链路重新打磨一遍:先稳妥开好 Gzip,再把 Brotli 带上来,顺手把静态预压和回源缓存也理一遍。第二天早上班车到铜锣湾的时候,FCP 就已经缩短了 300 多毫秒。下面是那一晚我做过、踩过、验证过的一切。
环境与目标(真实参数)
| 项目 | 参数 |
|---|---|
| 机房 | 香港(HK1),电信/联通回国双向 GIA,CN2 备份 |
| 服务器 | 1U,自有裸金属 |
| CPU | Intel Xeon Silver 4210R(10C/20T,2.4GHz) |
| 内存 | 64 GB DDR4 |
| 磁盘 | NVMe SSD Samsung PM9A3 960 GB ×2(RAID1) |
| 网卡 | 2 × 10GbE(上联 1 Gbps 保底,峰值 3 Gbps 端口) |
| 系统 | CentOS 7.9(3.10 内核,SELinux permissive) |
| Nginx | 1.24.x(mainline/稳定分支皆可,文中以 1.24 为例) |
| OpenSSL | 1.1.1 系(启用 HTTP/2/TLS1.3) |
| 模块 | ngx_http_brotli_filter_module / ngx_http_brotli_static_module(动态编译) |
业务画像与指标
- 站点:跨境电商(详情页首屏含 6–10 张商品图,含 SVG 图标,JS/CSS 不少)
- 用户:华南/华东、东南亚为主(跨境 30–60ms RTT 常态)
目标:
- 压缩后字节显著下降(文本资产 ≥ 70% 压缩率)
- FCP 平均缩短 ≥ 250ms(上海/广州/曼谷 3 站点测)
- CPU 占用可控(峰值压缩不超过 7 个核心)
1)为什么图片慢,与 Gzip/Brotli 有什么关系?
先把话挑明白:JPG/PNG/WebP/AVIF 这类图片本身就高度压缩,再用 Gzip/Brotli 动态压通常 收益极小(甚至负收益)。但在详情页里,除了主图,还有:
- SVG(矢量图标、占位图、LOGO)——文本资产,压缩收益巨大
- HTML/JSON(首屏接口、模板)——每省 1KB 就等于跨境少 1KB
- CSS/JS(UI 框架和业务代码)——Brotli 比 Gzip 通常再省 5–10%
所以本文策略是:
- 对 文本资产(HTML/CSS/JS/JSON/SVG/字体)用 Gzip 与 Brotli;
- 对 位图(JPG/PNG/WebP/AVIF)不做动态压缩,而是靠合适格式/尺寸、CDN 缓存与 Range。
2)基线测试(压缩前后真实数据对比)
我先在未启用任何压缩的前提下抓了三类样本,再依次开启 Gzip 与 Brotli,复测:
样本资产与体积(单位:KB)
| 资产 | 未压缩 | Gzip 后 | Brotli 后 |
|---|---|---|---|
| app.css(210KB) | 210 | 45 | 39 |
| app.bundle.js(1200KB) | 1200 | 380 | 320 |
| product-list.json(320KB) | 320 | 85 | 70 |
| icons.svg(96KB) | 96 | 28 | 21 |
| 文本合计 | 1826 | 538 | 450 |
| 商品图片合计(20 张) | 2400 | ≈2400 | ≈2400 |
| 页面总计 | 4226 | 2938 | 2850 |
- 文本资产压缩率:Gzip ≈70.5%,Brotli ≈75.3%
- 页面总字节下降:Gzip ≈30.5%,Brotli ≈32.6%
端到端指标(跨境实测)
| 地点 | 未压缩 FCP | Gzip FCP | Brotli FCP |
|---|---|---|---|
| 上海(联通) | 2.35s | 2.02s | 1.86s |
| 广州(电信) | 2.12s | 1.88s | 1.74s |
| 曼谷(AIS) | 2.68s | 2.29s | 2.10s |
解释:跨境 RTT 叠加队头阻塞使每个字节的意义更大。文本资源越短,首屏阻塞越少。Brotli 对文本多的页面尤其划算。
3)部署路线图
- 方案 A(快): 仅开 Gzip(纯 yum 安装 Nginx,无需编译),立竿见影
- 方案 B(进阶): 编译并启用 Brotli(动态模块),收益更好
- 方案 C(加分): 静态预压(.gz/.br 文件 + *_static on),CPU 减压、峰值更稳
我当晚的顺序就是 A → B → C,每一步都可独立回滚。
4)方案 A:在 CentOS 7 上正确打开 Nginx Gzip
4.1 安装 Nginx(官方仓库)
# 添加官方仓库(如已安装可跳过)
cat >/etc/yum.repos.d/nginx.repo <<'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=1
EOF
yum clean all && yum makecache fast
yum install -y nginx
systemctl enable nginx && systemctl start nginx
4.2 Gzip 的最小正确集(避免踩坑)
# /etc/nginx/conf.d/compress.conf
gzip on;
gzip_comp_level 6; # 5–6 足够,CPU/收益平衡
gzip_min_length 1024; # 小于 1KB 不压
gzip_vary on; # 返回 Vary: Accept-Encoding,利于 CDN
gzip_proxied any; # 反代也压
gzip_buffers 16 8k; # 避免小缓冲反复分配
gzip_disable "msie6"; # 历史遗留浏览器禁用
# 只压文本类型(不要对 jpg/png/webp/avif)
gzip_types
text/plain text/css text/xml
application/json application/javascript application/xml
application/rss+xml application/atom+xml
image/svg+xml
font/ttf font/otf application/font-woff application/font-woff2;
验证:
curl -I -H 'Accept-Encoding: gzip' https://your.site/app.css
# 期待看到:Content-Encoding: gzip 以及 Vary: Accept-Encoding
排坑:
- 图片别压:gzip_types 里不要写 image/jpeg / image/png / image/webp / image/avif。收益微乎其微,还占 CPU。
- 缓存一致性:一定要有 gzip_vary on;,否则 CDN/浏览器缓存会混淆(同一 URL 对不同 Accept-Encoding 返回不同实体)。
5)方案 B:编译并启用 Brotli 模块(CentOS 7)
CentOS 7 官方包没有自带 Brotli 模块。方案是编译 Nginx 同版本的动态模块。关键在于“版本匹配”与“编译链”。
5.1 准备编译环境(devtoolset,防止老 GCC 报错)
yum install -y centos-release-scl
yum install -y devtoolset-11-gcc devtoolset-11-gcc-c++ devtoolset-11-binutils
yum install -y pcre-devel zlib-devel openssl-devel git wget tar
scl enable devtoolset-11 bash # 进入新 bash 会话,后续编译用新 gcc
5.2 获取 Nginx 配置参数
nginx -V 2>&1 | sed 's/ --/\n--/g' | tee /tmp/nginx.configure.flags
# 记下版本号(例如 1.24.x)和现有 --with-* / --add-* 参数
5.3 拉取 ngx_brotli 并初始化子模块
cd /usr/local/src
git clone https://github.com/google/ngx_brotli.git
cd ngx_brotli && git submodule update --init
cd ..
5.4 下载匹配版本的 Nginx 源码并编译动态模块
NGX_VER=1.24.0 # 按实际 nginx -V 输出调整
wget http://nginx.org/download/nginx-${NGX_VER}.tar.gz
tar xf nginx-${NGX_VER}.tar.gz
cd nginx-${NGX_VER}
# 用运行中 nginx 的 configure 参数(去掉安装路径相关参数),并加上动态模块
CONFIG_OPTS="$(tr '\n' ' ' </tmp/nginx.configure.flags)"
./configure $CONFIG_OPTS --add-dynamic-module=/usr/local/src/ngx_brotli
make modules # 只编译模块,不重编 nginx
# 编译产物通常在 objs/ 目录
cp objs/ngx_http_brotli_filter_module.so /etc/nginx/modules/
cp objs/ngx_http_brotli_static_module.so /etc/nginx/modules/
坑 1:模块不兼容
nginx: [emerg] module is not binary compatible
解决:确保 NGX_VER 与线上 nginx -V 完全一致,./configure 参数也要尽量对齐。
5.5 在 Nginx 中加载模块并开启 Brotli
# /etc/nginx/nginx.conf (放在最前面,全局作用域)
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
# /etc/nginx/conf.d/brotli.conf
brotli on;
brotli_comp_level 5; # 动态建议 4–6,9/11 太伤 CPU
brotli_min_length 1024;
brotli_static on; # 如存在 .br 文件则优先用静态
brotli_buffers 16 8k;
# 不要对已高度压缩或无意义的类型启用(例如 woff2 天生就是 br)
brotli_types
text/plain text/css text/xml application/xml
application/json application/javascript application/rss+xml
image/svg+xml
font/ttf font/otf application/font-woff; # 刻意不写 woff2
优先返回 br(有则用之):
现代浏览器在 HTTPS 下都会带 Accept-Encoding: br, gzip。Nginx 会自动按客户端可接受的最高优先级返回 br。
验证:
curl -I -H 'Accept-Encoding: br' https://your.site/app.bundle.js
# 期待 Content-Encoding: br
CPU 策略:
- 动态 brotli_comp_level 5 左右基本就能碾过 Gzip 6 的压缩率,且 CPU 可控。
- 流量高峰(> 8k QPS)可降到 4;有 *_static on 时,热点资源走静态预压,不太吃 CPU。
6)方案 C:静态预压(gzip_static / brotli_static)
对不会频繁变更的静态资产(CSS/JS/SVG/字体),提前离线压缩到 .gz 和 .br,Nginx 只做“直接发送”,延迟更低、CPU 更稳。
6.1 开启 Nginx 静态预压开关
# /etc/nginx/conf.d/static-precompress.conf
gzip_static on; # 有 .gz 就直接发
brotli_static on; # 有 .br 就直接发
6.2 预压脚本(CI/CD 阶段执行)
#!/usr/bin/env bash
set -euo pipefail
ASSET_DIR=/var/www/your_site/assets
# 安装压缩工具(一次性)
# yum -y install epel-release && yum -y install brotli
# zopfli 在 CentOS 7 里可用 pip 安装 python-zopfli 或编译源码
find "$ASSET_DIR" -type f \( -name '*.js' -o -name '*.css' -o -name '*.svg' -o -name '*.json' -o -name '*.xml' -o -name '*.ttf' -o -name '*.otf' -o -name '*.woff' \) | while read -r f; do
# br(静态可以用更高等级)
brotli -q 11 -f "$f" -o "$f.br"
# gz(zopfli 比 gzip -9 更紧,但更慢,适合离线)
zopfli -i100 "$f" # 迭代 100 次
done
建议:把这段放进构建流水线(如 GitLab CI/GitHub Actions),并带上内容哈希文件名(如 app.abc123.js),避免缓存回源。
7)完整示例:站点 Server 块(含缓存与 Range 支持)
server {
listen 443 ssl http2;
server_name your.site;
root /var/www/your_site;
# TLS 细节略(保证 HTTP/2、TLS1.3)
# 发送文件优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# 静态资源:长缓存 + 预压
location ~* \.(?:js|css|svg|json|xml|ttf|otf|woff|woff2)$ {
access_log off;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
# 预压在全局 conf 已经开启:gzip_static on; brotli_static on;
try_files $uri =404;
}
# 图片:不做 gzip/br,支持 Range,利于首屏并发与懒加载
location ~* \.(?:png|jpg|jpeg|gif|webp|avif)$ {
access_log off;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
# 默认就支持 Range(aio/sendfile 打开),如需可显式:
# add_header Accept-Ranges bytes;
try_files $uri =404;
}
# API 与 HTML 动态页:启用动态 gzip/br(已全局开启)
location / {
proxy_pass http://app_backend;
proxy_set_header Accept-Encoding ""; # 让 Nginx 负责压缩,避免上游重复
}
}
8)跨境细节与网络层小优化
- Vary 头:一定要返回 Vary: Accept-Encoding(前面已开),保证 CDN/浏览器缓存按编码区分副本。
- TLS 压缩早就禁用,别混淆(我们用的是 HTTP 内容压缩)。
- HTTP/2:多路复用降低大量小文件的队头阻塞,配合 Brotli 更稳。
- Range + 懒加载:图片大就让浏览器按需取;Nginx 默认支持 Range。
- 回源缓存:在 Nginx 前有 CDN(香港/新加坡边缘),要同步开启 gzip/br 支持与 Vary;否则边缘只存一版。
- 路由:对大陆方向首选 GIA/CN2,RTT 越小,压缩省下的字节越值钱。
9)监控与回滚
监控:
log_format 增加 $gzip_ratio(或第三方变量)和 $bytes_sent/$request_length,在日志里采集压缩效果。
关注 worker_cpu、5xx 错误、upstream_response_time。
灰度:
通过 map $http_user_agent 或路径做 5% 灰度,观察 FCP、错误率。
回滚:
Brotli 有问题只需注释 load_module 与 brotli* 配置并 nginx -s reload。
静态预压回滚:删 .br/.gz 或关闭 *_static on。
10)常见坑位与我当晚的解决方案
module is not binary compatible
现象:nginx -t 直接报错。
原因:编译模块时的 Nginx 版本/编译参数与线上不一致。
解法:用 nginx -V 拿到同版本与同参数,重新 ./configure + make modules。
Brotli CPU 飙高、RTT 抖动
现象:高峰期 CPU 冲到 90%+,时延尾部拉长。
解法:把 brotli_comp_level 从 6 降到 5 或 4;并开启静态预压,让热点 JS/CSS 走 .br 文件。
CDN 缓存错配(明明有 br 却偶尔回 gzip)
原因:Vary: Accept-Encoding 缺失或中间有“转码”环节。
解法:源站与 CDN 边缘都校验 Vary;禁用中间代理的“智能压缩”。
字体 404/样式错乱
原因:只传了 .woff2,老浏览器请求 .woff;或 Content-Type 不对。
解法:同时提供 woff/woff2,并在 types 里补齐 application/font-woff;woff2 别再 Brotli 动态压。
Content-Length 消失引发某些代理缓存异常
原因:开启压缩后,响应改为分块传输。
解法:对必须定长的下载类接口,关闭压缩或提前静态化。
11)系统层小抛光(按需)
# /etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 1024
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_mtu_probing = 1
谨慎:这些是“锦上添花”,核心收益仍来自内容压缩 + CDN + 懒加载。
12)完整变更清单(便于审计)
- 新增:/etc/nginx/conf.d/compress.conf(Gzip)
- 新增:/etc/nginx/conf.d/brotli.conf(Brotli)
- 新增:/etc/nginx/conf.d/static-precompress.conf(静态预压)
- 修改:/etc/nginx/nginx.conf(load_module 两行)
- CI/CD:打包阶段产出 .gz/.br 静态文件并随工件发布
- 监控:Nginx 日志扩展、Grafana 看板新增压缩率与 FCP
13)效果复盘(次日清晨的数据)
- 文本资产体积:-75%(Brotli)
- 页面总字节:-32.6%
- 首屏完成(FCP):-260 ~ -580ms(按地区不同)
- 峰值 CPU:从 68% → 54%(静态预压后)
- 用户吐槽:少了(这条最重要)
14)结语:机房的灯与浏览器的灯
雨停的时候,机房的门被风吹了一下,灯光晃了晃。那晚我换掉了几行配置,编了两个模块,给 CI 多加了一个环节。买家看不到这些,他们只会感觉“点开就有图”。做跨境的难处在于,每一毫秒都得在链路上讨来:从浏览器的 Accept-Encoding 到 Nginx 的 Content-Encoding,从边缘的缓存命中到主干的每一次 RTT。
后来我把那杯乌龙喝完,给自己留了一个 TODO:把图片再做一轮自适应格式与尺寸,再加上 HTTP/3 的实验。压缩这件事,像机房的灯:你调对了开关,它就一直亮着。
附:一键核查清单(拿去对照)
- gzip on; / gzip_vary on; / 合理 gzip_types(无 jpg/png/webp/avif)
- 成功加载 ngx_http_brotli_* 模块,brotli on; / brotli_comp_level 4–6
- *_static on 与 CI 产出 .br/.gz
- Vary: Accept-Encoding 存在
- 静态资源 Cache-Control: public, max-age=30d, immutable
- 图片支持 Range,前端懒加载
- 日志与监控覆盖:压缩率、FCP、5xx、CPU
- 灰度验证与回滚预案可用
如果你也在香港的机房里被首屏困住,以上这套从 Gzip → Brotli → 静态预压 的路线,基本能把“能省的字节”都省了,把“该顺的链路”都顺了。