记一次跨云网络故障排查:TLS 握手间歇性超时

从「消息发不出去」到「真相是 DNS 轮询到坏 IP」

Posted by Marlin on July 16, 2026

⚠️ AI 生成标识 · 本文由 AI Agent 协助撰写,人类作者审核发布。

起因

我把服务器从阿里云迁移到了京东云,顺便把 AI 助手从 v1.x 升级到了 v2.0.0。结果发现助手回复消息变得极其缓慢——有时候一条消息要等 5 分钟才收到回复。

一开始我以为是大版本升级导致的 Bug,或者模型推理变慢了。但排查后发现,问题跟代码无关,跟模型无关,纯粹是网络问题。

排查过程

第一轮:直觉陷阱

我第一个反应是”模型推理变慢了”——每次助手回复前都要调大模型,多轮 tool call 串行执行,每次推理都要重新等模型返回,加起来自然就慢了。

但这个推理被一个关键信息推翻了:之前从来没这么慢过,换服务器 + 升版本后才出现的。 如果仅仅是模型推理慢,那应该在旧服务器上也一样慢才对。

第二轮:看日志

我让助手去查运行日志,发现了大量这样的错误:

1
2
ConnectionTimeoutError: Connection timeout to host 
https://api.sgroup.qq.com/v2/users/.../messages

这不是模型推理慢,而是消息发不出去! 助手早就推理完了,但往 QQ 的 API 发消息时连接超时了。

第三轮:拆解网络

为了搞清楚到底是哪一步出了问题,我设计了几个测试来隔离变量:

测试 目的 方法
测试1 检查 TCP 连接 纯 socket 连接,不涉及 TLS
测试2 检查 TLS 握手 socket + ssl 完整握手
测试3 走代理对比 通过代理服务器访问
测试4 排除软件干扰 用 curl 空环境变量直连

第四轮:实锤

结果非常清晰:

1
2
3
腾讯QQ API:  TCP=0.007s ✅  →  TLS=❌ 8.0s 超时
腾讯官网:    TCP=0.025s ✅  →  TLS=0.043s ✅
百度:        TCP=0.010s ✅  →  TLS=0.040s ✅

TCP 三次握手 7 毫秒就通了,但在 TLS 握手阶段卡死了 8 秒。

更离谱的是,这个问题是间歇性发作的——同一台服务器,同一套代码,同一个 API,有时候 50 次测试全部成功(平均 0.08s),有时候 20 次测试失败 18 次(成功率 10%)。

转折:真相比想象的更简单

到这里我一度认为是跨云网络的中间设备(防火墙/DPI)对 TLS ClientHello 包做深度检测导致的随机丢包。直到我随手跑了个 ping——结果让我大跌眼镜。

ping 命令揭开真相

1
2
3
$ ping api.sgroup.qq.com
PING ins-npwk3wle.ias.tencent-cloud.net (175.27.13.145)
→ 100 packets transmitted, 25 received, 75% packet loss 🔴

75% 的 ICMP 丢包率? 这比 TLS 握手还夸张。ping 是纯 ICMP 包,连端口都不需要——如果 ping 都丢 75%,那别的协议怎么可能正常?

但等等,我再跑一次试试:

1
2
3
$ ping -w 120 api.sgroup.qq.com
PING ins-npwk3wle.ias.tencent-cloud.net (101.91.19.174)
→ 41 packets transmitted, 41 received, 0% packet loss ✅

同一个域名,换了一个 IP,马上 0% 丢包? 这说明 api.sgroup.qq.com 背后不止一台服务器。DNS 轮询每次可能返回不同的 IP。

我又多跑了几次 ping,看看还会解析到什么地址。反复试下来,目前发现了三个不同的 IP:

IP 丢包率 RTT
175.27.13.145 76% 🔴 4.6ms
101.91.19.174 0% ✅ 21.5ms
61.151.231.145 0% ✅ 27.2ms

通的时候 RTT 只有 4.6ms,但 10 个包里只回来 2~3 个——这是典型的 中间路由设备选择性丢包,而不是服务器本身挂了。

结论:api.sgroup.qq.com 的 DNS 轮询返回三个 IP,其中一个(175.27.13.145)有 76% 的 ICMP 丢包,其余两个完全正常。

间歇性发作的原因

1
2
3
4
DNS 轮询:
  第一次 → 175.27.13.145 → ❌ 76% 丢包 → TLS 超时
  第二次 → 101.91.19.174  → ✅ 0% 丢包 → 正常
  第三次 → 61.151.231.145 → ✅ 0% 丢包 → 正常

这就是为什么问题一会儿好一会儿坏——取决于 DNS 轮询到了哪个 IP。运气好命中正常 IP,服务秒通;运气不好命中坏 IP,TLS 握手超时 10 秒。

我之前还做过整夜监控(653 次测试,6.5 小时),结果成功率在 25%~96.8% 之间大幅波动。现在想来,其实就是 DNS 轮询到坏 IP 的比例在变化罢了——根本不是”中间设备间歇性抽风”。

解决方案

知道了根因,解决就简单了。

临时方案:修改 /etc/hosts

把 api.sgroup.qq.com 固定指向正常的 IP 即可:

1
101.91.19.174  api.sgroup.qq.com

改完后服务立刻恢复,QQ Bot 再也没有发生过 TLS 握手超时。

但这不是长久之计——腾讯如果轮换 IP,101.91.19.174 可能某天就失效了。不过对于个人项目来说,撑几个月问题不大。

给腾讯云提工单

我也给腾讯云提交了工单,附上了完整的 ping 日志和 TLS 握手测试数据,请求排查 175.27.13.145 的高丢包问题,或者将其从 DNS 轮询中摘除。

关键知识:TCP 三次握手 vs TLS 握手

虽然这篇文章讲的是一次实际排障,但过程中的 TCP/TLS 知识值得单独拎出来讲清楚。

TCP 三次握手(传输层)

当你的程序调用 socket.connect() 时,操作系统内核会帮你完成三次握手:

1
2
3
4
5
6
7
8
9
客户端                         服务器
  |        SYN (seq=x)          |
  | ──────────────────────────> |  ① 客户端:喂,我要连你
  |     SYN+ACK (seq=y, ack=x+1) |
  | <────────────────────────── |  ② 服务器:收到,我能连
  |     ACK (ack=y+1)           |
  | ──────────────────────────> |  ③ 客户端:好的,连上了
  |
  ▼ 连接建立成功 ✅

关键特点:

  • 在操作系统内核中完成,纯内核态,不涉及用户态程序
  • 不涉及证书、不涉及加密、不涉及任何业务逻辑
  • 耗时极短,通常 0.00x~0.0x 秒
  • 只要 IP 可达、端口开放,基本秒通

TLS 握手(安全层)

TCP 连接建立好之后,在 TCP 的通道上再走一套流程,目的是加密通信。这完全在用户态完成,由 SSL 库(如 OpenSSL)实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
客户端                         服务器
  |  ClientHello                 |
  |  (TLS版本, 密码套件列表)      |
  | ──────────────────────────> |  ① 客户端:我要加密,支持这些算法
  |  ServerHello + Certificate   |
  |  (选定算法 + 服务器证书)      |
  | <────────────────────────── |  ② 服务器:用AES-GCM,这是我的证书
  |  密钥交换                    |
  | ──────────────────────────> |  ③ 客户端:验证证书,生成密钥
  |  Finished                    |
  | <────────────────────────── |  ④ 服务器:密钥确认,开始加密
  |
  ▼ 安全通道建立 ✅

关键特点:

  • 在用户态完成,涉及 SSL 库的复杂运算
  • 需要证书验证、加密算法协商、密钥交换
  • 涉及多个网络往返(RTT)
  • 正常耗时 0.0x~0.x 秒
  • 底层依赖 TCP 连接,如果 TCP 都不通,TLS 就更不可能通

为什么要同时用 ping 和 curl 来排查

这次排障让我深刻理解了一个道理:不同的工具测的是不同层面的东西。

工具 测什么 排查意义
ping ICMP 协议,网络层连通性 排查路由丢包
curl HTTP over TCP/TLS,应用层 验证完整业务链路
mtr ICMP 逐跳路径 定位哪一跳丢包
Python socket 直连 TCP 连接(不加密) 隔离 TLS 的影响

如果 DNS 轮询到了坏 IP,所有工具都会挂——ping、curl、socket 全都不通。但如果 DNS 轮询到了好 IP,所有工具都正常。这就是”间歇性”问题的本质——不是网络本身波动,而是你每次请求去到的服务器不一样。

总结

这次排查最大的教训是:别预设根因,先用最简单的工具试一下。

  • ❌ 直觉一:版本升级有 Bug(看日志排除)
  • ❌ 直觉二:模型推理慢(对比测试排除)
  • ❌ 直觉三:中间设备丢 ClientHello 包(似乎合理但错了)
  • ✅ 真相:DNS 轮询到一个 76% 丢包的后端 IP

如果我早两个小时跑 ping,就不用做那么多复杂的 TLS 握手套接字测试了。

另一个教训是:跨云架构中 DNS 轮询是一个隐性风险点。 后端某台服务器出问题时,客户端不会收到错误提示,而是每隔几次请求随机被路由到故障节点上。这种问题最难排查——因为日志里没有任何关于”这次解析到哪个 IP”的记录,你看到的只是”有时候超时”。

顺便说一句,如果你也想跑类似测试,这个 Python 脚本纯标准库,无需 pip install,任何 Linux 服务器上都能直接用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python3
"""TLS 握手监控 + DNS 解析对比"""
import socket, ssl, time

def test_ip(ip, hostname):
    t0 = time.time()
    try:
        sock = socket.socket()
        sock.settimeout(10)
        sock.connect((ip, 443))
        ctx = ssl.create_default_context()
        tls = ctx.wrap_socket(sock, server_hostname=hostname)
        tls.close()
        return f"✅ {time.time()-t0:.3f}s"
    except Exception as e:
        return f"❌ {time.time()-t0:.1f}s {type(e).__name__}"

host = "api.sgroup.qq.com"
print(f"DNS 解析 {host}:")
ips = [ip[4][0] for ip in socket.getaddrinfo(host, 443)]
for ip in ips:
    print(f"  {ip}: {test_ip(ip, host)}")

运行一次就能看到当前 DNS 解析到了哪些 IP,以及哪些是健康的。