环境
- 版本: Docker
nodeget-server:latest(13 小时前重启过)
- 主机: 4 核 VPS,Ubuntu
- 运行时间: 13 小时即复现(重启后 CPU 立降,半天内回到 100%)
现象
CONTAINER CPU %
nodeget-server 101.24% ← 稳定吃满一个核
SPID %CPU TIME COMMAND
3312614 35.0 4:43:55 nodeget-server ← 唯一热线程,13 小时跑了近 5 小时 CPU
3312613 1.0 0:11:29 nodeget-server ← 其他线程几乎不动
根因分析(三合一)
① tg-bot-worker 短轮询 (check-online-status cron)
cron_expression = */10 * * * * → job_name = check-online-status → js_script = tg-bot-worker
每 10 秒执行一次 tg-bot-worker,每次都要 重新初始化 JS runtime pool(日志可见 initializing global JS runtime pool 每 10 秒出现一次)。过去 10 分钟触发 180 条相关日志行,每次执行:
- 起 JS runtime(
AsyncRuntime + AsyncContext)
- 建立到 Telegram Bot API (
api.telegram.org) 的 HTTPS 连接
- 执行短轮询获取 updates
- 连接没有正确回收
Telegram Bot 的标准做法是 long-polling(timeout=30s 的长连接),不应该每 10 秒用 cron 触发一次短轮询。
相关代码路径: server/src/crontab/mod.rs → run_job → js_worker → server/src/js_runtime/runtime_pool.rs
② HTTP 连接泄漏 — CLOSE_WAIT × 3
tcp 25 0 172.19.0.4:51192 → 172.67.183.104:443 (Cloudflare/Telegram) CLOSE_WAIT
tcp 25 0 172.19.0.4:51198 → 172.67.183.104:443 (Cloudflare/Telegram) CLOSE_WAIT
tcp 25 0 172.19.0.4:51190 → 172.67.183.104:443 (Cloudflare/Telegram) CLOSE_WAIT
Telegram 服务端已经关闭了连接(FIN 已收到),但 NodeGet 的 JS runtime HTTP 客户端没有 close socket,导致 3 个 CLOSE_WAIT。每个连接上还有 25 字节 Recv-Q 未读取的数据。
tokio 的事件循环可能在这些已关闭的 socket 上持续尝试读写,导致不必要的 CPU 开销。
③ Cron 调度风暴 — 564 次/分钟
这是第二个关键因素。1 分钟内 cron dispatch 了 564 个 task(平均 9.4 次/秒):
$ docker logs --since 1m nodeget-server | grep "task event sent to agent" | wc -l
564
TCPing 类监控任务(Telegram Bot API IPv4&6、Cloudflare DNS、广东移动、Peekabo CDN 等)绑定了多个 agent,每个 cron job 触发时要向每个 agent 各发一次 dispatch,导致 dispatch 数量 = jobs × agents 的乘积效应。
这些高频 dispatches 全部堆到 tokio runtime 上,加上 JS runtime 反复 init + 死连接轮询,最终表现为一个 tokio worker 线程空转吃满 CPU。
相关代码路径: server/src/crontab/task.rs → crontab_task() 为每个 agent uuid 逐一插入 task 记录并发送事件
证据汇总
| 指标 |
数值 |
| 容器 CPU |
101.24%(稳定) |
| 热线程 CPU 累计 |
4h43m(13h 运行中占比 36%) |
| tg-bot-worker 触发频率 |
*/10 秒(6 次/分钟) |
| JS runtime 重复初始化 |
6 次/分钟 |
| CLOSE_WAIT 连接数 |
3(到 Telegram API) |
| Cron dispatch 速率 |
9.4 次/秒(564/分钟) |
| FD 数量 |
37(非 FD 泄漏) |
复现方式
- 部署 nodeget-server 并配置多个 TCPing cron job(每个绑定多个 agent)
- 启用 tg-bot-worker(check-online-status cron)
- 运行 8-12 小时后 CPU 开始攀升至 100% 并稳定
docker restart nodeget-server 可临时恢复,但 12h 后重现
建议修复方向
- tg-bot-worker: 改用 Telegram Bot long-polling 模式(
timeout=30s),而不是每 10 秒 cron 触发短轮询
- JS runtime: JS runtime pool 应该在首次初始化后复用,而不是每次 cron 执行都重新 init(或至少缓存一段时间)
- HTTP 连接管理: JS runtime 侧的 HTTP 客户端需要在请求完成后正确关闭 socket,避免 CLOSE_WAIT 堆积
- Cron dispatch 合并: 对于同一 cron job → 多个 agent 的场景,可以批量 insert task 记录 + 批量发送事件,减少单次 dispatch 的数量
- tokio runtime: 考虑给 CPU-intensive 或 IO-busy 的 task 设置独立的 tokio runtime,避免单个 worker 线程被阻塞
日志样例
2026-05-30 01:22:50.979 crontab: triggering cron job job_id=9 job_name=check-online-status cron_expression=*/10 * * * *
2026-05-30 01:22:50.981 crontab: running js_worker job js_script_name=tg-bot-worker
2026-05-30 01:22:50.985 js_runtime: initializing global JS runtime pool
2026-05-30 01:22:40.953 crontab: task event sent to agent agent_uuid=eafc... task_id=7846876 cron_name=TCPing - Telegram Bot API IPv4&6
2026-05-30 01:22:40.954 crontab: task event sent to agent agent_uuid=6b67... task_id=7846877 cron_name=TCPing - Cloudflare DNS IPv4
... (每秒 9+ 条 dispatch)
环境
nodeget-server:latest(13 小时前重启过)现象
根因分析(三合一)
① tg-bot-worker 短轮询 (check-online-status cron)
每 10 秒执行一次 tg-bot-worker,每次都要 重新初始化 JS runtime pool(日志可见
initializing global JS runtime pool每 10 秒出现一次)。过去 10 分钟触发 180 条相关日志行,每次执行:AsyncRuntime+AsyncContext)api.telegram.org) 的 HTTPS 连接Telegram Bot 的标准做法是 long-polling(
timeout=30s的长连接),不应该每 10 秒用 cron 触发一次短轮询。相关代码路径:
server/src/crontab/mod.rs→run_job→js_worker→server/src/js_runtime/runtime_pool.rs② HTTP 连接泄漏 — CLOSE_WAIT × 3
Telegram 服务端已经关闭了连接(FIN 已收到),但 NodeGet 的 JS runtime HTTP 客户端没有 close socket,导致 3 个 CLOSE_WAIT。每个连接上还有 25 字节 Recv-Q 未读取的数据。
tokio 的事件循环可能在这些已关闭的 socket 上持续尝试读写,导致不必要的 CPU 开销。
③ Cron 调度风暴 — 564 次/分钟
这是第二个关键因素。1 分钟内 cron dispatch 了 564 个 task(平均 9.4 次/秒):
TCPing 类监控任务(Telegram Bot API IPv4&6、Cloudflare DNS、广东移动、Peekabo CDN 等)绑定了多个 agent,每个 cron job 触发时要向每个 agent 各发一次 dispatch,导致 dispatch 数量 = jobs × agents 的乘积效应。
这些高频 dispatches 全部堆到 tokio runtime 上,加上 JS runtime 反复 init + 死连接轮询,最终表现为一个 tokio worker 线程空转吃满 CPU。
相关代码路径:
server/src/crontab/task.rs→crontab_task()为每个 agent uuid 逐一插入 task 记录并发送事件证据汇总
复现方式
docker restart nodeget-server可临时恢复,但 12h 后重现建议修复方向
timeout=30s),而不是每 10 秒 cron 触发短轮询日志样例