Skip to content

feat(transport): DoH 客户端缺少透明重试与健康恢复机制(对比 TcpMuxClient),节点抖动后死连接无限复用导致持续超时刷屏 #41

Description

@github-xihe

问题描述

DohClient(src/engine/transport.rs)与 TcpMuxClient 相比,缺少连接自愈机制:当代理节点抖动导致 DoH 连接失效后,连接池中的死连接被无限复用,每次请求 3 秒超时并反复重试,日志持续刷屏,只能通过重启 kixdns 恢复。

对比(同文件内实现差异)

TcpMuxClient(有完整自愈)

  • 透明重试:复用连接失败时,用新连接重试一次(send()is_reused 检查 → send_attempt 失败 → 新建连接重试,预留至少 1.5s 预算)
  • 错误计数重置record_error() 连续错误 ≥ 阈值 → reset() 连接并清零计数
  • 连接老化check_connection_health() 检查 max_age → 超龄 reset
  • 空闲超时idle_timeout → 超时 reset
  • 健康检查:30 秒间隔执行

DohClient(当前实现,transport.rs 1218-1271 行)

pub async fn send(&self, packet, upstream, timeout_dur) -> anyhow::Result<Bytes> {
    // reqwest client 复用(pool_idle_timeout=90s, pool_max_idle_per_host)
    let bytes = timeout(timeout_dur, async {
        let resp = req.send().await.context("doh request send failed")?;
        // ... 读响应体
    }).await.context("doh request timeout")??;
    Ok(bytes)
}
  • ❌ 无透明重试(复用连接失败直接返回错误)
  • ❌ 无错误计数/连接重置
  • ❌ 无连接老化检查
  • ⚠️pool_idle_timeout=90s:但后台刷新(background refresh)每 ~10 秒触发一次 DoH 请求,连接永不空闲,此超时几乎不生效

实际影响(实测复现)

kixdns e521751c5a(main 2026-08-11),代理节点(dae HK 组)抖动期间:

upstream call failed, waiting for others upstream=doh:8.8.8.8/dns-query error=doh request timeout elapsed_ns=3001xxx
upstream call failed, waiting for others upstream=doh:1.1.1.1/dns-query error=doh request timeout elapsed_ns=3001xxx
  • 触发域名:www.google.comoauth2.googleapis.com(谷歌系,走 doh-fallback)
  • 频率:每 ~10 秒一对,持续 3+ 小时(1396 条)
  • 节点抖动恢复后仍持续刷屏(连接池死连接未清理),重启 kixdns 才恢复
  • 手动 curl(每次都新建连接)在节点恢复后立即成功——证实是连接池复用问题,非网络问题

期望行为

DohClient 具备与 TcpMuxClient 对等的自愈能力:

  1. 透明重试:复用连接请求失败(传输错误/超时)时,用新连接重试一次
  2. 连接池重置:连续错误超过阈值时重建 reqwest Client(等价于 reset 连接池)

修复建议

请上游自行评估实现方案。

参考线索:

  • TcpMuxClient::send()(transport.rs 800-850 行)的透明重试模式可直接借鉴
  • reqwest 连接池无法单独销毁某条连接,可考虑错误计数达到阈值时整体重建 DohHttpClientDohClient::new() 已封装构建逻辑)
  • 或调整 pool_idle_timeouthttp2_adaptive_window 等参数缓解(不治本)

环境

  • kixdns e521751c5a(main,2026-08-11)
  • dae 代理(dip(8.8.8.8, 1.1.1.1) -> HK),DoH 经代理访问 8.8.8.8/1.1.1.1(直连不可行:UDP 53 被污染、TCP 53/443 被墙)
  • 触发场景:代理节点抖动 + cache_background_refresh: true

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions