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反代当“纯转发”用了,它才是对象存储的影子控制层。