~/Downloads/download-server.js)| # | 改动 | 说明 |
|---|---|---|
| 1 | 分片直写最终偏移 | 分片不再落地为 <i>.part,而是写进会话目录里的稀疏文件 <会话>/data,偏移 = 序号 × 分片大小(fs.createWriteStream(data, {flags:'r+', start: index*cs})) |
| 2 | 收尾只做 rename | complete 校验通过后 rename(data → 目标路径)。数据文件放在 .upload-parts/<hash>/ 下、与目标同在 用户上传 分区内,因此 rename 是纯元数据操作;半成品也天然不会出现在列表页 |
| 3 | 预分配 | 首次收到分片时 ftruncate(fileSize) 把稀疏文件撑到最终大小,不占实际空间 |
| 4 | 收尾闸门保留 | 原来的「合并并发闸门」(DL_MERGE_CONCURRENCY=2)改用于「遗留分片补齐」,避免多文件同时搬盘 |
x-chunk-size 声明 > 会话记忆(meta.chunkSize) > 存量 .part 推断 > 本次 Content-Length 反推 > 默认 64MB。 前端已同步加上 x-chunk-size 头;老页面(不带该头)也能被正确推断,因此不需要强制刷新页面。Content-Length 校验「期望长度」(除最后一块外等于分片大小),不符直接 400 且不登记。finish(数据真正写完)之后才把分片登记进 meta,避免崩溃时把半个分片误标为已收到。.part 不算数:状态接口现在按「长度是否等于期望」判定已收到。否则客户端会跳过截断的块、收尾又报长度不符,形成「跳过→失败→再跳过」死循环。.part 不动,新分片直写 data;收尾时只把老 .part 按偏移补进 data,新数据不重复搬运。优先采用直写数据,半截 .part 视为不存在。fileSize/totalChunks 与本次请求不一致时,服务端主动重置会话(前端也会先 abort)。/tmp/xunlei-test.mjs)| 用例 | 结果 |
|---|---|
直写模式:会话目录只有稀疏 data,没有 .part;data 表观大小 = 文件大小 | ✅ |
| 3 分片(64+64+32MB)上传 → 收尾 → 逐字节一致 | ✅ 收尾 9ms |
存量会话(分片全在 .part)→ 收尾按偏移补齐 → 逐字节一致(legacyCopied=2) | ✅ |
半截 .part 被判为「未收到」,complete 正确 409 | ✅ |
| 长度不符的分片被拒(400)且不登记 | ✅ |
| 自定义分片大小 16MB×3 直写正确 | ✅ |
| 稀疏空洞(meta 声称收到但无数据)被检出 409 | ✅ |
sync 后读块设备计数)| 指标 | 旧方式(分片 + 合并拷贝) | 现在(迅雷式) |
|---|---|---|
| 块设备写 | ≈ 2.00×(分片 1× + 合并 1×) | 1.00×(实测 640MB 数据 → 写 640MB) |
| 块设备读 | ≈ 1.00×(合并时把分片读一遍) | 0.00×(实测读 0MB) |
| 收尾耗时(512MB 对照) | 48.3 s(拷贝) | 3 ms(rename) |
| 峰值占盘 | 2×(.part 与最终文件并存) | 1× |
.part —— 这部分是改造前写入的,仍必须搬一次(读+写),这正是当前 mergeQueued=7 在做的事,每个多 GB 文件在本机机械盘上要几分钟。data 文件)→ 这部分省掉了 2× 的重复 I/O。systemctl status dl-server # 服务状态
curl -s --noproxy '*' http://127.0.0.1:8899/healthz # 含 mergeActive / mergeQueued
tail -f ~/Downloads/dl-server.log # 带时间戳
DL_MERGE_CONCURRENCY(收尾并发,默认 2)、DL_UP_PARALLEL(前端并发,默认 5).upload-parts/<sha256(name)[:24]>/ 内含 meta.json(含 received/sizes/chunkSize/direct)、data(稀疏目标文件)、以及存量 *.partdownload-server.js.bak-20260912-214503 是改造前的版本(不含本日其余修复,仅作参考);如需回滚请连同 io_uring/连接治理改动一并评估~/Downloads/download-server.js (handleUploadChunk 直写、completeUpload 校验+补齐+rename、ensureSparseData、copyIntoAt、chunkSizeOf/chunkExpect、handleUploadStatus 合并判定)/tmp/xunlei-test.mjs(20 项专项)、/tmp/up-test.mjs(23 项回归)、/tmp/io-proof2.mjs(I/O 放大证明)
dl-hub/05-下载服务维护/tests/现象:用户看到 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 级的遗留分片补齐,于是长时间停在 检查中…。
服务端当时确实在高速工作(同期实测):
rchar +3239MB、wchar +3238MB/proc/<pid>/fd 中可见正在拷贝的 <i>.part → datamergeQueued 从 7 降到 2~3,已落盘模型从 25 增至 28修复:completeMerge() 进入收尾时立即把行内文案改为 📦 文件名 — 收尾落盘中…(等待落盘队列) (需刷新页面生效;服务端已于 23:27:56 重启加载)