网站上的安装包、备份文件或视频素材,经常可以开始下载,却在传到几十 MB、几百 MB 后突然中断。用户重新点击,只能从头开始;下载工具尝试续传,又收到完整文件或 416 Range Not Satisfiable。接入 CDN 后,问题还可能只出现在部分地区或部分节点。
这类故障要分三层检查:HTTP 分段下载是否正确,CDN 是否按同一对象处理 Range 请求,Nginx 与源站在长连接、缓冲和临时文件上有没有异常。先确认响应,再改配置。只把超时时间调大,往往会掩盖响应范围错误、对象版本变化或回源异常。

先复现一次完整下载和两次分段下载
选择一个能够稳定复现的文件,记录它的 URL、预期大小、发布时间和校验值。不要一开始就用浏览器反复点下载,因为浏览器通常不会完整展示请求头、响应头以及中断位置。
先查看响应头:
curl -sS --head "https://example.com/files/package.zip"
重点记录:
Content-Length:完整对象的字节数。Accept-Ranges: bytes:服务器明确表示支持字节范围请求。ETag与Last-Modified:用于判断下载过程中对象有没有变化。Content-Encoding:确认同一文件是否有时被压缩、有时保持原样。- CDN 自定义的缓存状态、节点或请求 ID:字段名称因服务商而异。
HEAD 响应只能用于初步观察。有些源站或 CDN 对 HEAD 与 GET 的处理并不完全一致,因此还要发送实际 Range 请求:
curl -sS -D range-1.headers \ -H "Range: bytes=0-1048575" \ -o range-1.bin \ "https://example.com/files/package.zip"
正常的单段响应应返回 206 Partial Content,并包含类似下面的头部:
Content-Range: bytes 0-1048575/734003200 Content-Length: 1048576
再请求文件中间的一段:
curl -sS -D range-2.headers \ -H "Range: bytes=104857600-105906175" \ -o range-2.bin \ "https://example.com/files/package.zip"
两次响应的文件总长度、ETag 和 Last-Modified 应保持一致。若 Range 请求被忽略并返回 200 OK,续传工具可能重新接收整个文件;若返回的 Content-Range 与请求范围不符,合并后的文件会损坏;若文件已经替换,旧断点也可能触发 416。
需要模拟断点续传时,可以先中断一次下载,再运行:
curl -C - -O "https://example.com/files/package.zip"
保留 curl -v 输出、响应头、失败时间和已下载字节数。这些信息能直接对应 CDN、Nginx 与源站日志。
分别测试 CDN 链路和源站链路
同一个 URL 经 CDN 失败,不代表源站也失败。HTTPS 站点可以使用 --resolve,在保持域名、SNI 和证书校验条件不变的情况下,把请求直接发送到源站 IP:
curl --resolve example.com:443:203.0.113.10 \ -sS -D origin-range.headers \ -H "Range: bytes=0-1048575" \ -o origin-range.bin \ "https://example.com/files/package.zip"
把源站结果与正常域名访问结果对照:
- 源站稳定返回
206,CDN 请求中断:检查 CDN Range 支持、回源超时、缓存状态和节点日志。 - 源站本身也中断:检查 Nginx、应用、对象存储、磁盘和源站网络。
- 只有某个 Range 偏移失败:检查源文件长度、缓存分片、对象替换以及
proxy_cache_max_range_offset等策略。 - 只有首次 MISS 失败,缓存 HIT 正常:重点检查 CDN 到源站的长连接、首轮回源和缓存填充。
- 不同节点返回不同
ETag或总长度:检查多源站文件是否同步,以及旧缓存是否清理干净。
如果源站设置了访问控制,不要为了测试长期暴露真实 IP。完成对照后恢复防火墙或回源鉴权策略。
Range 请求正确,不等于文件一定可以续传
HTTP Range 的基本条件是“同一个资源的字节位置保持稳定”。下载期间替换文件,即使 URL 没变,也可能让前后两个分段来自不同版本。
发布大文件时,建议使用带版本号或内容摘要的文件名,例如:
app-3.8.2-linux-amd64.tar.gz backup-20260924-8f31c2.zip
客户端续传时可配合 If-Range。当验证器仍匹配,服务器返回指定范围;验证器已经变化时,服务器改为返回完整的新对象,避免把两个版本拼到一起。强 ETag 更适合表示字节完全一致的对象;如果平台只提供时间精度较低的 Last-Modified,频繁覆盖同名文件时要格外谨慎。
还要检查压缩策略。ZIP、MP4、ISO 等已经压缩或不适合动态压缩的大文件,不应在不同请求中随机切换 Content-Encoding。Range 的字节偏移针对的是实际传输表示,节点一会返回 gzip,一会返回未压缩内容,分段长度和偏移就可能失去一致性。
检查 Nginx 响应缓冲和临时文件
Nginx 反向代理默认启用 proxy_buffering。开启后,Nginx 会尽快从上游读取响应,先使用内存缓冲区,放不下的部分可以写入 proxy_temp_path 对应的临时文件。大文件下载异常时,应同时检查错误日志和临时目录所在文件系统:
nginx -T | grep -E "proxy_buffering|proxy_buffers|proxy_max_temp_file_size|proxy_temp_path" df -h df -i
日志中如果出现临时文件写入失败、磁盘空间不足、inode 耗尽或权限错误,就先修复存储问题。不要直接把临时目录改成 777。
关闭缓冲可以让上游响应边到边发,但这会让慢速客户端更长时间占用上游连接。下面只是适用于专用下载路径的起点,不应不加区分地放到整个站点:
location /files/ {
proxy_pass http://download_backend;
proxy_http_version 1.1;
proxy_buffering off;
proxy_read_timeout 300s;
send_timeout 300s;
}
proxy_read_timeout 计算的是两次上游读取之间的空闲时间,send_timeout 计算的是两次向客户端写入之间的空闲时间,都不是“整个文件必须在多少秒内下载完成”。如果上游持续输出数据,长下载可以超过这些数值;如果中间长时间没有任何数据,再大的总带宽也无法避免超时。
保留缓冲时,应根据并发量、磁盘速度和可用空间设置缓冲区及临时文件上限。把 proxy_max_temp_file_size 设为 0 会禁止把代理响应写入临时文件,它不是通用性能优化。修改前先确认当前瓶颈来自临时文件,而不是上游速度、磁盘容量或客户端网络。
每次修改后先执行:
nginx -t
确认配置无误后再平滑重载,并保留可回滚的旧配置。
检查 Nginx 缓存和分片回源
当 Nginx 自己承担代理缓存时,大偏移 Range 请求可能直接穿透到上游。proxy_cache_max_range_offset 可以规定允许缓存处理的最大偏移;超过该偏移的 Range 请求会转发给上游,响应不进入缓存。如果故障总发生在文件后半段,应检查这个指令是否存在,以及上游能否稳定处理对应偏移。
对于体积很大、访问量高且经常发生 Range 请求的文件,可以评估 Nginx Slice 模块。它把完整响应切成固定大小的子请求,并分别缓存:
proxy_cache_path /data/nginx/cache keys_zone=download_cache:20m max_size=200g;
location /files/ {
slice 1m;
proxy_cache download_cache;
proxy_cache_key $uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 1h;
proxy_pass http://download_backend;
}
这段配置不能直接复制到生产环境。ngx_http_slice_module 默认不一定编译进 Nginx,分片大小还会影响子请求数量、缓存文件数量、回源并发和首段延迟。启用前先运行 nginx -V 确认模块,在测试环境验证完整下载、随机 Range、并发请求、缓存命中和源文件更新。
proxy_force_ranges on 可以让代理响应支持字节范围,即使原响应没有 Accept-Ranges。它适合可确认字节稳定的静态对象,不适合用来掩盖动态接口、实时生成文件或会随请求变化的响应。源站返回的内容不稳定时,强制 Range 只会让损坏更隐蔽。
检查 CDN 的缓存对象是否一致
不同 CDN 对 Range、分片缓存、最大缓存文件、回源超时和动态压缩的处理不同,不能只看控制台里的“缓存已开启”。应从实际响应和服务商日志确认:
- Range 请求在 HIT 与 MISS 时是否都返回正确的
206。 - 回源请求是否携带 Range,还是由 CDN 拉取完整文件后再切片响应。
- 同一 URL 在不同节点返回的
Content-Length、ETag、Last-Modified是否一致。 - 缓存键是否意外受查询参数、鉴权头或 Cookie 影响,导致同一文件形成多份对象。
- 刷新缓存时是否只清理了入口 URL,却留下旧分片或旧版本对象。
- 下载响应是否被边缘函数、WAF、限速规则或动态压缩改写。
排障期间可以为响应增加一个短期诊断头,标记源站实例或文件版本,但不要暴露内部 IP、目录和凭据。若文件在多台源站之间同步,发布流程应先完成所有节点写入与校验,再切换对外版本。
日志要能回答“断在哪一层”
Nginx 访问日志至少应临时包含 Range、状态码、发送字节数、请求时长、上游时长和缓存状态。例如:
log_format download_diag '$time_iso8601 $request_id $remote_addr '
'"$request" status=$status bytes=$body_bytes_sent '
'range="$http_range" content_range="$sent_http_content_range" '
'request_time=$request_time upstream_time=$upstream_response_time '
'cache=$upstream_cache_status upstream=$upstream_addr';
这套日志能区分几种常见情况:
- Nginx 记录
499:客户端或客户端前面的网络设备先断开连接。 - Nginx 返回
502、504:继续查看同一请求 ID 对应的上游错误。 - 状态是
206,但body_bytes_sent明显小于本段预期长度:连接在响应完成前中断。 - CDN 显示回源超时,源站没有对应请求:检查 CDN 到源站的网络、DNS、防火墙或回源地址。
- 源站请求完成,CDN 到客户端失败:检查边缘节点、客户端网络、限速和 CDN 发送超时。
诊断日志可能快速增长。问题定位后应恢复正常日志格式,并按现有策略轮转和清理。
修复后按四种场景验收
不要只测试一次完整下载。至少完成下面四组验证:
- 从零开始下载完整文件,核对大小和 SHA-256。
- 下载中途主动中断,再从断点续传,核对最终 SHA-256。
- 请求文件开头、中间和尾部三个 Range,确认均返回正确的
206与Content-Range。 - 分别测试 CDN HIT、CDN MISS 和直连源站,确认总长度、验证器和文件内容一致。
可在服务器端生成校验值:
sha256sum package.zip
客户端完成下载后运行同样的校验。只有文件大小相同并不能证明内容正确,尤其是在断点续传、节点切换或源文件被覆盖的场景中。
最终保留一份记录:文件版本命名规则、源站校验值、CDN Range 行为、Nginx 下载路径配置、超时含义、缓存刷新步骤和日志查询方法。以后再出现“大文件总在某个位置中断”,可以先用同一组 Range 请求判断对象是否一致,再决定检查 CDN、Nginx 还是源站。
参考资料
- RFC 9110:HTTP Semantics,Range Requests、206 Partial Content、Content-Range 与 If-Range。
- MDN Web Docs:HTTP range requests。
- Nginx 官方文档:
ngx_http_proxy_module,包括proxy_buffering、proxy_read_timeout、send_timeout、proxy_force_ranges、proxy_cache_max_range_offset与临时文件相关指令。 - Nginx 官方文档:
ngx_http_slice_module。
资料核验日期:2026-09-24。




