2026-09-12-迅雷式落盘改造完成.md

迅雷式落盘改造完成(稀疏文件 + 偏移直写 + rename)


一、改造内容

#改动说明
1分片直写最终偏移分片不再落地为 <i>.part,而是写进会话目录里的稀疏文件 <会话>/data,偏移 = 序号 × 分片大小(fs.createWriteStream(data, {flags:'r+', start: index*cs}))
2收尾只做 renamecomplete 校验通过后 rename(data → 目标路径)。数据文件放在 .upload-parts/<hash>/ 下、与目标同在 用户上传 分区内,因此 rename 是纯元数据操作;半成品也天然不会出现在列表页
3预分配首次收到分片时 ftruncate(fileSize) 把稀疏文件撑到最终大小,不占实际空间
4收尾闸门保留原来的「合并并发闸门」(DL_MERGE_CONCURRENCY=2)改用于「遗留分片补齐」,避免多文件同时搬盘

关键设计决定(都踩过坑)

  1. 分片大小必须确知:偏移 = 序号 × 分片大小,而分片大小由客户端决定。取值优先级: x-chunk-size 声明 > 会话记忆(meta.chunkSize) > 存量 .part 推断 > 本次 Content-Length 反推 > 默认 64MB。 前端已同步加上 x-chunk-size 头;老页面(不带该头)也能被正确推断,因此不需要强制刷新页面。
  2. 长度校验前移:直写下长度错位会直接损坏文件。现在在写入之前用 Content-Length 校验「期望长度」(除最后一块外等于分片大小),不符直接 400 且不登记。
  3. 崩溃安全:只在写流 finish(数据真正写完)之后才把分片登记进 meta,避免崩溃时把半个分片误标为已收到。
  4. 半截 .part 不算数:状态接口现在按「长度是否等于期望」判定已收到。否则客户端会跳过截断的块、收尾又报长度不符,形成「跳过→失败→再跳过」死循环。
  5. 存量会话混合兼容:老分片留在 .part 不动,新分片直写 data;收尾时只把老 .part 按偏移补进 data,新数据不重复搬运。优先采用直写数据,半截 .part 视为不存在。
  6. 换文件兜底:会话的 fileSize/totalChunks 与本次请求不一致时,服务端主动重置会话(前端也会先 abort)。

二、验证结果(真实 ext4 /dev/sda5 上)

专项测试:20 / 20 通过(/tmp/xunlei-test.mjs)

用例结果
直写模式:会话目录只有稀疏 data,没有 .part;data 表观大小 = 文件大小✅
3 分片(64+64+32MB)上传 → 收尾 → 逐字节一致✅ 收尾 9ms
存量会话(分片全在 .part)→ 收尾按偏移补齐 → 逐字节一致(legacyCopied=2)✅
半截 .part 被判为「未收到」,complete 正确 409✅
长度不符的分片被拒(400)且不登记✅
自定义分片大小 16MB×3 直写正确✅
稀疏空洞(meta 声称收到但无数据)被检出 409✅

回归测试:23 / 23 通过(原有上传功能全套:跳过去重、改名保存、断点续传、子目录落盘、页面注入)

I/O 放大证明(640MB 上传,sync 后读块设备计数)

指标旧方式(分片 + 合并拷贝)现在(迅雷式)
块设备写≈ 2.00×(分片 1× + 合并 1×)1.00×(实测 640MB 数据 → 写 640MB)
块设备读≈ 1.00×(合并时把分片读一遍)0.00×(实测读 0MB)
收尾耗时(512MB 对照)48.3 s(拷贝)3 ms(rename)
峰值占盘2×(.part 与最终文件并存)1×

三、对「正在进行的那一批」的影响(重要)

四、运维与回滚

systemctl status dl-server            # 服务状态
curl -s --noproxy '*' http://127.0.0.1:8899/healthz   # 含 mergeActive / mergeQueued
tail -f ~/Downloads/dl-server.log     # 带时间戳

五、相关文件

六、上线后发现并修掉的「看起来卡住」问题(23:27)

现象:用户看到 5 行停在 🔍 … — 检查中…,以为卡死。

实情:这 5 个文件当时已经全部落盘成功(chilloutmix 3.97GB、Counterfeit-V2.5-pruned 1.99GB、Counterfeit-V2.5_pruned 3.97GB、CounterfeitV30_v30-pruned 1.99GB、CounterfeitV30_v30 3.95GB)。用户看到的是文案缺口: 当前端检查到「所有分片其实早已收齐」(重选续传的常见情形)时,会从 检查中… 直接进入收尾落盘,中间不更新行内文案;而服务端此时正在做 GB 级的遗留分片补齐,于是长时间停在 检查中…。

服务端当时确实在高速工作(同期实测):

修复:completeMerge() 进入收尾时立即把行内文案改为 📦 文件名 — 收尾落盘中…(等待落盘队列) (需刷新页面生效;服务端已于 23:27:56 重启加载)

下载此文件