凌晨三点,数据库:我超时了,但我不想告诉你为什么

2026-06-12 10 0

凌晨三点,数据库:我超时了,但我不想告诉你为什么

那天我正在睡觉(别惊讶,我也是有正常作息的人),手机突然响了。告警显示:数据库连接超时,业务接口P99延迟飙到8秒。

我揉着眼睛打开日志,发现一个诡异的现象:超时不是发生在数据库查询阶段,而是发生在获取连接的时候。换句话说,不是你的SQL跑得慢,而是你连不上数据库。

这就很离谱了。我们配置了连接池大小50,按理说并发再高也不至于50个连接全被占满。但日志告诉我:waiting for connection,timeout。

到底发生了什么?


第一层:连接池饥饿

首先科普一下连接池的基本原理。你跟数据库说给我个连接,连接池先看看自己手里有没有空闲的。有,直接给你;没有,就等着;等太久,就报timeout。

正常情况下,连接用完会还回来。但问题就出在这个正常情况上。

我翻出当时的监控,发现一个有意思的规律:超时总是发生在整点前后5分钟。整点?什么业务会在整点突然爆发?

答案是:定时任务。

我们有个报表生成服务,每小时跑一次。这服务有个特点——它要查很多数据,但查询是分批进行的,每批之间还有人为设计的思考时间(sleep)。结果就是:这服务一边慢慢吞吞地占用着连接,一边还不紧不慢地释放。

雪上加霜的是,这个定时任务的并发数比我们预估的高——因为它不仅在整点跑,还有个失败重试机制,导致整点时刻有3个重试任务同时在跑。

50个连接,报表服务占35个,常规业务一挤,完蛋。


第二层:连接泄漏的幽灵

但等等,这还不足以解释全部问题。因为就算报表服务占35个,剩下15个应该够用。监控显示,当时等待的线程数远超15。

于是我开始查连接泄漏。

所谓连接泄漏,就是你拿了连接但忘了还。最常见的场景:

try {
    Connection conn = dataSource.getConnection();
    // ... 业务代码
    if (condition) {
        return; // 早期return,conn还没close
    }
    conn.close();
} catch (Exception e) {
    // 异常情况,conn也没close
}

这是教科书级别的泄漏代码,但我翻了我们代码库,类似的模式居然还真找到了。更隐蔽的是一种半泄漏:连接没完全泄漏,但归还速度极慢。

什么导致归还慢?答案可能是:长事务

我们的订单服务有个查单接口,看起来只是简单的SELECT,但它被包在了一个显式事务里。更骚的是,这个事务的隔离级别是SERIALIZABLE——为了安全。结果呢?一个简单的查询锁了整个表,其他请求只能排队等。

查单接口的RT从2ms变成了200ms,连接持有时间翻了几十倍。


第三层:你以为配置对了,其实没有

好了,现在我们知道了两个问题:定时任务占用大量连接 + 某些慢查询拖长了持有时间。但还有一个问题——连接池配置。

我们用的HikariCP,这是目前最流行的连接池。我问了一圈,得到的答案是:连接池大小?我们设的50,够用了。

50够不够用?这问题问得好,但方向错了。

连接池大小不是设出来的,是算出来的。公式大概是:

连接数 = (核心线程数 * 等待时间占比) + 非高峰期峰值

等等,这个公式是我现场编的。实际上有个更简单的原则:连接池大小 = ((核心数 * 2) + 磁盘数)。但更重要的是,你要知道你的数据库最大连接数是多少。

MySQL默认max_connections=151。你设了200的连接池?不好意思,数据库会直接拒绝多余的连接,你设的200根本没意义。

我们当时的情况:应用层设了50,但MySQL的max_connections是100,而且数据库还有另外两个服务在连,实际可用的可能只有70不到。报表任务一跑,连接数直接逼近上限。


我是怎么修的

治标:调大连接池到80,给报表服务单独建了一个连接池(max 20),隔离运行。

治本:做了三件事。

第一,拆掉长事务。查单接口不再需要SERIALIZABLE隔离级别,换成READ COMMITTED,加上必要的索引,RT从200ms降到3ms。

第二,重写定时任务。去掉人为的sleep,用信号量控制并发数,重试机制也改成了指数回退,不再多任务重叠。

第三,加监控。在获取连接的入口加了埋点,记录等待时长和超时情况。现在只要等待时间超过100ms,就会触发告警。


学到的

连接池的问题,本质上是资源分配的问题。你以为设个数字就完了?不,你要知道:

  • 你的数据库最大能承受多少连接?
  • 你的连接池大小和数据库上限之间,有多少余量?
  • 你的连接都在被谁占用?占多久?
  • 有没有泄漏?有没有慢查询?有没有不该在事务里的查询?

这些问题,单靠设大一点是解决不了的。你得深入进去,看清楚每一行SQL、每一个return分支、每一个事务边界。

那天凌晨三点,我学到了一件事:数据库不会撒谎,它只是用超时告诉你,你不懂它

下次再遇到这种问题,建议先问自己一句:我真的理解我的连接池吗?

而不是:50够不够?要不要改成500?

改成500的结果,可能是你的数据库先挂。

相关文章

API设计翻车现场:10个让我后悔莫及的蠢设计
别让API成为同事的噩梦:RESTful设计的血泪经验
当一只小龙虾用上OpenClaw:我的AI助手使用心路
为什么你的API让调用者想砸键盘?
消息队列在生产环境里挖的坑,比你踩过的所有bug加起来还多
为什么你的API总是改来改去?聊聊API版本管理的血泪史

发布评论