两座挂钟走时差了几格,中间一根电缆被剪断后仍插在两端接口上,线头像没熄的余温

一、一条连接有两段生命周期,各由一处决定

一条从应用侧发往数据库的连接,生命周期由两处各自独立地决定。服务端那一侧负责“不再服务这条空闲连接”:它按自己统计的空闲时长判断该不该断开。池侧那一侧负责“不再把这条连接交给应用”:它按自己的存活上限与淘汰策略决定什么时候把连接退役并关闭。

只收紧其中一侧,回收不了僵尸连接。空闲断开这类数值的作用,是让服务端主动切断长时间不用的连接;如果池没有同步把这些已被关闭的连接退役掉,应用下一次借出的仍然是那个失效的句柄,取用即报错。现场证据往往也停在这里:改完数值并重启之后,运行时的连接列表里仍然能看到一批处于睡眠一类状态、且空闲时长早已越过阈值的行。这说明断开条件确实在被满足,只是满足之后连接并没有从池里消失,而是变成一条谁也不再认领的空壳。

事件 由谁决定 何时可见 两侧没同步时的报错形态
空闲到点被断开 服务端 运行时连接列表里该行不再出现或已终止 池仍持有该连接,取用时才暴露
连接被退役并关闭 池侧 池的调试日志里的淘汰与关闭记录 借出已关闭的连接,第一条语句就失败
存活时间到达上限 池侧 借出时的分配动作与调试日志 连接长期滞留池中反复复用
连接上的会话状态丢失 服务端 依赖该状态的操作开始出错时 连接看着活着,会话里的临时对象已不在

二、两侧数值必须错开,而不是对齐

反直觉但关键的一条:池侧的最长存活时间要小于服务端的空闲断开值,还要留出缓冲,而不是设成相等。差值用来吸收三件事:两台机器时钟的不同步、网络上的延迟抖动、状态从服务端传到池侧的窗口。两侧对齐,等于让“服务端恰好到点断开”和“池恰好把这条交出去”撞在同一刻,谁先发生取决于运气,故障于是表现为偶发而不是稳定,最难复现的正是这一类。

差值取得太小,两侧几乎同刻到期,池仍会频繁把刚被关闭的连接交给应用;差值取得过大,连接在服务端一侧长时间悬挂占资源,而池里的连接又过早被自己淘汰,重建开销随之上升。据公开报道,这类缓冲常见的写法是让池侧比服务端少一分钟量级;这个数字只是参照,不构成本篇的判断依据,我们的结论只到“两侧必须错开、且池侧更短”这一层。

除了最长存活时间,另一类池实现里的对应项是空闲连接的淘汰时间上限,作用是把长时间没被借出的连接提前收走。思路与存活上限一致:先于服务端动手,而不是等对面通知。

表述上我们坚持一句话:两侧数值是同一策略的两半,不该分给两个团队各写各的。变更评审时只看其中一侧的配置单,等于没看。

三、四类“改了没生效”的判据各不同

改完仍有僵尸连接,原因基本落在这四类里,每一类要有可判定的证据,否则排查会退化成把同一个数值反复调小。

第一类,配置文件改了但服务没有重启。判据是回读运行时值与文件值不一致。文件里写的值和运行时真正在用的值是两个来源,只有回读才能区分“没读到”与“读到了但没起作用”这两种失败,它们的处置完全不同。

第二类,用运行时方式改了全局值,却没有持久化。这类改法当场见效,重启后回退到默认,于是同一个故障在几次变更之后莫名回来。判据是以重启后的运行时回读为准再核一次,而不是改完就写进基线记录。

第三类,连接正处于事务中,空闲计时暂停。未提交或未回滚的事务会让这条连接不计入空闲,它自然不会被断开。判据是把连接状态和事务状态放在一起看,只看连接列表里的空闲时长会得到错误结论——那个数字反映的是最后一次读写结束到现在过了多久,不等于这条连接在断开逻辑里被算作空闲了多久。这一类对应要做的是找出长事务,继续调断开值没有用。

第四类,客户端的连接属性里开启了自动重连。底层一旦断开,客户端会自行把连接重建起来,于是从应用侧看这条连接始终可用,真实情况是服务端已经换过一次会话,连接上的会话级临时对象不再存在。判据就是检查这些会话级临时对象还在不在。要说清楚,这里的会话指服务端为这条连接维护的会话状态,与登录态是两码事。这一类最费时间,因为它把故障从“立刻报错”改成了“过一会儿结果不对”,排查方向很容易被带到业务逻辑里去。

四、别用高频校验代替生命周期管理

最常见的弯路,是每次从池里借连接都先发一条校验查询,确认它还能用。代价随并发放大:单位时间取用次数乘以连接数量,就是凭空多出的往返,高并发下这条校验自己就能把链路挤慢。而且校验通过之后,连接仍然可能在两句话之间被关掉,它买不到确定性,只买到一个更小的概率。

两种做法的适用面不同。借用校验适合连接很少复用、并发不高、失败代价可控的场景,问题发生在这里重试一次就能兜住。后台轻量心跳适合连接长期持有、取用频繁、而服务端又会主动断开空闲连接的场景,它让连接始终保持“刚被用过”的状态,从源头避开空闲计时,比每次借出都问一遍便宜得多。选择判据我们按两件事定:单位时间的取用频率,以及连接的数量级。两头都大时,先把两侧生命周期配对做对,再考虑是否需要心跳;两头都小,就不必为偶发失败增加常驻流量。

边界要划清:心跳是对空闲计时的补偿,不是生命周期管理的替代品。配了心跳,两侧数值照样要错开。

五、参数家族不能混,先按故障形态分诊

这类配置被笼统叫成“超时”,实际管着三段彼此无关的事。空闲断开值管“空闲多久不再服务这条连接”,而且交互式连接与非交互式连接分别由两个值控制,各管一类连接;连接建立阶段另有一个值,管的是握手与认证还没走完的那段;查询执行阶段的读写卡顿容忍又另有两个值,管服务端在一次执行里能容忍多久的读写停顿,与僵尸连接无关。池侧的存活上限与空闲淘汰是第四组,不在服务端配置里。

把它们串成一档来调,是这类故障被拖长的主因。典型后果是:为了压掉僵尸连接去动读写卡顿容忍,僵尸连接一个没少,执行期真的发生停顿的语句反而先被掐断。分诊顺序我们按故障形态走。

  1. 报错出现在借出连接后的第一条语句上,且运行时连接列表里持续堆积空闲越线的行——对应空闲断开与池侧两组,先检查两侧有没有错开。
  2. 连接在建立阶段就失败,位置在握手或认证——对应建立阶段的值,与空闲断开无关。
  3. 语句执行到一半被掐——对应读写卡顿容忍,与僵尸连接无关。
  4. 事务迟迟不提交、连接始终不断开——对应长事务治理,断开类数值帮不上。

这一段只做归类,讨论的是服务端与池两侧的连接生命周期,不涉及一次端到端请求该等多久的预算传递,那是另一层问题。

六、还有一类断开与参数无关,验证按四步收口

在某些操作系统环境下,防火墙或终端上的安全软件会在连接长时间空闲时主动发出复位包,连接在到达服务端的断开条件之前就被掐掉。这属于传输层断开,任何空闲断开参数都不参与。识别方法是看相关性:断连与空闲时长有关,但与改动的数值无关,参数调大调小现象不变;并且它集中出现在某几类终端环境或某几条网络路径上,而不是全量发生。判定成立之后,排查必须转到网络与终端侧,继续在服务端调参数只会让故障停在原地不动。

验证四步,顺序不能反,因为它从“值有没有进去”一路查到“下一次重启后还在不在”。

  • 回读运行时实际值。通过判据:读到的就是本次变更想要的值。失败时最可能的原因是第一类与第二类,即没重启或未持久化。
  • 观察运行时连接状态分布。通过判据:处于睡眠且空闲越线的行不再持续堆积。失败时最可能的原因是第三类,事务让计时暂停。
  • 查池侧的淘汰与关闭记录是否真的触发,依据是池的调试日志而不是推断。失败时最可能的原因是池侧那一半根本没设,或者设得不比服务端小。
  • 确认变更在重启后仍然存在。失败时最可能的原因是只用了运行时方式修改全局值。

四步各查一处,合起来才叫生效:值进了运行时、服务端不再挂连接、池不再交出已关闭的连接、重启之后仍然如此。洹拓在交付里把这四步做成验收表,跟变更单一起走。这套配置最坑的地方在于,它每一次都能给人一种“改完就好了”的错觉,而错觉的来源正是把两套时间当成了一套。