一个半夜被叫起来处理的问题去年冬天做的一个数据采集项目现场大概两百多个节点通过无线链路往中心网关上报数据。上线跑了三天都正常第四天凌晨两点值班同事打电话说监控大屏上设备在线数从两百多掉到了三十几个而且剩下的还在持续掉。远程连上去看网关日志里密密麻麻全是同一类记录连接刚建立握手还没走完就报了超时然后连接被回收。注意是刚建立就超时不是连不上也不是连上之后跑一会儿才断。这两个现象背后的根因完全不同如果混在一起排查方向会跑偏。当时第一反应是网络抖动但ping网关本身延迟很低。第二反应是认证服务挂了但认证服务进程还在端口也在监听。折腾到凌晨四点最后定位到的是一个很隐蔽的资源竞争问题——连接建立路径上有一个共享的初始化步骤在高并发下被串行化了导致每个连接的实际可用时间窗口被压缩到几乎为零。这个坑让我意识到连接建立瞬间超时断开这个现象看起来是一类问题实际上至少能分成五六种截然不同的原因。下面按我这些年踩过的坑把分类和对应的判断方法整理一下。第一类握手阶段的时间预算被吃掉了这是最常见也最容易被误判的一类。连接建立不是一个瞬间动作它内部有若干个子步骤链路层确认、传输层握手、应用层协议协商、认证鉴权、会话初始化。每个步骤都有各自的超时计时器但这些计时器往往是独立配置的而且默认值可能非常激进。我见过一个典型的配置传输层握手超时设了3秒应用层认证超时设了2秒但认证步骤里有一次远程调用那个远程调用的超时是5秒。结果就是传输层握手很快完成了认证步骤刚开始应用层的2秒计时器就到期了连接被判定为超时并断开。从外部看就是刚建立就断。这类问题的现场特征是连接建立的成功率跟并发量强相关。低并发时一切正常并发一上来就大面积超时。因为每个步骤的耗时在低负载下是几十毫秒高负载下可能变成几百毫秒甚至秒级原本够用的时间预算就不够了。排查方法是在连接建立路径的每个子步骤打上时间戳记录从开始到完成的耗时分布。不要只看平均值要看P99和最大值。很多团队只监控了连接建立的总成功率没有拆分到子步骤所以一直找不到瓶颈在哪。连接建立时间线单次采样 t0.000 链路层就绪 t0.012 传输层握手开始 t0.089 传输层握手完成 t0.091 应用层认证开始 t0.095 认证内部远程调用发起 t2.093 应用层认证超时触发2秒预算耗尽 t2.095 连接被回收 t5.090 远程调用才返回但已经没人等了注意最后两行的时间差。远程调用实际上在5秒后才返回但连接在2秒时就已经被回收了。这个返回结果变成了一个孤儿如果代码里没有正确处理这种迟到返回还可能引发二次异常。第二类文件描述符或句柄耗尽这一类在嵌入式网关和资源受限的设备上特别常见。连接建立的过程需要分配资源套接字、缓冲区、会话上下文、可能的加密上下文。如果这些资源在连接断开时没有被正确释放就会泄漏。泄漏的速度可能很慢跑几天才耗尽也可能很快在高并发下几分钟就耗尽。耗尽之后的表现很有意思新的连接请求还能进来因为监听套接字还在内核还能完成传输层握手。但是应用层要分配会话上下文时失败了于是连接被立即关闭。从客户端看就是连上了但马上断了。这类问题的现场特征是设备运行时间越长故障越严重。重启之后能好一阵子然后复发。而且往往伴随着内存占用缓慢上升。排查方法是定期统计当前打开的套接字数量、会话上下文数量、缓冲区池的空闲数量。如果这些数字在连接断开后没有回落就是泄漏。更直接的办法是在资源分配失败的地方打日志看看是不是真的分配失败了。# 观察句柄数量的变化趋势 时间 当前句柄数 空闲会话数 10:00 1024 512 11:00 1536 256 12:00 2048 64 13:00 2560 0 - 开始出现连接建立即断 13:05 2560 0注意13:00这个点空闲会话数降到0之后新的连接就无法分配会话了。但句柄数还在涨说明有另一部分资源也在泄漏。第三类认证或鉴权服务的瞬时不可用这一类容易被误判为网络问题因为现象确实是连接建立后立刻断开而且客户端侧看到的往往是超时。实际情况是连接建立路径上依赖了一个外部服务做认证这个外部服务因为某种原因出现了短暂的不可用比如自己的连接池满了、正在做主备切换、被限流了。认证请求发出去之后没有及时得到响应连接建立流程就卡住了直到超时被回收。这类问题的现场特征是故障往往是突发突止的持续时间可能只有几秒到几十秒。而且影响范围跟认证服务的容量相关可能只有部分节点受影响不是全量。排查的关键是区分认证服务真的挂了和认证服务还活着但响应变慢了。前者会有明确的进程退出或端口不可达后者则表现为响应时间飙升。很多监控只检查了端口存活没有检查响应时间所以漏掉了这种情况。另外要注意一个陷阱认证服务本身可能有多级缓存。缓存命中时响应很快缓存未命中时要去后端查耗时可能差两个数量级。如果缓存刚好在那个时间点集中失效就会出现一批连接同时超时。第四类连接队列溢出导致的连锁反应这一类比较隐蔽因为它涉及到操作系统内核的行为。监听套接字有两个队列半连接队列和全连接队列。半连接队列存放的是收到了传输层握手第一个包但还没完成三次握手的连接全连接队列存放的是已经完成三次握手但应用层还没accept的连接。如果应用层accept的速度跟不上连接进来的速度全连接队列就会满。队列满了之后内核默认行为是丢弃新的握手完成通知或者根据配置发送重置。客户端看到的就是连接建立失败或建立后立即断开。但这里有一个更隐蔽的连锁反应当全连接队列满时内核可能会触发SYN cookie机制或者调整半连接队列的行为。这些调整会影响到后续所有连接的建立时序导致原本正常的连接也开始超时。这类问题的现场特征是故障往往在流量突增时出现而且一旦出现恢复得比较慢因为队列的消化需要时间。监控上可以看到accept队列的长度飙升但很多团队没有监控这个指标。排查方法是在操作系统层面查看监听套接字的队列状态以及在应用层统计accept的调用频率和每次accept之间的间隔。# 查看监听队列状态示意 监听端口 全连接队列当前长度 队列上限 8080 128 128 - 已满 8081 3 128注意8080这个端口队列已经满了新的连接完成三次握手后无法入队会被内核丢弃或重置。第五类时钟跳变或计时器精度问题这一类比较少见但一旦出现排查起来非常痛苦因为它会破坏所有基于时间的推理。连接建立过程中的超时判断依赖于时钟。如果系统时钟发生了跳变比如NTP同步时向后调整了时间那么一个原本还有1秒才超时的连接可能瞬间就超时了。反过来如果时钟向前跳超时判断会被推迟。更隐蔽的是计时器精度问题。有些平台的计时器精度只有10毫秒或100毫秒而连接建立的某些步骤耗时可能就在这个量级。如果超时阈值设得和计时器精度接近就会出现随机性的误判。这类问题的现场特征是故障没有明显的规律跟负载、并发量、运行时间都不相关。可能一天出现几次也可能几天出现一次。日志里看连接建立的时间戳有时候会出现负值或者异常大的间隔。排查方法是检查系统时钟同步的日志看看故障时间点附近有没有时钟调整。同时在代码里用单调时钟如果有的话来做超时判断而不是用墙上时钟。第六类协议状态机的不一致这一类是最难排查的因为它涉及到两端的状态机对不齐。连接建立是两端协商的过程。如果一端的实现有bug或者两端的协议版本有差异可能导致一端认为连接已经建立了另一端还在等待某个确认。这种情况下先认为建立完成的那一端可能会立即发送数据但另一端的状态机还没准备好接收于是数据被丢弃或触发异常连接被断开。从外部看就是连接建立后立即断开而且断开的原因往往报的是协议错误或数据格式错误而不是超时。但如果客户端的超时设得比较短也可能表现为超时。这类问题的现场特征是故障跟特定的对端版本或配置相关。升级一端之后可能就好了或者换一个对端就好了。日志里往往能看到协议解析失败的记录。排查方法是对比两端的连接建立日志把两端的时间线对齐看看在哪一步出现了分歧。这一步往往需要抓包因为应用层日志可能没有记录足够细的协议交互信息。现场排查的优先级建议面对连接建立瞬间超时断开这个现象我一般按这个顺序排查先看是不是资源耗尽。这个最容易确认也最容易修复。检查句柄数、内存、会话数、连接队列长度。再看是不是时间预算不够。把连接建立路径的每个子步骤耗时打出来看P99和最大值。如果某个步骤的耗时接近或超过了它的超时阈值就是它。然后看依赖服务。认证、鉴权、配置下发这些外部依赖它们的响应时间有没有异常。不要只看存活要看延迟分布。最后才看协议和状态机。这个需要抓包和两端日志对比成本最高但前几类都排除了之后往往就是它。还有一个经验如果故障是突发突止的优先看依赖服务和队列溢出如果故障是逐渐恶化的优先看资源泄漏如果故障跟并发量强相关优先看时间预算和队列如果故障没有明显规律优先看时钟和状态机。