Nginx反代对象存储缓存命中率只有3%?参数剥离与Range切片让回源归零

## 先复盘一个真实翻车现场 业务侧用预签名URL(presigned URL)直传/下载对象存储,前端套了一层Nginx反代做统一鉴权和流量审计。上线后看监控,缓存命中率只有3%左右,Nginx到S3的回源流量拉满,轻则账单翻倍,重则连接池被打爆。 起初怀疑是正常的(毕竟签名URL每次都在变),但仔细看访问日志就明白了: ```nginx "GET /bucket/app/data.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA...&X-Amz-Date=20250501T120000Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=9f8c..." ``` **同一个对象的每次请求,签名参数都不同。** Nginx的默认缓存key是完整URI,等于每次请求都是一个独立的URL,Cache全部Miss。这是反代对象存储最典型的坑,网上几乎没人提。 ## 核心技巧一:缓存key只保留业务参数,彻底剥离签名参数 不要用`proxy_cache_key $uri$is_args$args;`,要精简为业务参数维度: ```nginx proxy_cache_path /var/cache/s3 levels=1:2 keys_zone=s3cache:10g max_size=100g inactive=30d use_temp_path=off; server { location / { proxy_pass http://s3_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; # 核心:缓存key只由 URI + versionId 决定 # 对象存储场景,versionId 是唯一影响对象内容的业务参数,其余全丢了 proxy_cache_key $scheme://$host$uri$is_args$arg_versionId; proxy_cache s3cache; proxy_cache_valid 200 206 10m; # 后端有ETag和Last-Modified就开启这两个 proxy_cache_revalidate on; proxy_cache_background_update on; proxy_cache_lock on; } } ``` 注意两点: - `$arg_xxx` 只能取到QueryString中某个具体参数的值。你以为的“所有业务参数”如果很多,可以逐个追加:`$uri$is_args$arg_versionId$arg_lang`,但签名相关的`X-Amz-*`一律不写进去,自然就从缓存key中消失了。 - `proxy_cache_key`中不要包含`$is_args`之外的完整QueryString,否则前面的工作全白费。 ## 核心技巧二:大文件用 `slice` 分片缓存,Range请求才能命中 如果对象都是视频、镜像等几百MB的大文件,上面的配置仍有隐患:Nginx必须把整个对象拉下来才能缓存,而客户端一般是Range请求(断点下载器、播放器),第一个Range片段走了回源,后续Range又走了回源。你看到的可能是大量206响应直接穿透。 开启分片缓存: ```nginx location / { # 按4MB切片切分缓存单元 slice 4m; proxy_cache_key $scheme://$host$uri$is_args$arg_versionId$slice_range; proxy_cache s3cache; proxy_cache_valid 200 206 200m; # 关键:把Range头透传给上游S3,S3天然支持Range回源,效率极高 proxy_set_header Range $slice_range; proxy_set_header If-Range $slice_range; proxy_ignore_headers X-Accel-Redirect X-Accel-Buffering; proxy_buffering on; } ``` `slice`指令会在Nginx内部把请求切分成多个4MB的`Range`头,每次回源只取4MB。对象存储(S3/OSS/COS)本身对Range支持极其完善,不会像普通静态服务器那样去读整个文件再截断,所以性能没有损耗。 **配合前面的key剥离,整个链路成为:** - 客户端请求带预签名参数 → 命中Nginx整缓存或切片缓存,直接返回; - 缓存未命中且是Range请求 → 只回源获取4MB切片,而不是整个大文件。 ## 核心技巧三:缓存失效策略不要依赖TTL,用ETag + 后台更新 很多团队图省事,把`proxy_cache_valid`调成1分钟,结果就是热点对象每分钟回源一次。更优的做法: ```nginx http { # 长TTL兜底 proxy_cache_valid 200 206 24h; # 但用ETag做强校验 proxy_cache_revalidate on; # 缓存过期后发送条件请求,ETag未变则续期 proxy_cache_background_update on; # 一个请求触发回源校验,其他请求直接拿旧缓存,避免惊群 proxy_cache_lock on; # 同一时间只允许一个请求回源 } ``` 这样源端对象确实更新了(ETag变化),Nginx最长只等一个TTL周期就失效秒被感知;源端未更新则通过304续期,24h有效期可以无限延长。热点请求不会频繁打源。 ## Apache场景:用mod_proxy + mod_cache的取舍 不是只有Nginx才踩这个坑,Apache做反代时也会被签名参数糊弄。Apache `mod_cache_disk`对QueryString的处理更粗糙,但可以做一次内部rewrite剥离签名参数: ```apache RewriteEngine On # 把所有 X-Amz-* 参数和 & 一并剔除再交给mod_cache RewriteCond %{QUERY_STRING} ^(.*)(?:&?X-Amz-[^&]+)(.*)$ [NC] RewriteRule ^(.*)$ /$1?%1%2 [PT,QSA,NE] ``` 然后用`CacheKeyIgnoreQueryString`强制忽略所有QueryString(如果你的业务参数只有签名的话): ```apache CacheEnable disk / CacheKeyIgnoreQueryString On CacheDefaultExpire 3600 ``` 但Apache的缓存命中率和并发能力弱于Nginx,如果是高吞吐场景慎用。 ## 一个更彻底的冷门方案:Nginx直存裸桶,应用层零参与 上面的架构,应用先到Nginx、Nginx再去S3,还是有一层转发。如果你用的是轻云互联的大带宽服务器(Ceph/S3网关配万兆内网),可以在Nginx里直接做**客户端PUT/POST到S3的请求转发**,绕开应用层: ```nginx location /upload/ { # 禁止Nginx把body存到临时文件再转发 proxy_request_buffering off; proxy_http_version 1.1; proxy_set_header Host s3.amazonaws.com; proxy_pass https://s3_upstream; # 透传原始签名 proxy_set_header Authorization $http_authorization; proxy_set_header x-amz-content-sha256 $request_body; } ``` 这个场景在轻云互联的高配实例上很划算:请求body流式转发,Nginx内存占用极低,upload带宽直接打满万兆网卡。前提是对象存储的签名必须由客户端在本地生成好,Nginx不做任何篡改。别尝试用Nginx去改写签名,签名验证会直接失败。 ## 排错命令清单 调完配置别急着上线,先验证: ```bash # 查看缓存key具体值(nginx需要添加$cache_key到log_format) tail -f /var/log/nginx/access.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head # 检查缓存命中率 # 从日志中提取 HIT / MISS 状态统计 grep -o '"upstream_cache_status": *"[^"]*"' /var/log/nginx/access.log | sort | uniq -c # 回源流量实时监控 iftop -i eth0 -f "port 443" -n ``` 如果发现`MISS`占比依然高,用`curl`带签名参数访问两次,打印响应头查看`X-Cache-Status`和`X-Cache-Key`: ```bash curl -sI "https://your-domain/bucket/file.txt?X-Amz-Signature=...&X-Amz-Expires=3600" | grep -Ei "x-cache|x-cache-key" ``` ## 最后一句 很多折腾对象存储的团队天天盯着S3应用层调优,忘掉Nginx这一层才是最容易放大问题的瓶颈。把签名参数从缓存key中抹掉,再配合切片和服务端ETag校验,回源能让它归零。如果你刚好有一台轻云互联的大带宽VPS做边缘缓存,这套配置下去,你会看到带宽曲线断崖式下跌。 别再把Nginx反代当“纯转发”用了,它才是对象存储的影子控制层。