Nginx-proxy缓存断点续传缓存 range

一、引言:为什么你的大文件缓存总是"碎"的?

在音视频平台、软件下载站、OTA升级服务等场景中,一个令人头疼的现象反复出现:

  • 用户下载一个2GB的安装包,中途断开后重新连接,Nginx却从头开始传输,而非从断点续传;
  • proxy_cache 对同一个大文件产生了数十个碎片化的缓存文件,磁盘IO飙升,命中率极低;
  • 客户端发送 Range: bytes=1000-2000 请求,Nginx回源时却拉取了整个文件,带宽被浪费99%;
  • 开启了缓存,但日志中大量 MISSBYPASS,Range请求似乎永远无法命中缓存;
  • 多客户端同时请求同一文件的不同片段,后端被重复回源打垮。

这些问题的根源在于:HTTP Range协议与Nginx proxy_cache的默认行为之间存在语义鸿沟。标准的proxy_cache将整个响应视为一个不可分割的原子单元,而Range请求要求将资源切分为可独立寻址、独立缓存的子范围。两者直接对接,必然产生碎片化、回源放大和续传失败。

本文将从HTTP Range协议本质出发,深入解析Nginx处理Range请求的三种模式,给出生产级slice分片缓存配置,并覆盖音视频拖拽、多线程下载等高频场景的调优策略。


二、协议基础:HTTP Range与206响应

2.1 Range请求的核心语义

HTTP/1.1 RFC 7233定义了Range机制,允许客户端请求资源的某个子范围:

复制代码
GET /video.mp4 HTTP/1.1
Host: example.com
Range: bytes=1048576-2097151

服务端正确响应为 206 Partial Content

复制代码
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-2097151/1073741824
Content-Length: 1048576
Accept-Ranges: bytes

2.2 断点续传的完整流程

复制代码
1. 客户端首次请求 → 服务端返回200 + Accept-Ranges: bytes
2. 传输中断,客户端记录已接收字节数 N
3. 客户端重连,发送 Range: bytes=N-
4. 服务端返回 206 + Content-Range: bytes N-{total-1}/{total}
5. 客户端拼接数据,完成传输

📌 关键认知:断点续传不是"特殊功能",而是HTTP标准协议的一部分。Nginx作为代理,必须正确透传Range头、生成206响应、并在缓存层支持子范围检索。任何一环缺失,续传就会退化为全量重传。


三、Nginx处理Range的三种模式

3.1 模式对比

模式 原理 优点 缺点 适用场景
原生透传 Nginx不缓存Range响应,每次回源获取子范围 实现简单 无缓存收益,回源带宽浪费 小文件、低频访问
全量缓存+本地切片 首次回源拉取完整文件并缓存,后续Range从本地切片响应 Range命中率高 首次延迟高,大文件占满带宽 中等大小文件(<100MB)
slice分片缓存 将文件按固定大小切片,每个切片独立缓存和回源 首字节快、并行回源、缓存粒度可控 配置复杂,需slice模块 大文件、音视频、CDN源站

3.2 默认行为的陷阱

Nginx proxy_cache 默认不缓存206响应。当客户端发送Range请求时:

  1. Nginx检查缓存 → 未找到完整文件的缓存条目
  2. 回源获取206响应 → 收到部分数据
  3. 判断206不可缓存 → 直接透传给客户端
  4. 下次相同Range请求 → 再次回源

这就是"Range请求永远MISS"的根本原因。要解决此问题,必须显式启用Range缓存支持。


四、方案一:全量缓存 + 本地切片(中小文件)

适用于文件大小可控(通常<100MB)、首次访问可接受完整下载的场景。

4.1 配置模板

复制代码
http {
    proxy_cache_path /var/cache/nginx/files levels=1:2
                     keys_zone=file_cache:50m max_size=50g
                     inactive=7d use_temp_path=off;

    server {
        listen 80;

        location /downloads/ {
            proxy_cache file_cache;
            proxy_cache_key "$host$request_uri";
            proxy_cache_valid 200 7d;

            # ⭐ 关键:启用Range支持
            proxy_cache_revalidate on;
            proxy_set_header Range $http_range;
            proxy_set_header If-Range $http_if_range;

            # 允许缓存206响应(Nginx 1.19.3+)
            proxy_cache_valid 206 7d;

            add_header X-Cache-Status $upstream_cache_status;
            add_header Accept-Ranges bytes always;

            proxy_pass http://backend;
        }
    }
}

4.2 工作原理

  1. 首个Range请求到达 → 缓存MISS → Nginx回源获取完整文件(忽略Range)
  2. 完整文件写入缓存 → Nginx从本地缓存中切出请求的范围 → 返回206
  3. 后续任意Range请求 → 缓存HIT → 直接从本地切片响应

⚠️ 局限性:首次请求无论Range范围多小,都会触发完整回源。对于10GB视频文件请求最后1秒的内容,仍需等待全量下载完成。此模式不适合大文件和低延迟场景。


五、方案二:slice分片缓存(大文件生产标配)

5.1 slice模块原理

ngx_http_slice_module 将大文件逻辑上切分为固定大小的切片(如1MB),每个切片作为独立的缓存单元:

复制代码
原始文件: [====10GB====]
切片视图: [1MB][1MB][1MB]...[1MB] (共10240个切片)

客户端请求 Range: bytes=5000000-6000000
→ 映射到切片 #4 和 #5
→ 仅回源这两个切片(2MB),而非完整10GB
→ 切片独立缓存,后续相同范围直接HIT

5.2 安装确认

slice模块自Nginx 1.9.8起内置,但默认未编译。验证方式:

bash 复制代码
nginx -V 2>&1 | grep -o 'http_slice'
# 若无输出,需重新编译添加 --with-http_slice_module

5.3 生产级slice配置模板

复制代码
http {
    proxy_cache_path /var/cache/nginx/slice levels=1:2
                     keys_zone=slice_cache:100m max_size=200g
                     inactive=30d use_temp_path=off;

    upstream storage_backend {
        server 10.0.1.1:8080;
        server 10.0.1.2:8080;
    }

    server {
        listen 80;

        # ========== slice核心配置 ==========
        slice              1m;                    # 切片大小1MB
        slice_align        on;                    # 对齐到切片边界

        location /media/ {
            # ========== 缓存配置 ==========
            proxy_cache slice_cache;
            proxy_cache_key "$host$uri$slice_range";  # ⭐ key必须包含slice_range
            proxy_cache_valid 200 206 30d;
            proxy_cache_lock on;                  # 防止同一切片并发回源
            proxy_cache_lock_timeout 10s;

            # ========== Range透传 ==========
            proxy_set_header Range $slice_range;  # ⭐ 用slice_range替代http_range
            proxy_set_header If-Range "";         # ⭐ 禁用If-Range,避免切片失效
            proxy_set_header Host $host;

            # ========== 回源优化 ==========
            proxy_max_temp_file_size 0;           # ⭐ 禁止临时文件,直接写入缓存
            proxy_buffering on;
            proxy_buffer_size 16k;
            proxy_buffers 8 128k;

            # ========== 响应头 ==========
            add_header X-Cache-Status $upstream_cache_status always;
            add_header X-Slice-Range $slice_range always;
            add_header Accept-Ranges bytes always;

            proxy_pass http://storage_backend;
        }
    }
}

5.4 六个关键配置解读

proxy_cache_key 必须包含 $slice_range
复制代码
# ❌ 错误:所有切片共享同一key,互相覆盖
proxy_cache_key "$host$uri";

# ✅ 正确:每个切片有独立key
proxy_cache_key "$host$uri$slice_range";

$slice_range 是slice模块生成的规范化范围字符串(如 bytes=0-1048575)。遗漏它会导致所有切片写入同一个缓存条目,数据完全错乱。

② 用 $slice_range 替代 $http_range 回源
复制代码
# ❌ 错误:透传客户端原始Range,可能跨越多个切片
proxy_set_header Range $http_range;

# ✅ 正确:回源时使用切片对齐后的范围
proxy_set_header Range $slice_range;

客户端请求 bytes=500000-1500000 跨越了两个1MB切片。若直接透传,后端返回1MB数据,但Nginx期望的是当前切片的精确范围,导致缓存写入失败。$slice_range 确保每次回源只请求一个完整切片。

③ 禁用 If-Range
复制代码
proxy_set_header If-Range "";

If-Range 用于条件性Range请求:"如果ETag没变就给我Range,否则给完整文件"。但在slice模式下,每个切片是独立实体,没有全局ETag概念。保留If-Range可能导致后端返回完整文件,破坏切片逻辑。

proxy_max_temp_file_size 0 是性能关键

默认情况下,Nginx先将回源响应写入临时文件,再拷贝到缓存目录。对于slice场景,每个切片都会触发一次临时文件IO。设为0后,响应直接流式写入缓存路径,消除双倍磁盘写入

⑤ slice大小的选择策略
文件大小 推荐slice大小 理由
< 10MB 不分片,用全量缓存 分片开销大于收益
10MB ~ 100MB 256KB ~ 512KB 平衡命中率与元数据开销
100MB ~ 1GB 1MB 通用最优值
> 1GB 2MB ~ 4MB 减少切片数量,降低keys_zone压力

📌 调优原则 :slice越小,缓存粒度越细、首字节越快,但元数据开销和lock竞争越大;slice越大,吞吐越高,但局部更新和续传灵活性越差。1MB是经过大量生产验证的通用起点

proxy_cache_lock 防止切片击穿

多个客户端同时请求同一未缓存切片时,lock确保只有一个回源请求,其余等待。对于热门视频的同一时间段,这能将后端压力降低数十倍。


六、高频场景专项调优

6.1 音视频拖拽播放

用户频繁跳转进度条,产生大量随机Range请求:

复制代码
# 缩短锁等待时间,避免拖拽卡顿
proxy_cache_lock_timeout 3s;

# 启用background_update,拖拽时优先返回旧切片
proxy_cache_background_update on;
proxy_cache_use_stale updating;

# 预加载相邻切片(需Lua或njs配合)
# 当请求切片N时,异步预热N+1和N-1

6.2 多线程下载 / P2P种子

下载工具同时发起数十个Range请求:

复制代码
# 增大并发连接限制
proxy_cache_lock_age 10s;       # 允许更多并发等待
limit_req zone=slice_limit burst=50 nodelay;

# 后端连接池复用
upstream storage_backend {
    keepalive 64;
}
location /media/ {
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

6.3 OTA固件升级

设备端断点续传 + 版本校验:

复制代码
# 保留If-Modified-Since用于版本验证
proxy_set_header If-Modified-Since $http_if_modified_since;
proxy_cache_revalidate on;

# 固件文件长缓存 + 主动purge
proxy_cache_valid 200 90d;
# 新版本发布时通过purge API清理旧版本缓存

七、监控与排障

7.1 关键日志格式

复制代码
log_format slice_log '$remote_addr [$time_local] "$request" '
                     '$status $body_bytes_sent '
                     '$upstream_cache_status $slice_range '
                     '$request_time $upstream_response_time';

7.2 健康指标

指标 健康值 异常处理
206 HIT率 > 70% 低于50%检查slice_range是否在key中
切片回源大小 ≈ slice_size 远大于slice说明Range透传错误
lock等待超时率 < 1% 过高增大lock_timeout或扩容后端
临时文件写入量 = 0 非零检查max_temp_file_size配置
keys_zone使用率 < 80% 过高扩大内存或增大slice大小

7.3 快速验证命令

bash 复制代码
# 测试Range是否返回206
curl -I -H "Range: bytes=0-1023" http://localhost/media/test.mp4

# 验证缓存命中
curl -sI -H "Range: bytes=0-1023" http://localhost/media/test.mp4 | grep X-Cache

# 检查切片key是否正确
curl -sI -H "Range: bytes=0-1023" http://localhost/media/test.mp4 | grep X-Slice-Range

八、常见踩坑速查表

现象 根因 解决方案
Range请求始终MISS 未启用206缓存或未用slice 添加proxy_cache_valid 206或使用slice模块
缓存数据错乱 cache_key缺少 $ slice_range key中加入 $ slice_range
回源流量远超预期 透传 httprange而非httpr​ange而非 slice_range 改用 $ slice_range回源
首次请求极慢 全量缓存模式+大文件 切换slice分片模式
拖拽播放卡顿 lock_timeout过长 缩短至3s + background_update
磁盘IO翻倍 未关闭临时文件 proxy_max_temp_file_size 0
206响应缺少Content-Range 后端不支持Range 确认后端Accept-Ranges: bytes
slice模块指令报错 未编译http_slice 重新编译加--with-http_slice_module
并发下载时后端被打垮 未开启cache_lock proxy_cache_lock on
If-Range导致返回完整文件 slice模式下未清除If-Range proxy_set_header If-Range ""

九、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
井川廊咏2 小时前
【总集篇】Linux | 如何创建一个 home 目录在 /data 磁盘的 sudo 用户(补档)
linux·运维·服务器
Kina_C3 小时前
LVS负载均衡的十二种核心调度算法
linux·运维·算法·负载均衡·lvs
AOwhisky3 小时前
Linux(CentOS)系统管理入门笔记(第十五期)——文件系统与存储管理(上篇):设备识别、挂载与文件查找
linux·运维·笔记·centos·文件系统
杨运交3 小时前
[053][核心模块]Java枚举缓存与ORM集成实践
java·开发语言·缓存
维核科技4 小时前
算力机房高负荷期稳供指南:GPU前置维保与应急处置方案
运维·服务器维修·gpu维修·算力卡维修
worilb4 小时前
Spring Cloud 学习与实践(16):Elasticsearch 商品搜索、中文分词与索引一致性
运维·jenkins
摇曳的精灵4 小时前
dify的docker部署
运维·docker·容器·dify
weixin_462901975 小时前
TCP+WebSocket双向转发服务器 完整部署运维手册
运维·websocket·tcp/ip
AAA@峥5 小时前
Ceph RBD 块存储全解析:原理与实操汇总
运维·分布式·ceph