ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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

MySQL 最大连接数(max_connections)从默认的 100 调到 16384,在我看来,这已经不算“调个参数”,而更接近一次带有明确风险边界的架构变更。这些年我亲手处理过至少三次跟这个数字直接相关的事故:一次是参数改大重启后实例起不来,错误日志里孤零零一行 Can't 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 connections
max_user_connections单个账号维度0(表示不限制)ERROR 1203: User already has more than 'max_user_connections' active connections
max_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_size,JOIN 用 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 16KB,performance_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_schema,max_connections 调大会连带影响它的内存预留。8.0 里 P_S 会为每个可能的线程预留实例记录,16384 这个量级下,光这一项的增量就可能到几百 MB。如果这台实例不靠 P_S 做监控,可以考虑把performance_schema_max_thread_instances显式设成一个合理值,而不是让它跟随 max_connections 自动放大。

3. 文件描述符,这才是改参数前更硬的那道墙

3.1 “Can't create a new thread” 的真实来源

绝大多数把 max_connections 往上调的人,第一次重启都会撞上这个错误,日志里大概是这个形式:

[ERROR] Can't 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 也没用,日志里只会留一行 warning:Could 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] LimitNOFILE=65535 LimitNPROC=65535

写完之后必须执行systemctl daemon-reload再重启服务,否则配置不生效——这一步漏掉的人非常多,因为 systemd 不会给你任何提示。

容器环境则是另一套写法。Docker 默认继承宿主的部分限制,但更可靠的方式是在启动时显式指定:

docker run -d \ --ulimit nofile=65535:65535 \ --ulimit nproc=65535: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 = 2048

back_log 这个参数值得单独说一句。它控制的是 TCP 连接建立完成但还没来得及分配线程时,内核 accept 队列能排多少人。连接数调大之后,如果 back_log 太小,高并发建连的瞬间会出现客户端连不上但服务端看起来毫无压力的怪现象。MySQL 5.6.6 之后 back_log 默认会跟随 max_connections,但内核那边的net.core.somaxconn又是一道上限,两边都要看:

sysctl -w net.core.somaxconn=4096 sysctl -w net.ipv4.tcp_max_syn_backlog=8192

改完记得写进/etc/sysctl.conf做持久化。这几项我在压测环境里验证过:不调 somaxconn,连接数上到两三千时就会出现偶发建连超时,调完之后曲线立刻平滑。

验证是否真的生效,两条命令足够:

# 看进程实际的 limits cat /proc/$(pgrep -o mysqld)/limits | grep -E "open files|processes" # 看当前打开的 fd 数量 ls /proc/$(pgrep -o mysqld)/fd | wc -l
SHOW 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 的配置读取有一套固定顺序,同一个参数在多个文件里出现时,后读的覆盖先读的:

  1. /etc/my.cnf
  2. /etc/mysql/my.cnf
  3. $MYSQL_HOME/my.cnf
  4. ~/.my.cnf(针对当前用户)
  5. 启动命令行里显式指定的--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 --concurrency=2000 --iterations=1 \ --number-of-queries=2000 \ --host=127.0.0.1 --user=test --password=*** \ --create-schema=test --query="SELECT 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_timeout:

SET 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_NAME='Threads_connected') AS connected, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_running') AS running, (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Max_used_connections') AS peak;

跑上两周,把 P99 乘 1.5 当作目标容量,再跟当前 max_connections 比。这个数比任何经验公式都靠谱。

6.2 应用侧连接池必须同步配合

数据库侧算出来的目标容量,要落到应用侧才算数。常见的连接池配置长这样:

maximumPoolSize=50 minimumIdle=10 connectionTimeout=3000 idleTimeout=600000 maxLifetime=1500000

这几个值怎么定,其中 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 和内核参数,然后改配置文件、重启、四项验证,最后盯着指标看一周。跳过任何一步,都可能在某个不合适的时刻被补回来。

返回列表