~/Downloads/download-server.js)一个空闲的已建立连接本身不消耗 CPU:没有数据可读、没有事件可派发,epoll 不会持续唤醒它。所以「7 条 ESTAB 连接 + CPU 94%」这个组合里,CPU 94% 只可能来自别处——事件循环当时正被独占/空转,于是:
那 7 条连接是受害者。上一轮已定位并修复的最可能元凶正是同步合并(readFileSync(64MB) + writeSync 逐块合并数 GB 文件):它同时解释了三件事——CPU 打满、事件循环停摆(连接全部僵住)、持续时间与合并体积成正比。修复后实测:3 个文件并发合并期间 /healthz 最大延迟 3ms。
结论:不要把「连接多/连接挂着」当成卡死的原因去治;要治的是「谁占住了事件循环」。
排查确认服务端此前没有任何读空闲判定:
server.timeout 未设置(Node 默认 0 = 永不超时)req.on('aborted') 未处理因此客户端掉线/关页/WiFi 闪断/网卡切换后,TCP 不会立刻释放,服务端就把这些「ESTAB 但零吞吐」的连接永久挂着,并残留写了一半的 .part。
| 层 | 机制 | 参数 |
|---|---|---|
| 1 | 连接级 TCP keepalive + setNoDelay | setKeepAlive(true, 30000) |
| 2 | 每 15s 巡检,销毁「上传中且超过阈值无数据」的连接 | DL_STALE_UPLOAD_MS(默认 120000ms) |
| 3 | 请求 error/close 时清理未完成的 .part(避免坏块污染续传会话) | — |
关键设计:只杀「上传中」的连接(socket 标记 kind='upload'),任何新请求开始即重置为 idle,因此长时间下载、WMV 转码、Office 预览等不会被误杀。
GET /healthz 新增连接维度: {"ok":true,"pid":…,"uptime":…,"rssMB":…,"mergeActive":…,"mergeQueued":…,"sockets":N,"uploadSockets":N,"staleUploads":N}[warn] 事件循环阻塞 8432ms(阈值 5000ms) | 套接字 7(上传中 6) | mergeActive=2 mergeQueued=1 | rss=812MB —— 再出现 94% CPU,日志会直接指出当时卡在什么状态。dl-healthcheck.timer 在服务真正卡死时 60 秒内自动重启(实测 67s 恢复)。用一个客户端发出 /api/upload/chunk 请求头 + 256KB body 后停住不发(不发 FIN、不再发数据,制造「ESTAB 零吞吐」):
起始:sockets=2 uploadSockets=1 staleUploads=0 半成品.part存在=true
t+110s … uploadSockets=1 staleUploads=0 半成品.part存在=true
t+120s … uploadSockets=1 staleUploads=1 半成品.part存在=true ← 超过 120s 阈值
t+122s 客户端侧:连接已断开(ECONNRESET) ← 服务端主动判死
t+130s sockets=1 uploadSockets=0 staleUploads=0 半成品.part存在=false ← 垃圾已清理
服务端日志证据:
[warn] 清理僵死上传连接 127.0.0.1:53990(122s 无数据)→ 断开并丢弃未完成分块
→ 服务端现在会自己判死并清理,不再需要人工重启。
active / active它是在线的上传客户端(ping 1ms,MAC 00:e0:70:b9:41:97)。注意:该主机同时出现在本机的有线 enp3s0f1 与无线 wlp4s0 两个接口上 —— 这条线索很重要:
笔记本在有线/无线之间切换(或 WiFi 漫游到另一个 AP)时,旧接口上的 TCP 连接在服务端不会收到 FIN,会以 ESTAB 状态残留且零吞吐 —— 与「7 条挂着的连接」完全吻合。这类连接服务端必须自己能判死,本次的三层兜底正是针对这种场景。
可选的加固(当前未改,需要再快可自行启用):内核级更快回收
sudo sysctl -w net.ipv4.tcp_keepalive_time=120 net.ipv4.tcp_keepalive_intvl=30 net.ipv4.tcp_keepalive_probes=3
(约 2.5 分钟由内核判定对端消失;应用层已有 30s keepalive + 120s 空闲判死,通常够用。)
可调参数(/etc/systemd/system/dl-server.service 的 Environment=):
DL_STALE_UPLOAD_MS(默认 120000):上传连接无数据多久判死;网络抖动大/客户端可能长时间挂起时可调大DL_MERGE_CONCURRENCY(默认 2)、DL_LAG_WARN_MS(默认 5000)curl -s --noproxy '*' http://127.0.0.1:8899/healthz # 连接数/上传连接/合并队列/内存
tail -f ~/Downloads/dl-server.log # 关注 [warn] 事件循环阻塞 / 清理僵死上传连接
systemctl status dl-server dl-healthcheck.timer
~/Downloads/download-server.js(liveSockets/markSocket/staleSweeper/watchLoopLag 现场记录//healthz 连接计数/上传请求中止清理)~/Downloads/dl-healthcheck.sh + .service + .timer/tmp/stale-conn-test.mjs(僵死连接复现)、/tmp/up-test.mjs(功能 23 项)、/tmp/concurrency-test.mjs(并发合并压测)