3. 常见的TLS连接超时现象

TLS 连接出问题,归根结底就跑不出三种现象:连接挂起、SSL 握手失败和超时错误码。我干了这么多年网络排障,遇到最多的就是这三类。下面一个个拆开说,顺便把我在项目里踩过的坑也列出来。

3.1 连接挂起(Connection Hanging)

连接挂起,说白了就是客户端和服务端之间 TCP 连上了,但之后数据流卡死,谁也不发消息。两边都在干等。我遇到过不少同事觉得“既然 TCP 已经 ESTABLISHED,那就没问题”——其实大错特错。

现象特征

  • 客户端 connect() 成功返回,但后续 read() / write() 阻塞。
  • 服务端 accept() 正常,但 SSL_accept() 一直不返回。
  • 连接既不主动断开,也不传输数据,就像死了一样。

为什么会这样?

  • 服务端半连接队列满:比如你发起了大量 TCP 连接,但服务端来不及调用 accept(),新连接被放置在半连接队列里,后续 TLS 握手消息根本到不了应用层。
  • 服务端进程僵死:我记得有一次线上告警,客户端连上去就卡住。查了一圈,发现服务端进程进入了 D 状态(不可中断睡眠),所有新连接都在内核里排队。
  • 防火墙/中间件拦截:某个规则只允许了 TCP 三次握手,但后续数据包被丢弃。

避坑指南:我曾经遇到一个案子,服务端明明开着,客户端却一直挂起。我用了 tcpdump 抓包,发现服务端在三次握手后立即发了 RST。为什么?因为服务端的 listen backlog 设置得太小,新连接被内核直接拒绝。调整 net.core.somaxconn 后立即解决。记住:挂起不一定是超时,也可能是对端悄悄关了连接。

3.2 SSL 握手失败(Handshake Failure)

SSL 握手失败,这是 TLS 排障里最常看到的。客户端和服务端在协商加密参数时谈崩了,直接返回错误。我习惯把握手失败分成三个阶段:

  1. ClientHello – ServerHello 阶段:客户端发支持的密码套件,服务端挑一个。如果服务端一个都看不上,握手直接终止,错误码通常是 SSL_ERROR_NO_CIPHERS。
  2. 证书验证阶段:服务端发证书,客户端验证。我见过最多的问题是“证书过期”和“主机名不匹配”。
  3. 密钥交换阶段:RSA 或 ECDHE 交换过程中,对方无法解密。

实战经验

我记得有一次排查支付接口,客户端总是报 handshake failure。用 openssl s_client 测试,发现服务端返回的证书链不完整——中间证书缺失。客户端是 Java 环境,默认不信任那些自签名的中间 CA。结果就是握手失败。

另外,SNI(Server Name Indication) 也是常见雷区。如果服务端配置了多个虚拟主机,但客户端没发 SNI,服务端可能返回默认证书,导致证书和域名不匹配。

避坑小技巧:我习惯在 tcpdump 里抓 TLS 握手包,重点关注 Alert 帧里的描述。比如 handshake failure(Alert 40)、bad certificate(Alert 42)。这些描述是定位问题的关键。

3.3 超时错误码归纳

超时错误码是系统抛出的最后一道防线。不管是连接挂起还是握手失败,最终都会表现为某个超时异常。下面是我汇总的常见错误码对照表:

错误码(Linux) 含义 典型场景
ETIMEDOUT (110) 连接超时 TCP 三次握手没完成,或者握手后没有收到响应。
EHOSTUNREACH (113) 主机不可达 路由表找不到目标,或者防火墙直接丢弃 SYN。
ECONNREFUSED (111) 连接被拒绝 目标端口没打开,或者服务端主动发了 RST。
ECONNRESET (104) 连接被重置 对端在握手过程中突然关闭连接。
EPIPE (32) 管道破裂 已断开的连接上继续写入数据。

注意:这些错误码只是 TCP 层的。TLS 层还有自己的错误,比如 OpenSSL 里 SSL_ERROR_SYSCALL 通常表示底层 socket 异常,SSL_ERROR_SSL 表示协议本身错误。我建议结合 errno 和 SSL_get_error() 一起看,才能准确判断问题在哪。

另外,很多框架(如 nginx、haproxy)会重新封装超时,比如 upstream timed out 或 SSL_read timeout。实际排查时,先看客户端自己的错误日志,大概率能对应到上面的系统错误码。

三种现象的关系

你想想看,连接挂起、握手失败、超时错误码并不是孤立的。连接挂起久了,系统就会抛超时错误码;握手失败通常不会挂起,而是直接报错。为了理清它们之间的关系,我画了一张结构图:

TLS连接超时现象分类 TLS连接超时 连接挂起 TCP连接无响应 SSL握手失败 证书/密码套件不匹配 超时错误码 系统或TLS层超时 说明 连接挂起最终会演变为超时错误码;握手失败则直接报告错误。 三者关系:挂起 → 超时错误码;握手失败 → 直接报错(不经过挂起)。

从图上能直观看到:连接挂起是“默默等待”的状态,一旦超时,系统就抛出相应错误码;而握手失败更像“当场翻脸”,不等超时就报错。我建议初学者遇到超时先抓包,区分是挂起导致的超时,还是握手失败瞬间产生的超时——这决定了你下一步该查 TCP 层还是 TLS 层。

我自己排查时总记住一个口诀:“挂起先看 backlog,握手失败看证书,超时错误找配置”。这三个口诀能覆盖 80% 的场景。剩下的 20% 嘛……嗯,就靠经验和 strace 了。