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 排障里最常看到的。客户端和服务端在协商加密参数时谈崩了,直接返回错误。我习惯把握手失败分成三个阶段:
- ClientHello – ServerHello 阶段:客户端发支持的密码套件,服务端挑一个。如果服务端一个都看不上,握手直接终止,错误码通常是
SSL_ERROR_NO_CIPHERS。 - 证书验证阶段:服务端发证书,客户端验证。我见过最多的问题是“证书过期”和“主机名不匹配”。
- 密钥交换阶段: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。实际排查时,先看客户端自己的错误日志,大概率能对应到上面的系统错误码。
三种现象的关系
你想想看,连接挂起、握手失败、超时错误码并不是孤立的。连接挂起久了,系统就会抛超时错误码;握手失败通常不会挂起,而是直接报错。为了理清它们之间的关系,我画了一张结构图:
从图上能直观看到:连接挂起是“默默等待”的状态,一旦超时,系统就抛出相应错误码;而握手失败更像“当场翻脸”,不等超时就报错。我建议初学者遇到超时先抓包,区分是挂起导致的超时,还是握手失败瞬间产生的超时——这决定了你下一步该查 TCP 层还是 TLS 层。
我自己排查时总记住一个口诀:“挂起先看 backlog,握手失败看证书,超时错误找配置”。这三个口诀能覆盖 80% 的场景。剩下的 20% 嘛……嗯,就靠经验和 strace 了。