2026-09-12-为什么迅雷收尾快-机制对比与改造方案.md

为什么迅雷「收尾」那么快?——机制对比与本机改造方案


一、一句话结论

迅雷没有「收尾合并」这一步。 它下载时就把数据直接写到最终文件的偏移位置,完成时只做一次改名(rename)——不搬运任何数据。而本站是「分片先存别处,收尾再把整份数据按顺序拷贝拼成最终文件」,收尾本身要读一遍、写一遍。

二、迅雷式(BT 类)机制的四个要点

  1. 预分配:开始下载时先把文件撑到最终大小(稀疏/预占空间)。位子先占好,写的时候直接落位。
  2. 偏移直写(核心):每个分片到达后用 pwrite(fd, buf, len, piece_index × piece_size) 写到它在文件中的最终偏移。分片之间互不依赖,所以能乱序写、多线程写、多来源并发写同一文件的不同区域。
  3. 完整性靠位图/分片校验,不靠拼接:每个分片单独校验并标记,全部置位即完成——不需要「把分片按顺序拼起来」这个动作。
  4. rename 收尾:完成时把临时名(.td/.xltd 等)改成正式名。同一分区内 rename 只改目录项,与文件大小无关。

附加的感知层原因:迅雷是异步落盘(缓冲区写完就报「完成」,后台继续 flush),而本站的合并同步卡在上传请求里——客户端必须等合并 HTTP 响应才释放任务名额,所以那段慢是「看得见」的。另外多来源并行补最后几个分片,尾部不会因单源慢而拖尾。

三、两种机制的代价对比

迅雷式(偏移直写 + rename)本站当前(分片 + 合并拷贝)
全程磁盘 I/O1× 文件大小3×(上传写 1× + 合并读 1× + 合并写 1×)
收尾耗时毫秒~亚秒级(rename)与文件大小成正比(读+写各一遍)
峰值占盘1×(预分配)2×(.part 与最终文件并存)
乱序/断点续传天然支持(写偏移)支持(分片文件),但收尾仍要整份拷贝
校验分片哈希/位图位图(meta.received)+ 总长校验

四、本机实测(机械盘 sda5,直接 I/O 绕过页缓存)

实验项实测结果
预分配 2GB 稀疏文件(truncate)1.64 s,实际占盘 0
偏移直写 256MB(写到最终位置)7.03 s → 36.4 MB/s(就是磁盘写速)
收尾 rename(1.25GB 文件)0.74 s
对照:合并拷贝 512MB(读 1× + 写 1×)48.3 s → 折合 10.6 MB/s 产出
单路直接顺序读 / 写54.6 / 23.0 MB/s
2 路并发读+写各 10 MB/s(合计约 20 MB/s)

换算到这批 137GB:迅雷式 = 137GB I/O、收尾≈0;本站式 = 411GB I/O,其中多搬 274GB ≈ 多花 187 分钟(按 25MB/s)。

五、改造方案(让本站也变成迅雷式)

目标设计

  1. 分片到达时不再写 .upload-parts/<hash>/<i>.part,而是:
    • 首次收到该文件的分片时,在目标目录创建隐藏的稀疏文件 .uploading-<uploadId>,ftruncate 到 fileSize;
    • 用 fs.createWriteStream(directPath, { flags:'r+', start: index × CHUNK_SIZE }) 把该分片直接写到最终偏移。
  2. complete:校验 meta.received 覆盖全部分片 + 文件大小正确 → renameSync('.uploading-<id>', finalTarget)(含 rename 改名保存逻辑)→ 删除会话目录。无数据拷贝。
  3. abort / 僵死清理:同时删除 .uploading-*。
  4. 启动孤儿清理:删除没有对应会话的 .uploading-*。
  5. 列表页隐藏:.uploading-* 以点开头(现有逻辑已隐藏隐藏文件),确保半成品不会被下载。

存量会话兼容(重要)

当前批次已有 67.8GB 分片存在 .part 里。混合策略:

风险与验证

六、相关文件

下载此文件