# 上传并发导致「卡死」：根因定位与修复

- 日期：2026-09-12
- 对象：本机下载中心 8899（`~/Downloads/download-server.js`）
- 起因：用户反馈「之前因为多并发导致进程卡死了」
- 说明：本文治的是**卡死（进程活着但事件循环被独占、服务无响应）**；
  另一种「进程被静默杀死」是独立故障，见同目录《2026-09-12-下载服务静默死亡根因与systemd托管.md》

---

## 一、卡死的真正来源（代码级定位）

| # | 位置 | 问题 | 后果 |
|---|---|---|---|
| 1 | `handleUploadComplete` 合并落盘 | `fs.readFileSync(64MB)` + `fs.writeSync` **逐块全同步**合并 | 合并 6.9GB 文件 = 6.9GB 同步读写，**事件循环被独占十几秒**；多个文件同时 complete 叠加成数分钟完全无响应 |
| 2 | 合并后的 `fs.rmSync(dir,{recursive:true})` | 同步递归删除含 GB 级 `.part` 的会话目录 | 每个文件合并后都要阻塞一次，量大时持续卡顿 |
| 3 | 无并发闸门 | 45 个文件同时上传，合并/删除/写盘互相叠加 | 峰值压力不可控，磁盘 IO 与内存被抢 |
| 4 | `server.requestTimeout` 默认 300s | 合并排队时响应可能超 5 分钟 | 请求被 Node 误杀（408） |
| 5 | `keepAliveTimeout` 默认 5s | 几十个大文件反复重建连接 | 连接churn，加重握手与调度负担 |

## 二、修复措施

| 措施 | 实现 | 效果 |
|---|---|---|
| **流式异步合并** | `createReadStream` → `createWriteStream`（`pipe(..., {end:false})`）替代 `readFileSync/writeSync` | 事件循环不再被合并独占 |
| **合并闸门** | 同时最多 2 个合并（`DL_MERGE_CONCURRENCY`，默认 2），其余排队 | 消除多文件并发合并风暴 |
| **异步清会话** | `fs.promises.rm` 替代 `fs.rmSync` | 删除 GB 级目录不阻塞 |
| **存活探针** | `GET /healthz`：不触碰磁盘，返回 pid/uptime/rss/mergeActive/mergeQueued | 供巡检与人工判活 |
| **探活巡检** | `dl-healthcheck.timer` 每 30s 请求 `/healthz`，连续 2 次失败 → 自动重启 | **卡死约 60 秒内自愈**（进程活着但卡死时唯一有效手段） |
| **事件循环滞后监控** | 单次阻塞 >5s 记 `[warn] 事件循环阻塞 Xms` | 再发生卡死可追溯现场（`DL_LAG_WARN_MS` 可调） |
| **顶层异常日志** | `uncaughtException`（记日志后 `exit(1)` 交 systemd 拉起）/`unhandledRejection`（记日志） | 不再出现「日志只剩启动行、无法定位」 |
| **超时放宽** | `requestTimeout=30min`、`headersTimeout=60s`、`keepAliveTimeout=30s` | 合并排队不被误杀 + 连接复用 |
| **前端并发闸门** | 同时最多 3 个文件在传（`UP_PARALLEL`），其余显示「排队中…」 | 服务端压力可控，单个文件更快传完 |

### 踩坑：Node 无法实现 systemd 的 sd_notify

原计划用 `Type=notify` + `WatchdogSec`（原生看门狗，能精确发现事件循环卡死）。实测 **Node 的 `dgram` 只支持 `udp4/udp6`，不支持 `unix_dgram`**，`dgram.createSocket('unix_dgram')` 直接抛 `ERR_SOCKET_BAD_TYPE`；异常被 try/catch 吞掉后 `READY=1` 永不送达，systemd 报 `start operation timed out` 启动失败。
→ 改为**外部探活巡检**：效果等价（事件循环卡死时 `/healthz` 必然超时）、零额外依赖，故最终采用 `Type=simple` + 巡检定时器双层兜底（systemd `Restart=always` 负责崩溃重启，巡检负责卡死重启）。

## 三、验证结果

1. **并发合并压测**（3 个 160MB 文件同时 complete）：
   - 3 个合并 419ms 全部完成，HTTP 200,200,200；
   - 合并期间 `/healthz` 探测 16 次：**最大延迟 3ms、平均 1.4ms**（旧实现会阻塞整个合并时长）；
   - 观察 `mergeActive` 峰值 = 2、且出现排队 → **闸门生效**；
   - 3 个产物与源数据**字节级一致** ✅
2. **卡死自愈实测**：`kill -STOP` 主进程模拟事件循环永久卡死
   - 卡死期间 `/healthz` → HTTP 000（超时，符合预期）
   - 22:08:22 巡检第 1 次失败 → 22:08:55 第 2 次失败并重启 → **22:09:00 恢复（约 67 秒）**，MainPID 2489497 → 2490400 ✅
3. **功能回归**：上传全套 23 项测试 **23/23 通过**（跳过去重、改名保存、分块断点续传、子目录落盘、页面注入）✅
4. **前端**：页面内联 JS 语法校验通过，并发闸门与跳过复选框均已生效 ✅
5. **在传资产无损**：45 个分块会话、65GB 全部在册；已完整落盘 13 个模型 ✅

## 四、日常运维

```bash
systemctl status dl-server                      # 服务状态
systemctl status dl-healthcheck.timer           # 巡检定时器
curl -s --noproxy '*' http://127.0.0.1:8899/healthz   # 健康检查（含合并队列）
tail -f ~/Downloads/dl-server.log               # 日志（现为 append；关注 [warn] 事件循环阻塞 / [fatal]）
```

可调参数（写在 `/etc/systemd/system/dl-server.service` 的 `Environment=`）：
- `DL_MERGE_CONCURRENCY`（默认 2）：同时合并数
- `DL_LAG_WARN_MS`（默认 5000）：事件循环阻塞告警阈值
- 前端 `UP_PARALLEL`（默认 3）：同传文件数上限，位于 `download-server.js` 的 `UPLOAD_JS`

## 五、相关文件

- 服务端：`~/Downloads/download-server.js`（`streamMerge`/`acquireMergeSlot`/`completeUpload`/`/healthz`/`watchLoopLag`/前端 `schedule`+`pump`）
- 巡检：`~/Downloads/dl-healthcheck.sh`、`dl-healthcheck.service`、`dl-healthcheck.timer`
- 单元：`~/Downloads/dl-server.service`（→ `/etc/systemd/system/`）
- 测试脚本：`/tmp/up-test.mjs`（功能 23 项）、`/tmp/concurrency-test.mjs`（并发压测）
