1. 一个被问烂了的问题,为什么还是值得认真聊
MySQL 最多能有多少连接?这大概是 DBA 面试里出现频率最高的问题之一。我刚带团队的时候也经常拿这道题去问新人,十个人里有八个能背出 151 这个默认值,但再往下问一句"这个上限是谁定的、连接数满了会发生什么、怎么判断该调多大"就立刻卡壳。说实话,这题看着简单,背后牵出来的是一整套连接管理机制、线程模型、系统资源核算和运维排查思路,远不是一个show variables like 'max_connections'就能说清的。
和不少同行聊过之后,我发现真正有价值的不是记住几个数字,而是理解这张"连接配额"的血缘关系:谁在限制它、谁在消耗它、谁在排队等它,以及当配额见底时为什么你的应用会突然报"Too many connections"。这篇文章我打算把这块彻底讲透,从参数本身一路拆到 Linux 文件句柄、线程栈、内存预算、连接风暴、连接池调优,最后给出一套可以直接照着做的排查和调优清单。不管你是开发、DBA 还是刚入门的运维,按这条线走一遍,以后再碰连接数问题心态会完全不一样。
我自己这些年排过不少连接相关的故障,从几十个连接的内部系统到上万连接的游戏后端都折腾过。有一个感触特别深:连接数问题从来不是单纯的数据库问题,它是应用、网络、操作系统和数据库四方博弈的结果。今天这篇就是把这些博弈点一个个摊开来讲。
2. max_connections 的默认值和它到底限定了什么
2.1 先摆事实:151 和 100000 是怎么回事
MySQL 的max_connections参数控制的是 MySQL 服务器允许同时保持的客户端连接总数上限。默认值在 5.7 和 8.0 里都是 151,这个数字主要考虑的是历史上单机 MySQL 的并发能力,同时给操作系统和其他服务留出余量。很多人在网上能看到有人把值调到 100000 甚至更高,那通常是压测环境或者特定的大内存高并发业务场景下的激进配置,不代表生产环境也应该这么干。
这里要特别区分两个概念:总连接数和活跃连接数。max_connections限制的是所有处于 "Sleep" 或者 "Query" 状态的连接总和。一个连接建立后如果暂时没有请求,它会处于休眠状态,同样占用一个连接名额和一部分线程资源。所以你在show processlist里看到的绝大多数连接可能都是 Sleep,这些休眠连接不干活,但依然"占着茅坑"。很多连接数爆掉的场景,并不是查询真的很多,而是应用侧连接池配得太大,一堆连接建立之后又不释放,把连接配额慢慢吃光了。
下面这张表的数值是 8.0 版本下的默认情况,供你心里先有个谱:
| 项目 | 默认值 | 说明 |
|---|---|---|
| max_connections | 151 | 最大并发连接总数 |
| max_user_connections | 0(不限制) | 单个用户最大连接数,0 表示不受限 |
| max_connect_errors | 100 | 单主机允许的连续失败连接次数,超过后会被临时封禁 |
| thread_cache_size | 取决于版本和系统 | 线程缓存数量,影响新建连接的速度 |
| back_log | 取决于版本和系统 | 连接请求在握手完成前的排队长度 |
注意max_user_connections这个参数,生产环境里有时候一台应用服务器的 IP 会集中建立大量连接,如果业务账号是共用的,很容易导致某一个应用把连接全占完。通过给关键账号单独设置这个值,可以起到隔离作用。我遇到过一个典型案例:两个团队共用一个数据库账号,其中一个团队发版时连接池配置失误,瞬间建了三千个连接,直接把另一个团队的查询全部阻塞。后来给每个应用单独建账号,并按业务峰值分配max_user_connections,类似的事故再没发生过。
2.2 连接数的计算逻辑和启动期配额
搞清楚连接数有多少,不能只看max_connections,还要看max_connections + 1这个隐含规则。MySQL 在设计上给自己保留了一个超级权限连接的余地:即便连接数已经达到上限,依然允许具有SUPER权限(8.0 里是SYSTEM_USER相关权限)的账号再建立一个连接。这样做的目的是让 DBA 在连接满的时候还能登录进去排查问题。
实际验证也简单。你可以先故意把max_connections调成一个很小的值,比如 3,然后用普通账号把连接打满,再用 root 账号去连接,你会发现 root 还是能连上。但这里有一个很坑的细节:如果连接数已经爆满,你尝试用 root 连接时,MySQL 依然会先报一次Too many connections,然后内部才会尝试释放预留连接给你。很多新手看到这个报错就以为 root 也进不去了,直接重启数据库,其实多试一两次就能进去。这个坑我排障时见得太多了,值得单拎出来说一下。
另外要留意 8.0 之后权限体系的变化。MySQL 8.0 把一部分超级权限拆成了更细粒度的权限,比如CONNECTION_ADMIN。如果你的账号在 5.7 里靠SUPER权限能突破连接限制,升级到 8.0 后可能需要额外授予CONNECTION_ADMIN才能达到同样效果。数据库升级之后有些脚本会莫名连不上,很多时候就是因为这个权限拆分,而不是连接数本身出了问题。
3. 隐藏的上限来源:操作系统文件句柄才是真正的铁门
3.1 从一条报错说起
有一类连接问题特别让人迷惑:MySQL 配置文件的max_connections明明已经调到 5000 了,show variables里也显示 5000,但连接数涨到 2000 出头就再也上不去,错误日志里还会出现类似Can't create a new thread或者Too many open files的提示。如果你也遇到过这种情况,大概率是撞上了操作系统层的限制。
每个客户端连接在 MySQL 内部对应一个 socket 连接,这个 socket 就是一个文件描述符(File Descriptor,简称 FD)。与此同时,MySQL 还需要打开表文件、日志文件、临时文件等,这些都要占用 FD。所以连接数上限真正能到多少,不仅取决于max_connections,还取决于 MySQL 进程能打开的 FD 上限。
具体的换算关系可以简化成这样一个式子:
所需 FD 数量 ≈ 连接数 × 单连接 FD 消耗 + 基础文件占用
其中单连接 FD 消耗通常是 1 到 3 个,因为除了 socket 本身,还可能有临时分配的文件或管道。基础文件占用则包括系统表空间、redo log、binlog、每张表的 .ibd 文件等。表数量越多,基础占用越高。
如果拿一个人口来打比方,max_connections相当于酒店系统允许登记的最大房客数量,而文件句柄限制相当于酒店大楼实际的房间和床位总量。系统参数说能住 5000 人,但楼只有 2500 个床位,那系统参数就成了空头支票。
3.2 实际操作里怎么检查和调整
检查当前 MySQL 进程的 FD 上限,可以直接看进程的 limits:
# 找到 mysqld 的进程号 pidof mysqld # 查看该进程的资源限制 cat /proc/$(pidof mysqld)/limits | grep "open files"如果输出里的 soft limit 和 hard limit 都是 1024 或者 4096,那基本可以断定连接上不去是因为这里太小了。常见发行版里默认的ulimit -n都不够大,需要调。
调整方式分两层。第一层是系统全局和用户级配置,编辑/etc/security/limits.conf:
mysql soft nofile 65535 mysql hard nofile 65535第二层是 systemd 托管下的配置。现在主流发行版都用 systemd 跑 MySQL,单纯改 limits.conf 可能不生效,还需要在 service 文件里追加:
[Service] LimitNOFILE=65535改完别忘记执行systemctl daemon-reload再重启 MySQL。很多人改了 limits.conf 发现没效果,就是因为忘了 systemd 这一层会覆盖它。
这里有一个我亲身踩过的坑:把LimitNOFILE=65535和max_connections同时调大之后,连接数确实上去了,但系统负载也跟着飙高,最后发现是 MySQL 打开的线程数量过多导致上下文切换开销暴涨。文件句柄是打开了,新的瓶颈又跑到了线程调度上。所以我现在的习惯是改完 FD 之后盯着vmstat的cs列和top里的 CPU 使用率再观察一周,确认没有新的瓶颈出现再继续压测。
3.3 连接建立的完整链路:从 TCP 到 MySQL 线程
为了让"为什么一个连接会消耗系统这么多资源"这件事更直观,我把一个连接从建立到执行的完整链路过一遍。它大致要经过这几个环节:
- TCP 三次握手完成,socket 建立,落入内核的 accept 队列。
- MySQL 的监听线程从队列里取出连接请求,分配一个连接对象。
- 连接对象进行握手验证,包括协议版本协商、用户名密码校验、权限加载等。
- 验证通过后,MySQL 从线程池或线程缓存中取出一个工作线程来处理这个连接上的查询。
- 连接进入空闲状态时,线程可以归还到线程缓存,连接本身仍然保持。
在这条链路上,至少有四个资源点和连接数直接相关:socket 文件描述符、内存中的连接对象、线程栈空间、认证过程需要的 CPU 和内存。任何一个环节被顶满,都会表现为连接失败或者请求变慢。这也是为什么我不太认同"只要把 max_connections 调大就能解决一切并发问题"这种说法。调大参数只是扩大了入口,但入口之后的每条路都被系统资源约束着。
4. 线程模型和连接风暴:为什么连接数变多后数据库反而更慢
4.1 线程池和 thread_cache_size 的作用
MySQL 经典的连接处理方式是"每连接一线程",也就是一个客户端连接独占一个工作线程,这个线程负责读取该连接上的命令并执行。连接多了,线程自然就多。频繁地创建和销毁线程需要做系统调用、分配内核栈,开销不低,尤其在高并发短连接场景下,这部分成本非常可观。
为了缓解这个问题,MySQL 提供了thread_cache_size参数,用于缓存空闲线程。当连接关闭时,工作线程不立即销毁,而是放回缓存等待下一个连接复用。默认值在不同版本和系统上有差异,通常不大。我之前在压测时试过把它从默认值调到 64、128、256 几个档位,发现对短连接场景的提升很明显,尤其是连接建立速率,但调到 256 之后收益开始递减,因为缓存线程本身也要占内存。
再来就是连接池。这里要分清两层连接池:应用侧连接池和 MySQL 侧线程池。应用侧连接池(比如 HikariCP、Druid)解决的是"应用与 MySQL 之间的连接复用",避免每次请求都新建连接。MySQL 侧线程池则是 MariaDB 和 MySQL 企业版里提供的thread_pool功能,它能限制同时执行的线程数,让大量连接的场景下只有一部分线程真正在跑,从而控制 CPU 争抢。
在开源 MySQL 社区版里没有线程池,连接多了之后如果大量连接同时活跃,线程调度会成为瓶颈。这也就解释了为什么有些系统连接数只有几百时一切都好,涨到几千后 CPU 虽然没满,但整体吞吐量反而下降。说直白一点,MySQL 的"每连接一线程"模型下,线程数和连接数基本呈线性关系,而线程调度成本却不是线性的。
4.2 连接风暴:为什么会瞬间打垮数据库
连接风暴是我在实战中见过最致命的一类故障,表现形式非常有特点:数据库连接数在几秒内冲上几千,然后应用开始报Too many connections,紧接着是一波超时和重试,重试又带来新的连接,形成一个恶性循环。
风暴的起因通常有两类。第一类是应用重启或发版后,应用连接池的初始连接数和最大连接数设置不合理,数百个实例同时启动,每个实例瞬间建立几百个连接,总量轻松突破数据库上限。第二类是数据库短暂抖动后,应用侧连接池的空闲连接大量失效,触发重连逻辑,客户端在重连时往往没有做退避和限制,导致连接请求在短时间内的峰值是平时的几倍甚至几十倍。
对于这两类问题,纯粹调大max_connections是治标不治本。更有效的组合拳是:
- 应用连接池的
initialSize、minIdle、maxActive都设置成合理的小值,不要图省事直接拉满。 - 连接池的
connectionTimeout和validationTimeout要短,避免应用在数据库故障时无限等待。 - 在应用层做连接获取的退避重试,比如失败后按指数退避,而不是立即重建。
- 数据库侧开启
skip-name-resolve,减少连接建立时反向 DNS 解析的超时风险。
说到skip-name-resolve,这是连接速度优化里一个被低估的参数。MySQL 默认在握手阶段会对客户端 IP 做反向 DNS 解析,如果 DNS 服务慢或者不可用,连接建立时间可能从毫秒级变成秒级。在连接风暴场景下,这会让握手超时集中在同一时间段,从而进一步放大问题。我经手的项目里只要确认应用都是通过 IP 连接,就会把这个参数打开,效果立竿见影。但它有一个副作用:processlist里看不到主机名,只有 IP。而且如果你在授权表里用的是'user'@'localhost'这种主机名形式,开启后可能匹配不上,需要提前改成 IP 或通配符。
4.3 内存预算:连接数上限的另一道账
每个连接不只消耗一个线程,还消耗内存。连接对象本身、网络缓冲区、线程栈等都会计入 MySQL 进程的 RSS。虽然 MySQL 的线程栈在绝大多数系统上默认是 256KB 到 1MB 不等,但连接多起来之后,这笔内存相当可观。
这里可以做一个简单的估算。假设一个连接平均消耗 2MB 内存(包含线程栈和缓冲区),max_connections=5000时,仅连接相关内存就可能达到 10GB。如果服务器的总内存只有 16GB,还要分给 innodb_buffer_pool、排序缓冲、join buffer 和操作系统,那么这个配置从一开始就是不可行的。很多人压测时把max_connections调得很高,然后发现 MySQL 直接被 OOM Killer 杀掉,就是没算这笔账。
在使用云主机时尤其要注意:云服务器的内存往往比同配置物理机小,而且有些云厂商默认会限制进程数或线程数。我曾见过一台 4GB 内存的容器里跑 MySQL,应用连接池配置了最大 200 个连接,结果 MySQL 占满内存被系统自动杀掉,日志里却查不到明显的 MySQL 错误,最后靠查看内核日志才发现是内存不足。所以调max_connections之前,把内存余量算明白比调参本身更重要。
5. 从监控到定位:连接数异常时的排查链路
5.1 一套可以直接复用的排查顺序
连接数报警或者出现Too many connections时,我建议按下面这条顺序走,而不是一上来就改参数或者重启数据库。顺序是有讲究的,先看现象,再看配置,然后看资源,最后才动手。
- 先确认连接数的实际状态和构成:
show processlist;或者查performance_schema里的连接统计。 - 看连接数是否真的打满:
show status like 'Threads_connected';和show status like 'Max_used_connections';对比。 - 看连接的状态分布:有多少 Sleep、多少 Query、多少 Connecting。状态分布能快速判断是并发查询真的多,还是连接池空闲连接堆积。
- 看 MySQL 错误日志:
Too many connections、Can't create a new thread、Too many open files是三种完全不同的问题方向。 - 看操作系统资源:CPU、内存、文件句柄、线程数、TCP 连接数。这一步能验证是不是 MySQL 之外的资源顶到了上限。
- 定位来源 IP 和账号:
show processlist里能直接看到 Host 和 User,也可以用performance_schema的表做聚合统计,找出占用最多的客户端。
在 MySQL 8.0 里,查询连接状态更推荐用performance_schema的视图,比如:
-- 统计当前连接数 SELECT * FROM performance_schema.threads WHERE PROCESSLIST_ID IS NOT NULL; -- 按用户聚合连接数 SELECT USER, HOST, COUNT(*) FROM performance_schema.threads WHERE PROCESSLIST_ID IS NOT NULL GROUP BY USER, HOST ORDER BY COUNT(*) DESC; -- 按状态聚合 SELECT PROCESSLIST_STATE, COUNT(*) FROM performance_schema.threads WHERE PROCESSLIST_ID IS NOT NULL GROUP BY PROCESSLIST_STATE ORDER BY COUNT(*) DESC;performance_schema的好处是能拿到更细的信息,比如线程类型、所属连接 ID、执行状态等,而且查询时本身不占用普通连接资源,非常适合故障期间使用。相比之下,反复执行show processlist在高压力下也可能增加一点点开销,可以用,但要克制。
5.2 一次真实故障的拆解:连接数没到上限却全部卡死
说一个我记忆很深的案例。有一次某个系统的数据库连接数稳定在 700 左右,max_connections配的是 2000,看着离上限很远,但业务端反馈所有请求都超时,数据库 CPU 才用了 30%。
我先跑了一遍show processlist,发现大量连接的State是Waiting for table metadata lock,还有一部分是Sending data。这个状态组合立刻让我意识到问题不在于连接数,而在于元数据锁。后续查下去,发现是凌晨有人跑了一个长时间未提交的 DDL 或其他事务,一直持有表的元数据锁,后续所有涉及该表的 DML 全部堵在锁等待上,连接越积越多,最终把应用侧连接池占满。
这个案例给我们的启发是:连接数只是表象,连接堆积的位置才是核心。同样是连接数上升,可能是查询慢、锁等待、网络问题、认证风暴,甚至是磁盘 IO 卡顿导致的 hang。如果把连接数当唯一指标,很容易走错方向。我后来的习惯是同时盯Threads_running和Threads_connected:Threads_connected高但Threads_running低,说明连接在空转,方向在应用侧或锁等待;两者都高,才说明数据库真的在承受并发压力。
5.3 临时救急手段和长期方案怎么选
连接数爆掉的时候,最紧急的一件事是让数据库先喘过气来。除了用预留连接登录之外,还有一种方式是动态调大max_connections而不重启数据库:
SET GLOBAL max_connections = 3000;这个操作能立刻缓解一部分压进来的连接请求,但要明白它只是给系统争取了排查时间。真正要做的是根据前面的排查链路找到连接暴涨的原因,否则临时调大的参数会在半小时后被新的连接风暴再次打满。
有些团队喜欢直接重启数据库来清连接,这是我的下下策。重启虽然能把所有连接清掉,但代价是缓存失效、未提交事务回滚、连接风暴可能马上重演,而且重启期间业务是彻底不可用的。除非连 root 都进不去且无法通过任何方式恢复,否则不建议走这一步。
长期方案我更看重这几件事:给应用连接池设置合理的容量和超时;给数据库账号按业务设置max_user_connections;部署连接数监控并在接近阈值时提前告警;对慢查询和锁等待做持续治理,减少连接被无效占用的概率。把这四件事做到位,比单纯调大一个参数要有效得多。
6. 不同负载场景下的连接数规划:从几十到上万怎么给
6.1 轻量级系统和内部工具:150 到 500 足够
很多人一上来就想把连接数调到几千,但大部分内部系统、管理后台、报表系统根本不需要那么多。这类应用的并发请求量有限,连接池也不需要配很大。连接数设成 150 到 500 已经留足了余量,而且这个区间的连接数不会对系统资源造成太大压力,出问题的概率低。
我见过不少内部系统因为连接数配置过大反而引出问题的情况。举个例子,一个几十人使用的后台,连接池maxActive竟然配了 800,数据库max_connections也调到了 2000。平时没事,但每次应用发布时,连接池预热的几百个连接同时涌进来,再加上其他系统,立刻就能把数据库的连接负载拉得很高。数据库其实根本不忙,纯粹是连接分配不合理。
对这类场景,我的建议是先把max_connections保持在默认值或稍高一些,然后根据监控数据逐步调整。连接池的初始化连接数设置为 5 到 10,最大连接数设置为 50 到 100 就已经很充裕。要记住连接数是一种资源,不用的连接不应该被建立。
6.2 标准 Web 业务:如何根据核心指标推算
以一个典型的 Web 应用为例,假设你有 20 个应用实例,每个实例的连接池最大连接数是 50,那么理论上最大连接需求是 1000。数据库的max_connections至少要高于这个数字,同时还要留出运维查询、备份、监控等额外连接的空间。在这种情况下,2000 到 3000 是比较常见的选择。
但"高于"不是唯一原则。你还需要估算数据库单机能承载的并发执行能力。一个粗略的判断方法是看Threads_running。如果数据库的Threads_running长期维持在 CPU 核心数的两三倍以上,说明执行层已经过载,再多的连接也只会排队。连接数规划不能只看应用侧需求,还要看数据库侧的处理能力。
连接池参数方面,HikariCP 这种轻量级连接池官方文档里的建议很直白:最大连接数等于数据库最大可用连接数除以应用实例数,再留出适当余量。很多性能专家也提出过一个经验公式:连接数等于后端核心数加一到两倍,用来把 CPU 喂饱但又不至于让线程上下文切换过多。当然这只是起点,最后还是靠压测和监控校准。
6.3 高并发或者分库分表场景:连接数是全局视角
到了高并发场景,单库能承载的连接数迟早会成为瓶颈。这个时候的解决方案不是继续调大单实例连接数,而是引入分库分表或读写分离,把连接压力分散到多个 MySQL 实例上。一个实例撑不住一万个活跃连接,但十个实例每个撑住一两千就很容易。
分库分表之后要特别注意全局连接数的协调。曾经有个项目在改造前期没有估算总量,结果每个分片都按单库承载能力配连接池,所有分片的连接数之和远远超过任何一个物理数据库能承受的范围。改造上线后出现一个诡异的现象:每个数据库的连接数看着都不到告警线,但整个系统大量请求卡顿。后来一查,才发现同一个应用连接池的请求被均匀打到了所有分片,而部分分片所在的物理机资源已经耗尽。这个问题靠监控单个数据库是发现不了的,必须做全局的连接数大盘。
读写分离场景下,连接数的分配逻辑也不一样。只读实例往往要承担更大的查询并发,但查询通常比较短,线程占用的时间也短。写实例的连接数不需要特别夸张,慢查询和事务锁往往集中在写入端,需要额外关注长事务和Sleep连接的比例。把读和写放在同一套连接规划里是常见的失误,应该分开设计、分开监控。
7. 从一知半解到能自己掌控连接数
聊到这里,核心的东西基本都过了一遍。最后分享一个我个人的判断标准:当你看到一个系统连接数很高,不要先急着评价或者调参,而是先问自己三个问题:连接是活跃的还是空转的?瓶颈在 MySQL 还是在操作系统?应用侧有没有可能优化掉一部分不必要的连接?这三个问题的答案,往往比参数本身更能说明问题。
随手抛一个很多人忽略的检查技巧:MySQL 重启之后,Max_used_connections会被清零,但performance_schema的events_statements和status_by_thread里仍能查到历史峰值。如果想知道系统曾经扛过多大的连接峰值,启动后记录一下这个状态值,它会成为你后续规划连接数的重要参考。我自己做容量规划时,一定会保留最近三个月内每周的Max_used_connections曲线,比拍脑袋给数字靠谱得多。
调连接数这件事,说到底是让数据库在"够用"和"可控"之间取一个平衡。盲目调大往往只是把风险从一个晚上推迟到另一个晚上。真正稳健的做法,是搞清楚每个连接背后消耗的资源链路,然后用监控数据说话,一层层把这个数校准到和业务、机器都匹配的位置。希望这篇梳理能帮你少踩几次坑,下次再有人问你 MySQL 最多多少连接时,你能从他问的是默认值、配置值还是实际承载值开始反问一句"你想问的是哪一个",然后真正把这个问题聊透。