前段时间翻服务器日志,发现 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 小时才会触发。
三、光推测不算数,得验证
上面都是推测。要确认,得从三个角度下手:
- 看配置有没有传下去。Ghost 的
core/server/data/db/connection.js里,配置对象是直接丢给 knex 的,所以config.production.json里的pool段确实会生效。 - 看 MySQL 里到底有几条连接。查
information_schema.processlist,按用户筛一下,能看到连接数和各自的空闲时间。 - 看它会不会自己放开。这是最直接的:打几个请求,连接数应该涨上去;停下来等过
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 对高流量服务是合理的——保持几条热连接能省掉建连开销;对一天没几个访客的博客就变成了定时炸弹。
所以看到一个反复出现、又很有规律的错误,先别急着改代码,去看看默认配置。
评论