成排的蓝色网络配线架接口
数据库 4 分钟

MySQL 连接池的坑:min 参数让连接成对断开

日志里每隔一两天就成对冒出两条 Connection Error。把「为什么是两条」和「为什么是一两天」拆开看,问题就清楚了。

前段时间翻服务器日志,发现 Ghost 每隔一两天就会报两条一模一样的错误:

Connection Error: Error: The client was disconnected by the server
because of inactivity. See wait_timeout and interactive_timeout
for configuring this behavior.

两个奇怪的地方:一是它从来不成单出现,永远是成对的两条;二是它每隔一两天才来一次,中间毫无规律。

把这两个"不对劲"拆开看,问题其实很清楚了。

一、为什么是成对出现

数字「2」本身就是答案的一半。Ghost 用的是 knex,底层的连接池是 tarn。tarn 的默认配置里有一个参数:

min: 2

min 的意思是「池子里至少保留几条连接」。关键在于 tarn 的回收规则——低于 min 的连接永远不会被回收。也就是说,只要这两条连接建立过一次,它们就会被永久占着,哪怕博客一整天没人访问。

所以日志里永远是成对的:池子里雷打不动地蹲着两条空闲连接。

二、为什么每隔一两天才报错

MySQL 那边有个参数叫 wait_timeout,默认值 28800 秒,也就是 8 小时。它的意思是:一条连接如果 8 小时没有任何活动,MySQL 就主动把它掐掉。

把这个和上面接起来,现象就解释通了:

  • Ghost 池子里有两条连接,闲着不动
  • 博客访问量低,经常超过 8 小时没人来
  • MySQL 到点把这俩掐了
  • tarn 下次用到它们时才发现是断的,于是报错

一次掐两条,所以报错也是两条。至于「一两天一次」,取决于这段时间里到底隔了多久没人访问——超过 8 小时才会触发。

三、光推测不算数,得验证

上面都是推测。要确认,得从三个角度下手:

  1. 看配置有没有传下去。Ghost 的 core/server/data/db/connection.js 里,配置对象是直接丢给 knex 的,所以 config.production.json 里的 pool 段确实会生效。
  2. 看 MySQL 里到底有几条连接。查 information_schema.processlist,按用户筛一下,能看到连接数和各自的空闲时间。
  3. 看它会不会自己放开。这是最直接的:打几个请求,连接数应该涨上去;停下来等过 idleTimeoutMillis,再查应该掉回 0。

实测下来是这样:

t=0s    空闲,没请求前          : 0 条连接
        (抓了 4 个页面)
t=2s    刚打完流量             : 4 条连接
t=22s   空闲 20 秒             : 4 条连接
t=47s   空闲 45 秒             : 4 条连接
t=67s   空闲 67 秒(超过 60s)  : 0 条连接

空闲连接能被彻底放掉,说明池子不会再给 MySQL 留下「可以超时」的东西。

四、改法

"pool": {
  "min": 0,
  "max": 10,
  "idleTimeoutMillis": 60000
}

min: 0 是关键——允许池子完全清空。idleTimeoutMillis 控制空闲多久释放,这里设 60 秒。max: 10 是上限,防止某个时刻突然并发太高把数据库连接占满。

有人会担心 min: 0 会导致每次请求都重新建连接、变慢。对这个博客这种低流量场景,建立连接的开销完全可以忽略(本地 MySQL 上就是微秒级),换来的是再也不会因为空闲被掐。

五、一个小教训

中间排查时还踩了一个时间上的坑:用 docker logs --since 过滤日志,看到的错误数量对不上,差点误判成「改了以后还在报」。原因是这个参数里不带时区的字符串,Docker 会按守护进程的本地时区去解析,而服务器是 JST(UTC+9),于是时间窗口整体偏了 9 小时,把改动之前的错误也捞了进来。

用 docker logs -t 带上每个条目的时间戳再核对,才是可靠的。

六、小结

这类问题的坑在于:它不是「坏了」,而是「默认值跟你的使用场景不匹配」。连接池的默认 min: 2 对高流量服务是合理的——保持几条热连接能省掉建连开销;对一天没几个访客的博客就变成了定时炸弹。

所以看到一个反复出现、又很有规律的错误,先别急着改代码,去看看默认配置。

评论