连接数这东西,平时没人注意,出问题就是大事。我见过太多项目跑得好好的,某天流量稍微上来一点,应用端瞬间报出一片Too many connections,运维同学第一反应就是把max_connections调大,结果调大了还是报错,最后发现是连接根本没释放,把数据库彻底拖垮。这个坑我踩过不止一次,所以今天把 MySQL 连接数从查询到配置的完整套路整理一遍,包括怎么查看当前连接、怎么定位是谁占满了连接、怎么根据机器配置算出合理的上限,以及常见故障的排查思路。适合刚接触 MySQL 的开发者,也给正在被连接数问题折磨的运维同学做个参考。
1. 先弄清楚连接数到底是什么,以及为什么它会变成瓶颈
1.1 一条连接在 MySQL 里的生命周期
很多人把“数据库连接”理解成“登录一次”,其实没这么简单。在 MySQL 里,一条连接从建立到释放,经历的是完整的网络连接 + 鉴权 + 会话初始化流程。应用端的连接池发起 TCP 握手,MySQL 端完成用户校验、权限检查,然后分配一个线程来处理这条连接上的所有查询请求。也就是说,MySQL 内部每多一条连接,就要多占用一个线程,同时还要为这个会话分配排序缓冲区、join 缓冲区、临时表空间等内存资源。
我经常拿餐厅来打比方:MySQL 就像一家后厨,连接数是厨房里能同时开工的灶台数量。灶台再多也有限,如果每个进来的客人都占着一个灶台不动,后厨再大也会崩溃。应用端的连接池、慢查询、长事务,都可能让灶台被占着不释放。
1.2 连接数太少和太多分别会出什么问题
连接数太少,症状非常直接:应用报Too many connections,连接直接被拒绝。这个报错不是你 SQL 写得有问题,而是 MySQL 连准入都不准了。更麻烦的是,这个错误通常不只在出问题的那台应用上出现,而是所有应用实例同时遭殃,因为你把全局连接数打满了。
连接数太多,情况更隐蔽。MySQL 不是无限资源,每个连接都要吃内存和 CPU。当连接数冲到几百上千,即使还没到max_connections上限,也会出现整体性能下降:查询变慢、锁等待变多、CPU 飙升。很多新手以为“连接数没到上限就没问题”,这是大错特错。真正的临界点往往比配置上限低得多。
1.3 为什么静态配置无法解决动态问题
还有一个认知要纠正:连接数配置不是“一劳永逸”的事。MySQL 的max_connections只是一个静态上限,它决定了 MySQL 最多愿意同时接待多少条连接。但你这个应用到底需要多少连接,取决于应用层连接池配置、并发请求量、每个请求执行时间、是否有慢查询、是否有长事务。今天这个配置可能是合理的,明天上线一个慢接口,半小时内就能把连接数吃满。
所以连接数的处理,一定是“查询—分析—配置—验证—再观察”的循环,而不是改一个参数就完事。这也是我写这篇文章的初衷:把查询和配置的完整方法论讲清楚,让大家遇到问题能自己定位和解决。
2. 查询当前连接数的实用命令与状态指标解读
2.1 一条 SQL 查看当前连接数
最常用的查询命令是这样:
SHOW STATUS LIKE 'Threads_connected';这个Threads_connected就是“当前有多少活跃连接”。注意,这里统计的是当前连接到 MySQL 的客户端连接数,包括空闲连接。只要应用连接池还保持着连接不释放,即使没有查询在跑,它也会被算进去。
如果想看更直观的信息,可以用:
SHOW PROCESSLIST;这个命令会列出所有 Sherthik 在执行或者其他状态的连接。它还会显示每个连接在干什么,比如Sleep表示空闲,Query表示正在执行查询,Locked表示在等待锁。这是排查连接数问题时的第一手资料,后面排查部分我会细说。
2.2 查看连接数相关的其他状态变量
光看Threads_connected还不够,我一般会把下面这几个状态变量一起拉出来看:
SHOW GLOBAL STATUS LIKE 'Threads%'; SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL STATUS LIKE 'Connections'; SHOW GLOBAL STATUS LIKE 'Aborted_connects';Threads_connected:当前连接的线程数。Threads_running:当前正在执行查询的线程数。这个值很关键,如果它长期大于 CPU 核数,说明数据库压力已经很大了。Max_used_connections:自上次重启以来,同时存在的最大连接数。这个值直接告诉你历史峰值有没有碰过天花板。Connections:累计尝试连接 MySQL 的总次数,包括成功的和失败的。Aborted_connects:连接尝试失败的次数。如果这个值持续增长,就要注意是不是认证失败或者连接被中断。
这些指标配合起来看,才能判断当前连接数是“正常水平”还是“危险水平”。
2.3 查看最大连接数配置
最大连接数就是max_connections,可以通过下面命令查看:
SHOW VARIABLES LIKE 'max_connections';默认值通常是151。对很多小网站来说够用,但对稍微有点并发量的应用来说,这个默认值往往是不够的。我还见过一些人直接用SET GLOBAL max_connections = 2000;改完就走,没有写进配置文件,结果 MySQL 一重启又变回 151,这才是真正的大坑。
2.4 组合查询:一条 SQL 搞定所有关键指标
我自己在实际排查时,不会分开敲好几条命令,而是把它们拼在一起看:
SELECT @@max_connections AS max_conn, (SELECT COUNT(*) FROM information_schema.processlist) AS current_conn, (SELECT MAX(cnt) FROM ( SELECT COUNT(*) AS cnt FROM information_schema.processlist GROUP BY db ) t) AS max_per_db, (SELECT COUNT(*) FROM information_schema.processlist WHERE command = 'Sleep') AS sleep_conn;不过information_schema.processlist在连接数很多的时候查询比较慢,线上大并发环境不太建议频繁执行。替代方案是直接看performance_schema里的连接状态,后面我会单独说。
2.5 用 performance_schema 定位每个用户的连接占用
当你发现连接数满了,最想知道的就是“谁占的”。用SHOW PROCESSLIST能看,但信息不够结构化。更好的方式是查performance_schema:
SELECT user, host, db, command, COUNT(*) AS cnt FROM performance_schema.threads WHERE processlist_id IS NOT NULL GROUP BY user, host, db, command ORDER BY cnt DESC;这个查询能按用户、来源主机、当前数据库和执行状态分类统计连接数,一秒定位是哪个应用、哪台机器在疯狂建立连接。比如看到某个user对应command='Sleep'的几条连接占了大几百,那大概率就是这台机器的连接池配置出了问题。
3. 连接数上限的合理配置:从参数到计算公式
3.1 修改 max_connections 的正确姿势
临时修改,不需要重启数据库,在线就能生效:
SET GLOBAL max_connections = 500;但注意,这是用GLOBAL级别设置的,只对后续新连接生效,已经存在的连接不受影响。而且一旦 MySQL 重启,这个值就会丢失。要永久修改,必须改配置文件。
在 Linux 环境下,一般是编辑/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf,在[mysqld]段下加一行:
[mysqld] max_connections = 500改完配置文件后重启 MySQL 服务:
sudo systemctl restart mysqld重启后再次确认:
SHOW VARIABLES LIKE 'max_connections';3.2 其他连接数相关参数,不要只盯着 max_connections
光调max_connections是不够的,连接数问题还和下面几个参数相关:
max_user_connections:限制单个 MySQL 用户的最大连接数。防止某个应用把全局连接数打满,非常实用。max_connect_errors:一台主机连续连接失败达到这个次数后,MySQL 会暂时禁止它连接。这个值默认比较小,如果应用配置错误反复重连,很容易触发。wait_timeout和interactive_timeout:非交互连接和交互连接的空闲超时时间,默认 8 小时。如果应用连接池不主动释放,空闲连接会占用大量连接数。skip_name_resolve:关闭 MySQL 对客户端的反向 DNS 解析。开启后可以降低连接建立时的延迟,也能避免 DNS 解析超时导致的连接问题。
我见过不少案例,max_connections已经调到 1000,但连接数还是很快见顶,最后发现是wait_timeout太长,一堆Sleep连接占着位置不释放。把wait_timeout调到 60 秒之后,连接数明显降下来了。
3.3 连接数到底设置多少合适,怎么算
这是很多人的困惑。max_connections设太小容易爆,设太大则浪费内存。一个简单的估算方法是:用 MySQL 平均单连接占用内存乘以预估连接数,再和可用内存对比。
首先查看单连接平均内存消耗,可以通过performance_schema或经验值来估算。简单粗暴的办法是看 MySQL 进程占用内存和当前连接数的比值:
ps aux | grep mysqld假设 MySQL 进程占用了 30GB 内存,当前有 300 条连接,那平均每条连接大概消耗 100MB 左右。如果你的服务器分配给 MySQL 的内存是 48GB,预留 20% 给系统和其他进程,那么安全连接数大概是:
可用内存 = 48 * 0.8 = 38.4GB 单连接内存 = 0.1GB 理论最大连接数 = 38.4 / 0.1 = 384当然这不是精确值,因为不同查询消耗的内存差别很大。排序、临时表、大查询会让单连接内存飙升几倍甚至几十倍。所以实际设置时,建议留足安全余量。比如理论算下来 384,线上可以先设 300,观察Max_used_connections会不会接近上限,再慢慢调整。
不要一味调大,我见过有人把max_connections设成 10000,结果 MySQL 启动后直接 OOM,就因为没算内存账。
3.4 如何设置 max_user_connections 限制单个应用
如果线上有多个业务共用一个 MySQL 实例,强烈建议给每个业务设置独立的账号,并限制单用户最大连接数。比如某个核心应用最多只给 200 个连接:
CREATE USER 'app_ad'@'%' IDENTIFIED BY 'xxxx'; GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app_ad'@'%'; SET GLOBAL max_user_connections = 0;这里max_user_connections = 0表示全局不限制。然后对指定用户单独限制:
ALTER USER 'app_ad'@'%' WITH MAX_USER_CONNECTIONS 200;这样即使这个应用的连接池配置失误,把连接数冲到了 500,它最多也只能建立 200 条连接,不会拖垮整个数据库实例。这个做法在多人共用的数据库上特别有用。
4. 应用层连接池与 MySQL 连接数的联动配置
4.1 连接池才是连接数管理的真正主战场
很多人一遇到连接数问题就改 MySQL 配置,其实大部分时候问题出在应用层的连接池。Java 系的 HikariCP、Druid,Go 的 database/sql 连接池,Python 的 SQLAlchemy 连接池,每个都有自己的并发模型。
连接池的本质是复用,而不是无限创建。它内部维护一组数据库连接,请求来了从池里拿,用完还回去,避免每次请求都新建连接。新建连接的成本非常高,TCP 握手加 MySQL 鉴权,一次可能就要几十毫秒,在高并发下会浪费大量时间和资源。
但连接池有个很容易踩的坑:最大连接数配的过大,但 MySQL 端max_connections没同步调整。比如连接池最大连接数配了 500,线上有 10 个应用实例,每个应用自己感觉都“没问题”,但 MySQL 端要承受 5000 条连接的洪峰,直接打满。
4.2 连接池大小怎么设置,标准参考值
业内有一个比较经典的经验公式:
连接数 = ((核心数 * 2) + 有效磁盘数)这来自 PostgreSQL 圈的知名建议,MySQL 也适用。对于一个 4 核的数据库服务器,连接数初期可以设置为((4 * 2) + 1) = 9左右。当然这个值看上去很小,很多人不敢信,但它背后的逻辑是:如果一个连接足够快,根本不需要太多连接;如果查询慢,连接再多也只会加剧竞争和上下文切换。
我通常建议从 20 到 50 起步,观察数据库的 CPU 和响应时间再调。如果查询基本都是几十毫秒内完成,几十个连接完全够用;如果频繁出现慢查询,连接数再多也解决不了问题,反而会把问题放大。
4.3 连接池的超时与保活参数
连接池里除了最大连接数,还有几个参数必须关注:
- 最小空闲连接数:保证任何时候都有一些现成连接可用,减少冷启动等待。
- 连接最大空闲时间:空闲超过这个时间就回收,防止 MySQL 端
wait_timeout掐断连接。 - 连接超时时间:请求从池里拿不到连接时,等待多久后抛异常。这个时间设太短容易误报,设太长则请求堆积。
- 连接存活检查:通过一个轻量的探活 SQL 定期检测连接是否有效。
拿 Java 的 HikariCP 举例,一个比较稳的配置是:
spring: datasource: hikari: minimum-idle: 10 maximum-pool-size: 50 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1这里maximum-pool-size: 50表示这个应用实例最多占用 50 条 MySQL 连接。如果部署 5 个实例,MySQL 端最大需要支持50 * 5 = 250条连接。再加上监控、备份等其他连接,MySQL 的max_connections就可以设定在 300 左右。
5. 常见连接数问题与排查技巧实录
5.1 一觉醒来发现 Too many connections
这是最经典的问题:某天早上应用日志里突然全是Too many connections。第一步不要慌,先尝试用管理员账号连上 MySQL,注意这时可能普通连接进不去,但 root 账号有时也进不去。如果 root 也进不去,就需要重启 MySQL 或者用gdb等高级手段,但那样会丢所有连接。更稳妥的顺序是先看监控,如果连不上就用mysqladmin带扩展参数尝试。
实际上,只要 MySQL 还没到完全无响应,用 root 连接通常也是可以的。连接上之后立刻执行:
SHOW PROCESSLIST;重点看Command列,大量Sleep意味着连接池没有正确释放连接;大量Query且Time很高,说明有慢查询堆积;大量Connect,说明应用在频繁创建新连接。
5.2 排查连接泄漏:谁能告诉我连接去哪了
连接泄漏是最头疼的问题。应用的连接池借出连接之后,代码里没有 finally 归还,时间一长,池里的连接就全被借光,新请求全部阻塞或报错。
定位方法分为两步:
第一步,在数据库端找出哪些连接长时间空闲却始终不释放:
SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command = 'Sleep' AND time > 60;如果大量连接处于Sleep超过 60 秒,基本可以判断是应用层连接泄漏或缺少回收机制。
第二步,去应用日志查连接池借出和归还的记录。HikariCP 的日志里会有借出连接未归还的警告;Druid 则可以通过监控面板看到活跃连接数和闲置连接数。
还有一种更隐蔽的情况:代码里开了事务,但事务没有及时提交或回滚,导致连接一直处于活跃状态。这种问题的排查重点不是连接池,而是业务代码里的事务边界。
5.3 修改配置后不生效,重启就还原
这是很多人忽略的:SET GLOBAL和配置文件的关系。SET GLOBAL只是临时修改,MySQL 重启后会用配置文件里的值。如果你修改了/etc/my.cnf但重启后不生效,大概率是 my.cnf 的[mysqld]段写错了位置,或者没有加载到你改的那个配置文件。
先用这个命令确认 MySQL 读取了哪些配置文件:
mysqld --verbose --help | grep -A 1 'Default options'它会列出 MySQL 启动时会读取的配置文件路径,按顺序加载,后面的配置会覆盖前面的。如果你把max_connections写在/etc/my.cnf的[client]段下面,MySQL 服务端是不会读取的。
5.4 连接数配置合理却仍然性能差
有时候连接数没碰顶,但数据库还是很慢。这时要看Threads_running和 CPU 核数的关系。
SHOW GLOBAL STATUS LIKE 'Threads_running';如果Threads_running长期大于 CPU 核数,说明有大量查询在同时争抢 CPU。这种情况下,单纯调连接数没有用,应该优先处理慢查询,给业务加缓存,或者考虑读写分离。
还有一种容易被忽略的情况:max_connections调的很大,但open_files_limit没同步调整。每条连接至少需要一个文件描述符,如果操作系统的文件描述符限制太小,连接数一高就会抛Can't create a new thread或者Too many open files。这时候即使调大max_connections也白搭,需要在操作系统层面同步调大 ulimit。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 报 Too many connections | 连接数打满或连接池配的过大 | 查 processlist,释放空闲连接,调大 max_connections,限制单用户连接数 |
| 大量 Sleep 连接 | 连接池未回收或超时时间过长 | 调小 wait_timeout,检查连接池最小空闲和最大生命周期 |
| 连接建立很慢 | DNS 反向解析超时 | 开启 skip_name_resolve |
| 连接数一高 CPU 就飙升 | Threads_running 过高,慢查询堆积 | 优化慢查询,加缓存,考虑读写分离 |
| MySQL 重启后配置失效 | 配置文件未修改或路径不对 | 确认配置文件加载路径,把配置写到 [mysqld] 段 |
| 报 Can't create a new thread | 操作系统线程或文件描述符不足 | 调大 ulimit 和 open_files_limit |
5.6 一个完整的现场排查示例
最后分享一个我前几天刚处理的案例。一个业务系统的连接数在下午突然冲到 400,max_connections是 500,虽然没有爆,但接口响应已经从 50ms 涨到了 800ms。
我先执行了这条组合查询:
SELECT user, host, db, command, count(*) AS cnt FROM performance_schema.threads WHERE processlist_id IS NOT NULL GROUP BY user, host, db, command ORDER BY cnt DESC;结果发现,一台应用服务器的连接数占了 300,其中 260 条是Sleep状态。再看这台应用服务器的连接池配置,maximum-pool-size配的是 100,但部署了 3 个实例,加起来确实是 300。问题在于 MySQL 端wait_timeout是默认的 8 小时,连接池里的空闲连接一直挂着不释放,而且应用侧又开启了cachePrepStmts,每个连接上还缓存了不少预处理语句,内存开销不小。
处理方式:把连接池的maximum-pool-size从 100 降到 50,idle-timeout设成 10 分钟,max-lifetime设成 30 分钟,同时把 MySQL 的wait_timeout调成 120 秒。改完之后,连接数稳定在 80 上下,接口响应也恢复到正常水平。
这个例子说明,连接数问题绝大多数不是单靠调大max_connections能解决的,而是应用层和数据库层协同配置的结果。
6. 一些值得记住的实操经验
再补充几个我在实际运维中总结的小经验。
如果是云数据库,比如 RDS 系列,连接数参数通常有特殊的修改入口,而且还有“连接数上限”之外的隐藏限制,比如 max_total_connections 和 max_user_connections 的组合。修改前先看一下控制台上的参数组说明,不要直接在实例内执行SET GLOBAL,因为云厂商的配置管理机制不一定允许你这么改,重启后可能还会被覆盖。
在排查连接数问题时,优先查看 MySQL 的错误日志。很多连接数相关的线索,比如Aborted connection、Connection closed等,都会记录在错误日志里。默认位置一般是/var/log/mysql/error.log,也可能是/var/lib/mysql/*.err。不要只盯着应用日志。
还有一点:备份脚本、监控系统、定时任务也会占用连接。我之前排查过一个案例,每天凌晨 3 点数据库连接数飙升,最后发现是 cron 脚本里配置的 mysqldump 每次都是新建连接,而且备份操作长时间运行,把连接数顶了上去。后来给备份任务单独分配了一个账号,并用max_user_connections=10限制该账号连接数上限,问题就解决了。
最后再说一个非常容易被忽略的点:连接数监控一定要做历史趋势,而不是只看当前值。Max_used_connections这个值虽然历史累加,但能反映自重启以来的峰值,建议结合监控系统把线程数、Running 线程数、活跃连接数的曲线都记录下来。当某天连接数开始异常增长时,有历史数据可以参考,能更快判断是业务自然增长还是代码 bug。
连接数这东西,说难不难,说简单也不简单。核心思路就是:查当前值、算上限、找占用、调配置、观察趋势。把这几步做好,连接数问题基本不会再来困扰你。