大文件回源的尴尬:一个视频把源站带宽打满
个人站长做站久了,总会遇到「大文件」这个坎。可能是教程站里的 PDF、软件站的安装包、影视站的切片视频,也可能只是几十兆的产品手册。这些文件一旦被多个用户同时下载,你的源站就会被反复回源读同一份数据——同一个 500MB 的文件,10 个人下载就等于源站读了 5GB,而且每次都是从磁盘重新读一遍。
很多人第一反应是「上 CDN」。但 CDN 有个现实问题:冷门文件第一次访问时,CDN 节点仍然要回源拿整个文件。如果这个文件很大(比如 1GB),回源那一下又慢又占带宽,用户首字节等待时间(TTFB)很长;更麻烦的是,如果 CDN 节点缓存空间有限,大文件被淘汰得特别快,导致「缓存命中率低 — 反复回源 — 源站被打满」的恶性循环。
其实 Nginx 自带一个专门解决这个问题的模块——ngx_http_slice_module(切片模块)。它能把一个大文件的回源请求拆成多个固定大小的分片请求,每个分片独立缓存。这篇文章就把它的原理、配置、和「为什么开了没效果」的排查讲清楚,配一套可以直接照抄的配置。
一、slice 模块到底做了什么:把大文件切成缓存友好的小块
要理解 slice,先理解它的默认行为。Nginx 作为反向代理缓存时,正常的缓存单位是「整个响应」。一个 1GB 的文件,就是要缓存 1GB,缓存管理器(cache manager)在决定淘汰谁的时候,这个文件就是一个不可分割的大块。
slice 模块改变了这个粒度。它把一个「大文件请求」在 Nginx 内部转成「一系列带 Range 头的小请求」。比如设置 slice 1m,那么一个 100MB 的文件会被拆成 100 个 1MB 的分片请求,每个分片作为独立的缓存对象存储。带来三个直接好处:
- 回源压力被平摊。客户端要第 3 个分片时,Nginx 只回源取那 1MB,而不是先把整个文件拉下来。用户拖动视频进度条到中间,也只触发对应分片的回源。
- 缓存淘汰更友好。被淘汰的最小单位是分片而非整个大文件,冷门大文件不会因为「太大」而被优先扔掉,热门分片能长期留存。
- 多用户并发下载时命中率提升。多个用户下载同一个大文件,只要他们访问到相同的分片,Nginx 缓存就能直接命中,源站只需要被读一遍。
一句话概括:slice 把「大文件」这个缓存管理器的噩梦,拆成了无数个规整的小问题。
二、和 Range、断点续传的关系:为什么会「配合出问题」
这里有个最容易混淆的点:客户端本身的 Range 请求,和 slice 模块内部的 Range 请求,是两回事。
客户端(比如浏览器、下载工具)如果想要断点续传,会自己带 Range: bytes=1000000-1999999 头。而 slice 模块是 Nginx 在后端对上游发起的 Range 请求。二者都会用到 HTTP 的 206 Partial Content,但方向不同。
理解这个区别之后,我们就能看懂一个经典现象:当你对已经缓存的分片做带 Range 的请求时,Nginx 的 slice 会尝试把客户端的 Range 对齐到分片边界,可能返回比你请求的更多的数据(补齐到分片边缘)。某些老旧的下载工具不理解这种行为,会误以为服务器返回了错误的数据,导致文件损坏。这就是为什么有时候「加了 slice 之后下载工具报错」。
所以对于纯下载站,是否开 slice 要权衡:如果客户端是标准的现代浏览器和主流下载工具,slice 是利大于弊的;如果站点服务的是一堆老旧的、对 206 语义较真的客户端,要先小范围测试。
三、完整配置:一套可直接照抄的方案
下面是一套经过实际使用的 slice 缓存配置。假设你要加速的是一个软件下载站,大文件放在 /download/ 路径下:
# 定义缓存区:key 用 URL,30GB 上限,内存索引 100MB
proxy_cache_path /var/cache/nginx/slice
levels=1:2
keys_zone=slice_cache:100m
max_size=30g
inactive=7d
use_temp_path=off;
server {
listen 80;
server_name download.example.com;
# 关键一:开启 slice,分片大小 1MB
# 分片大小要结合文件大小和缓存条目数权衡,1m~4m 是常用区间
slice 1m;
location /download/ {
proxy_pass http://backend_origin;
# 关键二:让 proxy_cache_key 包含 Range 信息
# 否则不同分片会共用同一个缓存 key,互相覆盖
proxy_cache_key $host$uri$is_args$args$slice_range;
# 关键三:向上游传递 Range 头
proxy_set_header Range $slice_range;
# 缓存策略:200 和 206 都要缓存
proxy_cache slice_cache;
proxy_cache_valid 200 206 7d;
# 允许在缓存过期后先用旧的、后台更新
proxy_cache_use_stale updating;
proxy_cache_background_update on;
proxy_cache_lock on;
# 大文件不要缓冲整个响应,边收边发
proxy_buffering on;
proxy_max_temp_file_size 0;
# 强制关闭上游压缩,避免压缩影响 Range 偏移
proxy_set_header Accept-Encoding "";
}
}逐个解释三个「关键」为什么必须这么配:
slice 1m:必须在server或更上层,不能藏在location里跟其他指令抢作用域。它定义分片粒度。proxy_cache_key带$slice_range:这是 slice 能不能正常工作的命门。每一个分片对应一个不同的Range值,如果缓存 key 不含它,所有分片会挤在同一个 key 上,互相覆盖,缓存彻底失效——症状是「配了 slice 但缓存命中率反而更低」。proxy_set_header Range $slice_range:把 Nginx 计算好的分片 Range 传给上游,上游才知道返回哪一段。
四、验证 slice 到底生效了没有
配完之后,最常见的困惑是「我怎么知道它真的在工作」。三个办法,从粗到细:
办法一:看响应头里的 Content-Range
用 curl 请求一个大文件,注意几个关键点——Nginx 应该返回 206 而不是 200,而且 Content-Range 的边界应该对齐到你的 slice 大小:
curl -sI -H "Range: bytes=0-100" https://download.example.com/download/big.iso
# 期望看到:
# HTTP/1.1 206 Partial Content
# Content-Range: bytes 0-1048575/1073741824 <-- 注意 1048575 就是 1MB-1
# 而你的请求只要 0-100如果 Content-Range 的右边界正好是「分片大小 - 1」,说明 slice 在按 1MB 切。如果它精确等于你请求的 100,说明 slice 没生效或请求被透传了。
办法二:看缓存目录里的文件数
请求完一个大文件后,去缓存目录数一下文件:
find /var/cache/nginx/slice -type f | wc -l
# 一个 100MB 的文件,理论上缓存里应出现接近 100 个分片文件办法三:看是否命中缓存
在配置里加上 add_header X-Cache-Status $upstream_cache_status;,然后重复请求同一个分片:
curl -sI -H "Range: bytes=0-100" https://download.example.com/download/big.iso | grep -i x-cache-status
# 第一次:MISS
# 第二次请求同样的分片:HIT这里要注意:如果你第二次请求的不是对齐的分片边界,Nginx 可能会 MISS 一个新的分片。测试时用固定的、对齐到分片边界的范围,才能稳定看到 HIT。
五、「开了没效果」的四层排查
- 第一层:proxy_cache_key 漏了 $slice_range。这是最高频的错误,占了一半以上的「slice 无效」案例。缓存 key 不含 Range,分片互相覆盖,表现是缓存目录里文件数量很少或者反复抖动。
- 第二层:上游不支持 Range。你的后端(比如某些 PHP 脚本、某些对象存储的转发端)不返回 206 只返回 200,slice 就没有意义。用 curl 直接打后端验证:
curl -sI -H "Range: bytes=0-100" http://backend/file,如果返回 200 而不是 206,先修后端。 - 第三层:磁盘缓存没写权限。
proxy_cache_path指向的目录,Nginx 的工作进程用户(通常是www-data或nginx)必须有写权限。权限不对时 Nginx 静默不缓存,日志里可能有failed (13: Permission denied)但要主动去翻。 - 第四层:缓存被 gzip 干扰。如果 Nginx 对上游启用了压缩,压缩后的字节偏移和原始文件不一致,Range 就错位了。所以配置里要
proxy_set_header Accept-Encoding ""关掉上游压缩,由 Nginx 自己决定是否对客户端压缩。
六、分片大小怎么选:1MB 还是 4MB
slice 的大小不是越小越好,也不是越大越好,它是在「缓存条目数量」和「回源请求次数」之间做权衡:
- 分片太小(如 256KB):文件被切成非常多片,缓存目录里文件数量爆炸,文件系统的 inode 和目录检索压力大,而且每个分片一次回源请求,回源请求数太多。
- 分片太大(如 32MB):失去了切片的意义,又退化成「大块缓存」,客户端拖进度条时还是要回源一大坨数据。
- 经验区间 1MB ~ 4MB:对于视频和安装包,1MB 到 4MB 通常是最舒服的。文件特别大的(1GB 以上)用 4MB,普通几十兆的用 1MB。
如果站点同时有视频和文档,可以用 map 指令按路径动态设置分片大小:
map $uri $slice_size {
default 1m;
~^/video/ 4m; # 视频用大分片
~^/download/ 2m; # 安装包用中等分片
}
server {
slice $slice_size;
# ...其余配置同上
}七、和 CDN 配合的正确姿势
最后回到开头的问题:个人站长大文件加速,到底该怎么组合?
- 源站开 slice:让源站自身的缓存对每个分片管理,回源压力从「整文件」降到「分片」。
- CDN 也开 Range 支持:大部分现代 CDN 都支持 Range 回源。当 CDN 需要某个区间的数据时,它会向源站发 Range 请求,正好被源站的 slice 缓存命中。
- 让 CDN 缓存分片:CDN 节点缓存的是「带 Range 的响应」,下次同样的区间可以直接命中,不再回源。
- 给冷门大文件设置长缓存。大文件内容通常不变,
Cache-Control: public, max-age=31536000配合长缓存,让分片在 CDN 和源站都能长期存活。
切片缓存这件事,看着是 Nginx 一个不起眼的模块,但它是「大文件」和「高并发」这对矛盾之间最优雅的桥梁。配好之后,你会发现源站的磁盘 IO 和带宽图表都会安静很多——因为大部分请求都停在了缓存层,压根没走到磁盘。这就是把「重复劳动」交给分层缓存去消化的价值。