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

香港服务器的Linux系统中,如何设置并优化 Nginx 缓存规则,避免跨境电商大促时商品详情页频繁超时?

发布人:Minchunlin 发布时间:2025-09-11 10:26 阅读量:589


凌晨 2:40,香港葵涌机房墙上大屏的监控红得刺眼:商品详情页(/product/…)在“黑五预热”流量冲峰里开始出现 504,P99 响应一路抬头到 3.2s,应用组同事在群里刷着“DB 连接打满”“价格服务不回”“PHP-FPM backlog 飙升”。

我盯着一条 access log——/product/sku123,30 秒内被撞了两万次,全都直捣后端。那一刻我确定:这不是“代码快一点”的问题,而是该把“可缓存的动态页”尽量挡在 Nginx 层。

接下来的 6 小时,我把缓存结构、缓存 key 维度、微缓存、并发锁、stale 回源与观察指标一件件补上。当早上 8 点第一波更大流量进来的时候,图上那条 P99 线平稳在 180ms。

环境与基线

硬件与网络(香港机房落地)

规格
机房 HK(多线 BGP,优先直连大陆,含 CMI/CT/CU 优化路由)
服务器 2× Intel Xeon Silver 4210R,128GB RAM
存储 2× NVMe SSD(Intel P4510 2TB)做 RAID1(mdadm),EXT4(noatime)
网卡 2×10GbE(LACP)
OS CentOS 7.9(内核 5.4 LTS,ELRepo)
Nginx 1.24.x(Mainline),编译内置 http_slicestreamgeoip2 可选
应用 PHP-FPM 7.4(池化 96 workers),Upstream 价格与库存微服务(gRPC/HTTP 混合)
连接周边 前置海外 CDN(可选),主流终端来自大陆、东南亚与中东

说明:CentOS 7 已停更,我为了 BBR 与 io_uring 之类能力,切了 ELRepo 内核(5.4),系统稳定性更佳。若你是 Rocky/Alma 8/9,同样策略可用。

症状与基线数据(大促前一晚)

指标 峰值前 峰值时(问题态)
详情页 QPS 2.1k 8.6k
P95 / P99 (ms) 320 / 540 1,900 / 3,200
5xx 比例 0.1% 4.7%(以 504 为主)
上游(APP)CPU 45% 92%
MySQL 连接 680 / 2,000 2,000(满)
价格服务超时 高(> 8%)

根因:可缓存的详情页(SKU 不变)被当作“每次都得算”的动态请求。热点 SKU 遭遇“缓存踩踏”,后端被同一条 SKU 暴打。

设计目标与原则

  • 热点 SKU 微缓存:把“秒级稳定性”交给 Nginx,TTLs 30–60s,为价格\库存“秒级一致但不是绝对强一致”的场景兜底。
  • 防“缓存踩踏”:cache lock + stale-while-revalidate,只让一个请求去回源,其它直接吃旧缓存。
  • 安全维度:缓存 key 必须隔离 SKU / 国家 / 币种 / 设备类型 / 语言。登录态、购物车、定制化必须绕过缓存。
  • 回源友好:保持上游keepalive、降重重试、健康探测、熔断与限流策略。
  • 可观测:日志里必须落地 $upstream_cache_status、cache 命中率、回源耗时等。

缓存策略设计(要点一图流)

  • 微缓存(Microcaching):详情页 30–60s;异常状态 5–10s;404 10s。
  • 并发锁:proxy_cache_lock on; + proxy_cache_background_update on;
  • 旧缓存可用:proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
  • Key 维度:$scheme$host$request_uri + $geo_country + $currency + $device + $lang(示例用 map/header 生成)
  • Cookie 绕过:session/cart/uid 等命中即 no_cache/bypass。
  • 避免压缩碎片化:不让上游返回压缩体,由 Nginx 统一 gzip,减少“按 Accept-Encoding”产生的多份缓存。
  • 回源头部:透传 X-Forwarded-For、X-Real-IP、X-Request-ID,便于上游定位。
  • 容量与目录层次:NVMe 上 max_size 控制 + levels=1:2,禁用临时盘复制(use_temp_path=off)提升写入效率。

系统与文件系统调优(关键但容易忽略)

/etc/sysctl.d/99-nginx-tuning.conf

net.core.somaxconn = 4096
net.core.netdev_max_backlog = 250000
net.ipv4.ip_local_port_range = 10240 65000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 2097152
vm.swappiness = 1
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5

limits 与挂载

# /etc/security/limits.d/nginx.conf
nginx soft nofile 1048576
nginx hard nofile 1048576
  • EXT4 挂载加 noatime,nodiratime,discard(NVMe)
  • RAID1 上把缓存分区单独做 LVM,便于扩容与迁移。

Nginx 核心配置(可直接套用)

下面以 proxy_cache(上游是 HTTP 应用)为例。如果你是 PHP-FPM,可替换为 fastcgi_cache,指令基本同构。

1)全局缓存区与日志

/etc/nginx/nginx.conf(节选)

user  nginx;
worker_processes auto;
worker_rlimit_nofile 1048576;

events {
    worker_connections  65535;
    multi_accept on;
    use epoll;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    # 统一 gzip,由 Nginx 压缩,避免按 Accept-Encoding 形成多份缓存
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css application/json application/javascript application/xml text/xml;

    # 避免上游直接返回压缩体(减少缓存碎片)
    proxy_set_header Accept-Encoding "";

    # 缓存目录(NVMe 分区)
    proxy_cache_path /data/nginx_cache/detail levels=1:2 keys_zone=DETAIL:512m
                     max_size=200g inactive=60m use_temp_path=off
                     loader_files=2000 loader_threshold=300;

    # 访问日志带缓存命中信息
    log_format main '$remote_addr - $request_id [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                    'rt=$request_time urt=$upstream_response_time ucs=$upstream_cache_status';
    access_log /var/log/nginx/access.log main;

    # 连接上游的通用头
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Request-ID $request_id;

    # 地域/币种/设备/语言的归一(示例:来自边缘/CDN 的头或本地 map)
    map $http_x_country $geo_country { default "HK"; CN "CN"; SG "SG"; AE "AE"; }
    map $http_x_currency $currency   { default "HKD"; CN "CNY"; SG "SGD"; }
    map $http_user_agent $device     { default "pc"; ~*"(iphone|android|mobile)" "m"; }
    map $http_accept_language $lang  { default "en"; ~*zh "zh"; ~*ar "ar"; }

    # 是否命中“登录/购物车/预览”等需要绕过缓存的 Cookie
    map $http_cookie $has_user_cookie {
        default 0;
        ~*(session|uid|cart|token) 1;
    }
    # 请求参数绕过(例如预览、AB 实验)
    map $arg_preview $is_preview { default 0; 1 1; }
    map $arg_ab      $is_ab      { default 0; ~.+ 1; }

    # 汇总成绕过标志
    map "$has_user_cookie$is_preview$is_ab" $nocache {
        default 0;
        "100" 1; # 有用户态
        "010" 1; # 预览
        "001" 1; # AB
        "110" 1; "101" 1; "011" 1; "111" 1;
    }

    upstream app_backend {
        server 10.0.10.21:8080 max_fails=3 fail_timeout=10s;
        server 10.0.10.22:8080 max_fails=3 fail_timeout=10s;
        keepalive 256;
    }

    server {
        listen 80 reuseport;
        server_name www.example.hk;

        # 产品详情页命中
        location ~ ^/product/ {
            proxy_cache            DETAIL;
            proxy_cache_key        "$scheme://$host$request_uri|$geo_country|$currency|$device|$lang";
            proxy_cache_valid      200 301 302 1m;     # 正常页 1 分钟
            proxy_cache_valid      404 10s;            # 404 短暂缓存,降低打点
            proxy_cache_valid      500 502 503 504 5s; # 异常短缓存,抑制雪崩
            proxy_cache_lock       on;                 # 防止并发回源
            proxy_cache_lock_timeout 5s;
            proxy_cache_min_uses   1;
            proxy_cache_background_update on;          # 后台刷新
            proxy_cache_use_stale  updating error timeout invalid_header http_500 http_502 http_503 http_504;

            # 控制绕过/不缓存
            proxy_no_cache         $nocache;
            proxy_cache_bypass     $nocache;

            # 避免个性化头污染缓存(谨慎!)
            proxy_ignore_headers   X-Accel-Expires;
            add_header             X-Cache-Status $upstream_cache_status always;

            proxy_read_timeout     2s;   # 上游慢就别拖
            proxy_connect_timeout  1s;
            proxy_send_timeout     2s;

            proxy_pass http://app_backend;
        }

        # 不缓存的区域(例如购物车、下单)
        location ^~ /cart/  { proxy_pass http://app_backend; }
        location ^~ /order/ { proxy_pass http://app_backend; }

        # 健康检查/监控
        location /nginx_status {
            stub_status on;
            allow 127.0.0.1;
            deny all;
        }
    }
}

要点

  • 统一 gzip:让 Nginx 压缩,避免按 Accept-Encoding 形成多份缓存副本。
  • Key 维度够用不泄漏:国家/币种/设备/语言是电商常见差异化因素(价格、模板、文案)。
  • Lock + Background Update:只让一个请求打穿,其他读旧缓存。
  • 短 TTL 的异常缓存:把临时高峰中的偶发 5xx 用 5s 缓住,抑制瞬时风暴。
  • 谨慎 ignore headers:不要无脑忽略 Set-Cookie,本例是保证绕过条件成立才缓存,否则易“个性化内容泄露”。

2)PHP-FPM 场景(fastcgi_cache 简版示意)

如果详情页是 PHP 渲染:

fastcgi_cache_path /data/nginx_cache/detail levels=1:2 keys_zone=DETAIL:512m max_size=200g inactive=60m use_temp_path=off;

location ~ ^/product/ {
    include fastcgi_params;
    fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
    fastcgi_pass   127.0.0.1:9000;

    fastcgi_cache              DETAIL;
    fastcgi_cache_key          "$scheme://$host$request_uri|$geo_country|$currency|$device|$lang";
    fastcgi_cache_valid        200 1m;
    fastcgi_cache_valid        404 10s;
    fastcgi_cache_valid        500 502 503 504 5s;
    fastcgi_cache_lock         on;
    fastcgi_cache_lock_timeout 5s;
    fastcgi_cache_use_stale    updating error timeout invalid_header http_500 http_502 http_503 http_504;
    fastcgi_no_cache           $nocache;
    fastcgi_cache_bypass       $nocache;

    add_header X-Cache-Status $upstream_cache_status always;
}

预热、失效与“不会翻车”的操作面

预热热点 SKU(简单可行)

# 热门SKU列表(根据近1小时 PV 排序)
cat hot_skus.txt | while read sku; do
  curl -s -m 2 "http://www.example.hk/product/$sku" -H "X-Country: CN" -H "X-Currency: CNY" -H "Accept-Language: zh" >/dev/null &
done
wait

细粒度失效(没有第三方 purge 模块时)

  • Key 加版本:在 proxy_cache_key 里附加 $arg_v,当商品价格变动时,发布系统给该 URL 带上 ?v=timestamp,实现自然穿透。
  • CDN 配合:若前置 CDN,优先在 CDN 层做 Tag/Path purge,源站尽量少做“全盘清”。
  • OpenResty 可选:若使用 OpenResty,可借 lua_shared_dict 维护 SKU→版本号映射,proxy_cache_key 引用变量,运维接口可原子更新版本号,命中即生效。

观测与告警

access log 示例

203.0.113.10 - 2f2a3c... [11/Sep/2025:02:58:10 +0800] "GET /product/sku123 HTTP/1.1" 200 45231 "-" "Mozilla/5.0 ..."
rt=0.182 urt=0.000 ucs=HIT
  • ucs=HIT/BYPASS/MISS/EXPIRED/UPDATING 一眼看懂缓存状态
  • Grafana/Prometheus 抓取:命中率、回源 RT、5xx 比例、NVMe IOPS/带宽、连接数
  • 告警阈值:详情页 P99 > 500ms 持续 3 分钟,命中率 < 70%,上游 5xx > 1%

实测数据(优化前后对比)

指标 优化前 优化后(微缓存 60s + lock + stale)
详情页 QPS(峰值) 8.6k 13.2k
命中率(热点 2k SKU) 18% 86%
P95 / P99 (ms) 1,900 / 3,200 120 / 180
5xx 比例 4.7% 0.3%
上游 CPU 92% 48%
MySQL 连接 2,000(满) 900 左右
价格服务超时 > 8% < 1%

解释:秒级微缓存+并发锁,把“热点 SKU 的重复回源”拦截在 Nginx;use_stale 避免上游偶发抖动时拉垮体验;上游减少无谓计算,腾出资源给真的动态请求(下单/支付)。

性能补丁包(不要忽略的小细节)

  • NVMe 专属优化:use_temp_path=off + 独立分区;loader_files/threshold 提升冷启动扫描效率。
  • 连接池:upstream keepalive 256,避免短连接抖动;proxy_http_version 1.1;(如需要)。
  • 超时要短:proxy_read_timeout 2s,宁愿早放手让 stale 顶上,不要“拖慢全场”。
  • HEAD/Range:详情页通常不用 range,若有大图/视频,拆静态域名+slice 模块单独缓存。
  • BBR(可选):内核 5.4 开启 BBR,跨境长肥管道稳定不少。
  • 时钟同步:NTP 必须硬;缓存 TTL 与日志时间对齐,否则排障极其折磨。
  • 大连接突刺:reuseport 配合 SO_REUSEPORT,多 worker 更均衡。

踩坑记录(我真遇到过)

把 Set-Cookie 忽略错了:一开始我粗暴 proxy_ignore_headers Set-Cookie;,结果个性化推荐位被缓存,不同用户看到相同“最近浏览”。
修正:通过 $nocache 精确绕过,仅在“无用户态”时缓存。

Accept-Language 影响模板:没把 $lang 放入 key,导致英语用户看到中文模板。
修正:Key 加 $lang,或在渲染层避免模板差异。

CDN 与源站 TTL 打架:CDN 60s、源站 30s,出现“CDN 仍旧 MISS 并打爆源站”的奇怪波形。
修正:统一把 源站 TTL ≥ CDN TTL;热点路径让 CDN 更长、源站短些配 stale。

预热脚本没带国家/币种头:导致预热错面向 HK,而大量流量来自 CN,命中率上不去。
修正:预热脚本按主要区域维度触发。

一键检查清单(上线前 10 分钟自检)

  1.  /product/ 命中 HIT?
  2.  登录后访问 HIT 是否消失(应 BYPASS)?
  3.  切换 X-Country/X-Currency,缓存 key 分片是否生效?
  4.  上游慢时,是否 stale 顶上且 P99 不抖?
  5.  热点 SKU 预热是否按主要地区/设备覆盖?
  6.  NVMe 分区监控容量与 IOPS 是否在健康区间?
  7.  告警规则就绪:命中率、P99、5xx、NVMe、连接数。

附:完整示例配置(可直接落盘)

见上文 nginx.conf 片段;实际我会按“server 级 include”拆分到 /etc/nginx/conf.d/detail.conf,并把 map 放到 maps.conf,便于多人协作与回滚。

当天上午 8 点半,第二波流量比凌晨更大。大屏上 P99 仍然贴着 180ms 的水平线,我的手终于从键盘上松下来。
走出机房,电梯镜子里我像熬夜的熊猫——但心里是轻的:我们没有靠“祈祷”度过高峰,而是把可缓存的压力前置在 Nginx,让后端专注真正的动态。
后来几个大促,我们照着这套策略微调 TTL、完善“按 SKU 版本号”的失效,命中率稳定 80%+。每次看到那条稳定的延迟曲线,我都会想起那晚机房冷飕飕的风——以及屏幕上消失的 504。

TL;DR(可粘贴给同事的 30 秒摘要)

  1. 对商品详情页用“秒级微缓存”+ 并发锁 + stale 背景刷新;
  2. 缓存 key 必须包含 SKU/国家/币种/设备/语言,登录/购物车/预览绕过;
  3. 统一由 Nginx 压缩,避免 Accept-Encoding 导致多份缓存;
  4. 上游超时宁短不长,让 stale 顶上,避免拖慢全局;
  5. 观测必做:$upstream_cache_status 落日志、命中率/回源 RT/5xx 告警完善。
目录结构
全文