上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-09-07 10:40 阅读量:669


那天晚上,香港葵涌机房外面雨下得很细,机房里只有空调的白噪音和机柜面板的绿灯。我盯着 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 → 静态预压 的路线,基本能把“能省的字节”都省了,把“该顺的链路”都顺了。

目录结构
全文