2026/11/2 20:15:40

MySQL最大连接数调到16384:内存、文件描述符与连接池实践

MySQL最大连接数调到16384:内存、文件描述符与连接池实践 MySQL 最大连接数max_connections从默认的 100 调到 16384在我看来这已经不算“调个参数”而更接近一次带有明确风险边界的架构变更。这些年我亲手处理过至少三次跟这个数字直接相关的事故一次是参数改大重启后实例起不来错误日志里孤零零一行 Cant create a new thread一次是连接数上去了内存跟着上去凌晨被系统的 OOM 机制清理掉最冤的一次是配置文件改了三遍都不生效折腾两个多小时才发现 mysqld 读的根本不是我以为的那个文件。这三件事都有一个共同点动手的人只盯着 max_connections 这一个数字而忽略了它背后必须同步调整的一整套依赖。这篇内容写给三类人被 Too many connections 打断过线上发布的后端接手老系统后发现连接池配置离谱的运维以及正准备把连接数一次性拉到 16384 但还没算清楚代价的开发者。我不会只丢给你一行配置文件了事真正让这次调整成功的是版本基准、内存预算、文件描述符、连接池配置、监控指标这五件事一起对齐。下面按我实际的动手顺序走先把“默认 100”这个前提校准再算内存账再解决更早撞上的那道墙然后才是改数和验证最后讲打满时怎么排查、到底该设多少、以及几个花钱买来的教训。1. “默认100”这个前提得先校准否则第一步就偏了1.1 版本演进里的默认值变迁与三个容易混淆的参数很多人一上来就问“我的 max_connections 是 100是不是默认值”这里有个很实际的偏差。MySQL 官方版本里5.1 及更早的默认值确实是 100但从 5.5 开始就改成了 151一直延续到 8.0。所以如果你在一台机器上看到 100大概率不是官方默认值在起作用而是某个人在 my.cnf 里手工写死了这一行或者用的是某个打包发行版自带的配置模板。这个区分很重要。前者意味着你必须去改配置文件才能永久生效后者意味着你改客户端配置或者服务端配置要分清对象。我见过太多人在命令行敲完SET GLOBAL max_connections 2000压测跑通就收工结果下一次滚动重启全部回滚到 100故障原地复现。动手之前先把三个名字长得像、作用范围完全不同的参数理清楚。它们混在一起看是排查连接问题时最常见的误判来源。参数作用范围5.7/8.0 默认值超限时的典型报错max_connections整个实例所有账号共享151ERROR 1040: Too many connectionsmax_user_connections单个账号维度0表示不限制ERROR 1203: User already has more than max_user_connections active connectionsmax_connect_errors单个客户端主机维度100Host x.x.x.x is blocked because of many connection errors这三者的关系值得展开说一句。max_connections 是所有连接的总闸门打满之后新连接直接被拒但已经连上的会话不受影响继续正常工作。max_user_connections 是给单个账号设的限额典型场景是给某个只读报表账号限流防止它把总连接池吃光。max_connect_errors 最容易被人忽略它统计的是“握手阶段失败次数”一旦某个客户端主机的累计失败次数注意是连续失败中断计数成功连接会清零超过阈值这个 IP 会被直接拉黑之后哪怕密码正确也连不上必须执行FLUSH HOSTS才能解锁。我遇到过一回一个应用因为配置里写了错的库名连接池启动时疯狂重连几百次失败把主机的错误计数顶满结果同一台机器上另一个正常业务也连不上了报错信息还特别有迷惑性。那次之后我养成了一个习惯只要做连接数相关的调整顺手把 max_connect_errors 从 100 抬到 1000成本极低能省掉一次深夜排查。1.2 托管环境会按规格重算别拿本地经验直接套如果你用的是托管型数据库服务情况会更复杂一层。这类服务的 max_connections 往往不是固定值而是跟实例规格绑定的常见做法是按内存大小乘一个系数再取整比如每 GB 内存对应若干连接规格越大允许的上限越高。你在控制台或者参数模板里改可能受规格上限约束也可能要求重启才生效还有些服务会明确提示调大连接数会影响实例的稳定性评级。我在一个项目里就吃过这个亏本地测试环境按 16384 调好脚本跑得挺欢上了托管实例之后发现参数模板里那一项根本改不上去一直报超出规格限制。最后只能反过来做把 16384 拆成“应用侧连接池收紧 实例规格升一档”才落下来。所以动手之前的第一步永远是确认边界SELECT version;看版本SHOW VARIABLES LIKE max_connections;看当前值再确认这个实例是不是托管服务、参数能不能改、改完要不要重启。这三件事花不到两分钟但能避免后面几小时的返工。2. 16384 个连接的内存账本得先算清楚再动手2.1 常驻开销与峰值开销必须分开算网上流传最广的一种说法是“每个连接占 10MB 内存”按这个乘 16384结论是 160GB然后直接被吓退。这个算法方向没错但数值严重失真原因是它把“按需分配”的缓冲区和“连接建立就占用”的内存混在了一起。MySQL 的每连接内存其实分成两摞。第一摞是常驻的连接一建立就基本确定线程栈由 thread_stack 控制默认 1MB但这是虚拟地址空间的预留值Linux 上实际提交的物理页通常是几百 KB 甚至更少连接收发缓冲由 net_buffer_length 控制默认 16KB再加上 performance_schema 为每个线程维护的记录这部分在 8.0 里不算小。第二摞是峰值的只在特定操作发生时临时分配排序用 sort_buffer_sizeJOIN 用 join_buffer_size全表扫描用 read_buffer_size范围扫描用 read_rnd_buffer_size事务写 binlog 用 binlog_cache_size。这些缓冲区用完就释放或者复用不是每个连接都常驻一份。把这个区别想明白估算方法就清楚了常驻部分乘以 max_connections峰值部分乘以“同一时刻真正在干活的并发数”。后者通常是连接数的百分之几到百分之十几这才是真实的账。2.2 一套可以直接套用的估算方法与参数妥协表我一般用下面这个公式做粗算误差控制在 30% 以内足够判断“这台机器扛不扛得住”常驻内存 ≈ max_connections × (线程栈实际提交 net_buffer_length P_S 线程记录) 峰值内存 ≈ 活跃并发数 × (sort_buffer_size join_buffer_size read_buffer_size read_rnd_buffer_size) 总额 常驻 峰值 innodb_buffer_pool_size 其他固定开销代入一组典型值线程栈实际提交按 256KB 算net_buffer_length 16KBperformance_schema 线程记录按 100KB 估那么每个连接的常驻开销大约 372KB。16384 个连接乘下来约 5.8GB。峰值部分假设业务高峰真正同时执行的连接有 500 个每个连接按 896KB 的缓冲总量算约 450MB。两项加起来 6.3GB 左右再加上 innodb_buffer_pool_size 和操作系统本身一台 32GB 内存的机器是完全撑得住的。但这里有个致命前提上面那组缓冲参数必须保持在默认量级。我见过不少老配置文件里写着sort_buffer_size 4M、join_buffer_size 8M这种配置在几十个连接的年代没什么问题一旦连接数上到四位数500 个并发排序就是 2GB 起步。所以大连接数场景下这些参数的方向必须是往小调而不是往大调。参数8.0 默认值大连接场景建议调整理由thread_stack1MB保持不动实际提交远小于预留值动它没意义net_buffer_length16KB保持不动太小会导致频繁扩容得不偿失read_buffer_size128KB64KB - 128KB只影响全表扫描业务上应尽量避免read_rnd_buffer_size256KB128KB - 256KB排序后回表用适度下调安全sort_buffer_size256KB128KB - 256KB每个排序连接独立分配最该盯紧的一个join_buffer_size256KB128KB - 256KB无索引 JOIN 才会用本质是慢 SQL 信号binlog_cache_size32KB32KB - 64KB大事务会溢出到磁盘临时文件反而更慢还有一块容易漏算的如果实例开启了 performance_schemamax_connections 调大会连带影响它的内存预留。8.0 里 P_S 会为每个可能的线程预留实例记录16384 这个量级下光这一项的增量就可能到几百 MB。如果这台实例不靠 P_S 做监控可以考虑把performance_schema_max_thread_instances显式设成一个合理值而不是让它跟随 max_connections 自动放大。3. 文件描述符这才是改参数前更硬的那道墙3.1 “Cant create a new thread” 的真实来源绝大多数把 max_connections 往上调的人第一次重启都会撞上这个错误日志里大概是这个形式[ERROR] Cant create a new thread (errno 11); if you are not out of available memory...看到它很多人的第一反应是“内存不够”然后去加机器其实方向跑偏了。errno 11 在 Linux 上是 EAGAIN最常见的两个成因是文件描述符耗尽RLIMIT_NOFILE和进程/线程数超限RLIMIT_NPROC内存不足只是排在后面的可能。机制上讲Linux 的 NPTL 里每个 MySQL 连接对应一个内核调度实体同时这个连接持有的 socket 也要占一个文件描述符除此之外每打开一张表、每打开一个 binlog 文件、监听端口、临时文件全都要 fd。所以 MySQL 对 fd 的需求不是简单地等于 max_connections而是 max_connections 加上表缓存相关的开销。MySQL 自己会尝试把 open_files_limit 抬到一个它认为够用的值计算方式大致是10 max_connections table_open_cache × 2但这个尝试受进程的 ulimit 硬限制约束。也就是说只要 ulimit 卡在 1024你在 my.cnf 里写一万遍 open_files_limit 也没用日志里只会留一行 warningCould not increase number of max_open_files to more than 1024 (request: 24586)。这行 warning 是我排查这类问题的第一抓手看到它基本就能定性。按 16384 连接、table_open_cache 4000 来算需求是10 16384 8000 ≈ 24.4K实际配置要留足余量设 65535 是比较稳妥的选择。3.2 systemd、容器、open_files_limit 的三种落地写法现代 Linux 发行版基本都用 systemd 托管服务直接在 shell 里敲ulimit -n 65535对服务是无效的那只影响当前 shell 会话。正确做法是给服务写一个 override# /etc/systemd/system/mysqld.service.d/limits.conf [Service] LimitNOFILE65535 LimitNPROC65535写完之后必须执行systemctl daemon-reload再重启服务否则配置不生效——这一步漏掉的人非常多因为 systemd 不会给你任何提示。容器环境则是另一套写法。Docker 默认继承宿主的部分限制但更可靠的方式是在启动时显式指定docker run -d \ --ulimit nofile65535:65535 \ --ulimit nproc65535:65535 \ --name mysql-test \ mysql:8.0如果用的是编排系统一般是在容器定义的 securityContext 或者 resources 段里设不同平台的字段名不一样以实际文档为准但核心是那两个 limit别只设 soft 不设 hard。配置文件这一侧还要把 open_files_limit 显式写上避免 MySQL 自己算出来的值偏小[mysqld] max_connections 16384 table_open_cache 4000 open_files_limit 65535 back_log 2048back_log 这个参数值得单独说一句。它控制的是 TCP 连接建立完成但还没来得及分配线程时内核 accept 队列能排多少人。连接数调大之后如果 back_log 太小高并发建连的瞬间会出现客户端连不上但服务端看起来毫无压力的怪现象。MySQL 5.6.6 之后 back_log 默认会跟随 max_connections但内核那边的net.core.somaxconn又是一道上限两边都要看sysctl -w net.core.somaxconn4096 sysctl -w net.ipv4.tcp_max_syn_backlog8192改完记得写进/etc/sysctl.conf做持久化。这几项我在压测环境里验证过不调 somaxconn连接数上到两三千时就会出现偶发建连超时调完之后曲线立刻平滑。验证是否真的生效两条命令足够# 看进程实际的 limits cat /proc/$(pgrep -o mysqld)/limits | grep -E open files|processes # 看当前打开的 fd 数量 ls /proc/$(pgrep -o mysqld)/fd | wc -lSHOW VARIABLES LIKE open_files_limit;如果 SQL 查出来的值明显小于你配的数字回到上面那行 warning 去找原因八成还是 ulimit 没放开。4. 改数的两条路径和四种验证缺一个都算没改成4.1 运行时生效和启动时生效的边界修改 max_connections 有两条路径用途完全不同必须分清。第一条是运行时修改SET GLOBAL max_connections 16384;这条命令立即生效不需要重启但重启后失效。它需要 SYSTEM_VARIABLES_ADMIN 权限MySQL 8.0或 SUPER 权限5.7 及更早。这条路径的正确用法是“紧急扩容”——线上告警了先临时顶上然后再去改配置文件。我见过有人把它当成正式修改手段结果一次机房搬迁之后所有实例打回原形。第二条是配置文件修改写进 my.cnf 的 [mysqld] 段重启生效。这是唯一能长期稳定的方式。这里有个方向性的安全常识值得强调从大往小改是危险的从小往大改是安全的。调大 max_connections 不会立刻分配内存只是把上限抬高了调小则不会主动杀掉现有连接但会让新连接在被拒之前就撞墙而且如果当前连接数已经超过新上限那些连接会一直挂着直到自然结束中间的监控会非常难看。所以缩小连接数必须配合应用侧连接池先降下来或者选在低峰期做。另外补一句权限细节。MySQL 8.0 出于安全考虑拆分了 SUPER 权限很多老运维脚本里的GRANT SUPER在 8.0 上不给力正确的语法是GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO ops_user%;这个权限点很小但很关键尤其在需要临时救火的场景下手里没有这个权限就只能干瞪眼。4.2 mysqld 到底读了哪个配置文件这是我在开头提到的那次“改三遍不生效”事故的根源。MySQL 的配置读取有一套固定顺序同一个参数在多个文件里出现时后读的覆盖先读的/etc/my.cnf/etc/mysql/my.cnf$MYSQL_HOME/my.cnf~/.my.cnf针对当前用户启动命令行里显式指定的--defaults-file最稳的确认方式是直接问 mysqld 自己mysqld --verbose --help 2/dev/null | grep -A1 Default options输出会列出它实际读取的配置文件顺序和路径。或者用mysqld --print-defaults这会打印出所有最终生效的启动参数包括从配置文件解析出来的和命令行传进来的一眼就能看出有没有被别的东西覆盖。还有一个隐蔽的坑是!includedir。很多发行版的 my.cnf 里会写一句!includedir /etc/mysql/conf.d/这意味着该目录下的所有.cnf文件都会被按字母序读取并且后读的覆盖先读的。如果你在这个目录里放了a-tuning.cnf和z-tuning.cnf两个都写了 max_connections最终生效的是 z 那个。更麻烦的是有些安装包会在这里预置一个文件把 max_connections 写死成 100你在主配置文件里怎么改都没用——这正是“默认 100”在很多生产环境里的真实来历。排查这类问题的顺序我一般是先--print-defaults看最终值再grep -r max_connections扫一遍所有配置目录两条命令交叉验证基本五分钟内定位。4.3 改完之后必做的四项验证参数改完重启很多人的验证就是敲一句SHOW VARIABLES LIKE max_connections看到数字对了就收工。这只验证了“参数被读到”没验证“系统真的扛得住”。我一般会做完整的四项验证项命令通过标准参数生效SELECT GLOBAL.max_connections;等于目标值文件句柄SHOW VARIABLES LIKE open_files_limit;配合 /proc 检查大于 max_connections table_open_cache × 2建连压测并发建连脚本或压测工具目标并发下无 ERROR 1040、无 errno 11错误清零SHOW STATUS LIKE Connection_errors_max_connections;压测前后不增长建连压测不用搞得很复杂mysqlslap 就能干mysqlslap --concurrency2000 --iterations1 \ --number-of-queries2000 \ --host127.0.0.1 --usertest --password*** \ --create-schematest --querySELECT 1这个命令会在同一时刻拉起 2000 个并发连接跑一轮简单查询。观察重点不是 QPS而是执行过程中有没有报错、结束后 Threads_connected 能不能正常回落。如果回落很慢说明连接释放路径有问题后面第五节会展开。5. 连接打满时的排查顺序从现象倒推到根因5.1 三条 SQL 看清连接到底被谁占着线上报 Too many connections 的第一分钟最忌讳的是直接冲上去调参数。连接数打满只是症状十倍以上的情况是应用侧连接泄漏或者慢查询堆积盲目调大只会把问题往后推顺便把内存也搭进去。我的第一条命令永远是按来源分组统计SELECT user, host, command, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, host, command ORDER BY cnt DESC LIMIT 20;这条查询一次性回答三个问题是哪个账号在占、来自哪台机器、连接处于什么状态。如果发现某个 host 独占九成连接问题基本就锁定在那台应用服务器上了接下来去查那个进程的连接池配置或者代码里的连接释放逻辑比在数据库侧瞎猜高效得多。第二条把 IP 剥出来看分布因为 host 字段通常带端口号SELECT SUBSTRING_INDEX(host, :, 1) AS client_ip, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY client_ip ORDER BY cnt DESC;第三条是看全局计数器这几个指标是判断问题性质的关键SHOW STATUS LIKE Threads_connected; -- 当前连接数 SHOW STATUS LIKE Threads_running; -- 当前活跃执行数 SHOW STATUS LIKE Max_used_connections; -- 历史峰值 SHOW STATUS LIKE Aborted_connects; -- 握手阶段失败次数 SHOW STATUS LIKE Connection_errors_max_connections; -- 因打满被拒次数Threads_connected 高但 Threads_running 很低是连接闲置堆积的典型特征指向池配置或者超时设置两个都高那是真有大量并发在执行得去看慢查询和锁等待。这两个数字的比值比连接数本身更能说明问题。5.2 Sleep 连接堆积最典型的连接泄漏信号接着上一条往下挖专门看长时间 Sleep 的连接SELECT id, user, host, time, command, state FROM information_schema.processlist WHERE command Sleep AND time 300 ORDER BY time DESC LIMIT 30;如果结果里出现几十上百条 time 在几千秒以上、来源高度集中的记录那几乎可以确定是连接池没有回收。正常工作的连接池空闲连接会被 idleTimeout 踢掉如果踢不掉要么是池里配了minimumIdle且数值很大要么是代码里拿连接的方式有问题——比如手动 getConnection 之后忘了 close或者用了某个框架的事务管理没配好。数据库侧能做的兜底是缩短 wait_timeoutSET GLOBAL wait_timeout 1800; SET GLOBAL interactive_timeout 1800;默认值 28800 秒是整整 8 小时对现代应用来说太长了一个泄漏的连接要挂 8 小时才被清掉期间它一直占着名额。改成 600 到 1800 秒之间比较合适。但这里有个必须注意的联动wait_timeout 一旦改小应用连接池的 maxLifetime 必须跟着改到比它更小否则会出现“池以为连接还活着服务端已经把它踢了”的情况表现是偶发的 Communications link failure很难排查。这两个参数还有一个容易搞混的地方通过命令行客户端连进去生效的是 interactive_timeout通过 JDBC 这类非交互式连接连进去生效的是 wait_timeout。所以两个都要设只改一个会漏。5.3 握手阶段失败是另一条独立的故障线Aborted_connects 增长是另一类问题的信号它统计的是“连接在握手完成前就断了”。常见成因有四类认证信息错误、客户端和服务端的字符集或 SSL 协商不匹配、max_connect_errors 触顶导致主机被拉黑、以及网络中间层负载均衡、连接代理把空闲连接提前掐断。区分方法很直接如果错误日志里出现Host x.x.x.x is blocked那就是 max_connect_errors 触顶处理方式是FLUSH HOSTS; SET GLOBAL max_connect_errors 1000;后者要写进配置文件才能持久。这里有个特别实用的救援手段值得记住MySQL 8.0.14 之后支持独立的管理端口配置如下[mysqld] admin_address 127.0.0.1 admin_port 33062这个端口上的连接不计入 max_connections也就是说连接池被打满的时候你还能从这个口子登进去执行 KILL 或者调参数不用去重启实例。我现在的生产模板里默认就加这两行用不上最好用上一次就值回票价。如果没有管理端口8.0 之前的版本还有个小机制会给拥有 SUPER 或 CONNECTION_ADMIN 权限的账号保留一个额外连接名额也就是实际可连数是 max_connections 1。这个口子在紧急情况下能救命但前提是你得有一个具备该权限的账号并且密码是你能拿到的——很多团队把这个账号密码锁在某个不常更新的地方真出事时找不到。6. 到底该设多少靠压测数据和连接池反推6.1 Max_used_connections 的正确读法最常见的判断依据是 Max_used_connections 与 max_connections 的比值超过 80% 就认为该扩容了。这个经验法则大方向对但有两个陷阱。第一个陷阱是它不会衰减。这个计数器记录的是实例启动以来的历史峰值除非重启否则永远停在那。如果半年前有过一次大促尖峰打到 3000从那以后业务其实平稳在 500 左右这个值依然是 3000你会一直误以为自己“快满了”。正确做法是采集 Threads_connected 的时间序列看 P95 和 P99而不是看一个静态峰值。第二个陷阱是它不区分连接的有效性。如果池里配了 500 个最小空闲连接那 Threads_connected 至少有 500这个基线会一直占着挤压真实的余量判断。采时间序列不复杂写个定时任务每分钟采一次就行SELECT NOW() AS ts, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEThreads_connected) AS connected, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEThreads_running) AS running, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEMax_used_connections) AS peak;跑上两周把 P99 乘 1.5 当作目标容量再跟当前 max_connections 比。这个数比任何经验公式都靠谱。6.2 应用侧连接池必须同步配合数据库侧算出来的目标容量要落到应用侧才算数。常见的连接池配置长这样maximumPoolSize50 minimumIdle10 connectionTimeout3000 idleTimeout600000 maxLifetime1500000这几个值怎么定其中 maxLifetime 最容易被忽略。它必须小于 MySQL 的 wait_timeout并且差值要留出余量。假设 wait_timeout 设的是 1800 秒maxLifetime 就设 1500000 毫秒25 分钟这样连接池会主动在服务端踢人之前把连接换掉避免拿到死连接。这个配置如果不配默认值往往是 30 分钟跟 8 小时默认 wait_timeout 搭配着用倒也没事但一旦有人把 wait_timeout 调小问题就来了。maximumPoolSize 这个值我一直反对拍脑袋定大。业界的经验公式是连接数 CPU 核心数 × 2 磁盘数SSD 环境下磁盘数按 1 算也就是说一台 8 核机器跑 17 个连接左右就够把数据库喂饱了。当然这个值跟业务特征强相关长事务多的场景需要更多但几十个已经是很宽裕的数字。如果池里配了 200实际上可能只有 20 个在同时干活剩下 180 个全是占着名额不干活的闲置连接。反过来推算全局容量就清晰了所需 max_connections 应用实例数 × 单实例池上限 × 冗余系数 运维监控预留举例20 个应用实例每个池上限 50冗余系数 1.3再加 50 个给监控、备份、运维手动登录结果约 1350。把 max_connections 设成 2000 已经很宽裕16384 完全是浪费。所以当有人提出“必须上 16384”时我会反过来问一句是真有 16384 个连接同时在工作还是应用实例数乘以池上限的数字加起来接近这个数如果是后者问题在池配置不在数据库上限。把每个池从 100 收到 30总需求立刻降三分之二压测一遍如果吞吐不掉那就是白赚。我经手的案例里十次有八次走的是这条路而不是往上加数字。7. 几次真金白银换来的教训7.1 集群和多实例环境下的参数漂移单机环境改完就完事集群环境完全是另一回事。主从架构里主库和从库的配置文件是各自独立的你只改了主库从库还是老值平时看不出来一切换主从问题就爆出来。组复制这类多节点方案更麻烦大部分参数要求在节点间保持一致不一致可能导致节点被踢出集群。我的处理方式是max_connections 这类参数必须落到配置管理的模板里不能手工改。模板改一次全节点推一遍同时确认推送结果SELECT hostname, global.max_connections, global.open_files_limit, global.table_open_cache;用一条查询把所有节点的关键参数拉出来对比谁没对齐一目了然。这个习惯也是被坑出来的有次做双主切换演练切过去之后连接数半小时内打满查了半天才发现新主库的那个参数没被模板覆盖还停留在几年前的旧值。托管环境还有一个额外注意点参数模板的绑定关系。有时候同一个模板被多个实例引用你以为是给 A 实例调参实际动的是所有引用了这个模板的实例。改动前一定要确认模板的引用清单或者干脆给关键实例单独建模板。7.2 连接数放开之后才暴露的连锁问题把上限从 100 抬到 16384从来不是“改完就好”它会连带触发一串之前被上限压住的问题。我按踩到的顺序列一下。内存是最直接的。前面算过16384 按常驻 372KB 算就是 5.8GB如果 sort_buffer_size 这类参数被人手工调大过实际占用能翻好几倍。上线前一定要在预发环境跑一轮满载压测观察 swap 使用量和系统是否触发内存回收。线程调度开销是第二层。每个连接一个线程几千个活跃线程放在同一个进程里上下文切换本身就是成本。可以通过vmstat 1观察 cs上下文切换列如果数字跟连接数一起飙升说明瓶颈已经从数据库内部转移到了调度层。这时候再往上加连接数是没有意义的得从业务侧减少并发。InnoDB 内部竞争是第三层也是最隐蔽的。连接数上去之后事务并发增加行锁冲突、undo log 膨胀、purge 线程跟不上这些在低并发时完全看不出来的问题会一起冒出来。表现是 Threads_running 很高但 QPS 不涨这时候要去看SHOW ENGINE INNODB STATUS里的 lock wait 信息和information_schema.innodb_trx里的事务年龄。监控采集本身也会变成负担。如果你采我前面那个定时任务一分钟一次问题不大但有些监控系统是秒级采集SHOW PROCESSLIST而 PROCESSLIST 在连接数几千的时候开销不小。我就见过监控把数据库压出问题的案例后来把采集频率降到 15 秒并改走 performance_schema 才缓解。最后是慢查询日志。连接数一放开之前因为排队被挤掉的慢查询会全部显形日志文件几天就能涨到几十 GB。上线前就要把long_query_time和log_output调整好该限流限流、该关表关表别等磁盘写满了才发现。回到开头那个问题。MySQL 最大连接数从 100 调到 16384技术上只是配置文件里的一行但它牵扯的是内存预算、文件句柄、连接池配置、集群一致性、监控策略这一整套东西。我现在做这件事的固定顺序是先算账再改 ulimit 和内核参数然后改配置文件、重启、四项验证最后盯着指标看一周。跳过任何一步都可能在某个不合适的时刻被补回来。