Skip to content

修复:避免多 tunnel 实例心跳导致代理节点事件重复入库#673

Open
zbsdsb wants to merge 1 commit into
fawney19:mainfrom
zbsdsb:fix/dedupe-proxy-node-tunnel-errors
Open

修复:避免多 tunnel 实例心跳导致代理节点事件重复入库#673
zbsdsb wants to merge 1 commit into
fawney19:mainfrom
zbsdsb:fix/dedupe-proxy-node-tunnel-errors

Conversation

@zbsdsb

@zbsdsb zbsdsb commented Jul 14, 2026

Copy link
Copy Markdown

现象

proxy_node_events 会在短时间内写入大量重复的 tunnel_err 记录,事件表持续膨胀。

实际数据中,同一个时间点可以出现十几万条记录,但 detailevent_metadata 只有少量不同值;同时,节点上报的累计 error_events_total 远小于数据库中的事件行数。这说明同一批近期 tunnel 错误被心跳重复持久化,并不是真实发生了数百万次独立错误。

代理节点仍然可以显示在线,因为问题发生在心跳指标和错误事件的累计逻辑,不代表 tunnel 连接一定离线。

触发条件

核心触发条件是:多个 aether-tunnel 进程共用同一个代理节点 ID

例如同一台机器同时通过 systemd 和 Docker 启动 tunnel:

  1. 两个进程各自维护独立的累计计数器。
  2. 两个进程使用同一个 node_id 交替发送心跳。
  3. 服务端看到累计计数器下降时,将其视为计数器重置,并把当前累计值当作本次新增值。
  4. 下一个高计数器实例再次上报时,又会产生一笔较大的 delta。
  5. recent_tunnel_errors 根据这个错误的 delta 被重复写入 proxy_node_events

相同 heartbeat 重试、heartbeat ID 回退或乱序,也可能造成相同事件被重复处理。

根因

新版 tunnel 客户端已经在心跳顶层发送:

  • heartbeat_session_id:标识当前 tunnel 进程/心跳任务会话。
  • heartbeat_id:同一 session 内严格递增,重试时复用原 ID。

但 gateway 的 InternalTunnelHeartbeatRequest 没有读取 heartbeat_session_id,Serde 会把该字段当作未知字段丢弃。数据层因此只能看到累计计数器,无法判断两次心跳是否来自同一个 tunnel 进程,也无法可靠识别 session 切换、重放和乱序。

修复方案

本 PR 直接复用 tunnel 已经发送的 heartbeat cursor,不新增数据库字段,也不需要数据库迁移:

  1. gateway 读取并校验可选的 heartbeat_session_id
  2. heartbeat_session_idheartbeat_id 写入代理节点 metadata。
  3. 只有 previous/current 属于同一 session,且当前 heartbeat_id 严格大于上一次时,才计算累计指标 delta。
  4. 以下情况作为新的计数基线,累计 delta 设为 0,并且不写入 recent_tunnel_errors
    • heartbeat session 切换;
    • 相同 heartbeat ID 重放;
    • heartbeat ID 回退或乱序;
    • 首次从无 cursor 的旧 metadata 切换到有 cursor 的新 metadata。
  5. 基线心跳仍会正常记录在线状态、活跃连接数和心跳 RTT。
  6. previous/current 都没有 cursor 时继续使用原有逻辑,兼容旧版 tunnel 客户端。

这样即使两个 tunnel 实例使用同一个节点 ID 交替上报,也不会再把各自的累计计数器反复计算成新增事件。

验证

已覆盖以下场景:

  • 同一 session、heartbeat ID 递增时正常计算 delta。
  • session A/B 切换时所有累计 delta 为 0。
  • 相同 heartbeat ID 重放时不重复写事件。
  • heartbeat ID 回退/乱序时不重复写事件。
  • 首次采用 cursor 时作为基线。
  • 旧客户端无 cursor 时保持原有行为。
  • gateway 能保存 cursor,并拒绝空白或过长的 session ID。

本地验证结果:

cargo fmt --all --check
通过

cargo clippy -p aether-data --all-targets -- -D warnings
通过

cargo test -p aether-data builds_tunnel_metrics_sample --lib
5 passed, 0 failed

cargo test -p aether-data repository::proxy_nodes --lib
19 passed, 0 failed

gateway 的 HTTP 回归测试已随 PR 提交,由仓库 Linux CI 继续执行。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant