/home 为机械盘(/dev/sda5,rotational=1)迅雷没有「收尾合并」这一步。 它下载时就把数据直接写到最终文件的偏移位置,完成时只做一次改名(rename)——不搬运任何数据。而本站是「分片先存别处,收尾再把整份数据按顺序拷贝拼成最终文件」,收尾本身要读一遍、写一遍。
pwrite(fd, buf, len, piece_index × piece_size) 写到它在文件中的最终偏移。分片之间互不依赖,所以能乱序写、多线程写、多来源并发写同一文件的不同区域。.td/.xltd 等)改成正式名。同一分区内 rename 只改目录项,与文件大小无关。附加的感知层原因:迅雷是异步落盘(缓冲区写完就报「完成」,后台继续 flush),而本站的合并同步卡在上传请求里——客户端必须等合并 HTTP 响应才释放任务名额,所以那段慢是「看得见」的。另外多来源并行补最后几个分片,尾部不会因单源慢而拖尾。
| 迅雷式(偏移直写 + rename) | 本站当前(分片 + 合并拷贝) | |
|---|---|---|
| 全程磁盘 I/O | 1× 文件大小 | 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)。
.upload-parts/<hash>/<i>.part,而是:
.uploading-<uploadId>,ftruncate 到 fileSize;fs.createWriteStream(directPath, { flags:'r+', start: index × CHUNK_SIZE }) 把该分片直接写到最终偏移。complete:校验 meta.received 覆盖全部分片 + 文件大小正确 → renameSync('.uploading-<id>', finalTarget)(含 rename 改名保存逻辑)→ 删除会话目录。无数据拷贝。abort / 僵死清理:同时删除 .uploading-*。.uploading-*。.uploading-* 以点开头(现有逻辑已隐藏隐藏文件),确保半成品不会被下载。当前批次已有 67.8GB 分片存在 .part 里。混合策略:
.part 不动。complete 时:只把「老 .part 存在的那部分」按偏移拷进临时文件(这部分不可避免要搬一次),新分片已经在位;然后 rename。meta.received 判定,重新上传该分片即可)。~/Downloads/download-server.js(handleUploadChunk / completeUpload / streamMerge / 孤儿清理)/tmp/up-test.mjs(23 项功能测试)