Skip to content

[BUG] nodeget-server 持续吃满 100% CPU(tokio runtime + JS runtime 间歇性死循环 + CLOSE_WAIT 泄漏) #137

@cold-sword

Description

@cold-sword

环境

  • 版本: 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 条相关日志行,每次执行:

  1. 起 JS runtime(AsyncRuntime + AsyncContext
  2. 建立到 Telegram Bot API (api.telegram.org) 的 HTTPS 连接
  3. 执行短轮询获取 updates
  4. 连接没有正确回收

Telegram Bot 的标准做法是 long-polling(timeout=30s 的长连接),不应该每 10 秒用 cron 触发一次短轮询。

相关代码路径: server/src/crontab/mod.rsrun_jobjs_workerserver/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.rscrontab_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 泄漏)

复现方式

  1. 部署 nodeget-server 并配置多个 TCPing cron job(每个绑定多个 agent)
  2. 启用 tg-bot-worker(check-online-status cron)
  3. 运行 8-12 小时后 CPU 开始攀升至 100% 并稳定
  4. docker restart nodeget-server 可临时恢复,但 12h 后重现

建议修复方向

  1. tg-bot-worker: 改用 Telegram Bot long-polling 模式(timeout=30s),而不是每 10 秒 cron 触发短轮询
  2. JS runtime: JS runtime pool 应该在首次初始化后复用,而不是每次 cron 执行都重新 init(或至少缓存一段时间)
  3. HTTP 连接管理: JS runtime 侧的 HTTP 客户端需要在请求完成后正确关闭 socket,避免 CLOSE_WAIT 堆积
  4. Cron dispatch 合并: 对于同一 cron job → 多个 agent 的场景,可以批量 insert task 记录 + 批量发送事件,减少单次 dispatch 的数量
  5. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions