350 台应用服务器2 个数据库集群每个集群 6 个分库每个分库minPoolSize5……当这些数字叠加在一起时每个数据库集群的Threads_connected轻松突破 10000数据库瞬间进入高压模式。这次告警我作为稳定性负责人临时接手排查——服务并非我设计日常也非我维护但正是这种“旁观者”视角让我看清了连接池配置与分库数量之间被长期忽视的乘积炸弹。一、事故现场还原某天下午监控系统突然爆出两条告警数据库连接使用率超过 70%两个 MySQL 集群各自的Threads_connected均突破 10000具体数值集群 A 约 10,500集群 B 约 10,200双双超过预设的 10,000 告警阈值当时我作为稳定性负责人被拉进紧急会议开始排查这套我从未参与设计、日常也不负责维护的服务。初步了解架构后发现应用使用了分库分表数据访问层采用美团开源的 Zebra 数据中间件。Zebra 会为配置的每个物理数据库每个分片独立创建并维护一个连接池应用服务器直连物理 DB 的数量等于分片数。架构细节2 个数据库集群cluster_0 / cluster_1每个集群下各有6 个物理数据库db_0 ~ db_5。Zebra 配置了这两组数据源每个应用服务器启动后会建立2 × 6 12 个独立的 C3P0 连接池。每个连接池C3P0的配置为initialPoolSize5 minPoolSize5 # 关键每个连接池启动后会至少保持 5 个空闲连接 maxPoolSize30数据库服务器配置每个集群的数据库实例运行在独享容器中规格为24核 / 140GB内存 / PCIE-SSD 2900G / 千兆网卡。这样的配置在业界已属中高端但在 10,000 连接面前仍然捉襟见肘。为什么告警值是 10,000而不是350 × 6 × 30 63,000因为连接池的minPoolSize5意味着每个应用服务器对每个物理数据库会维持至少 5 个常驻连接。扩容后连接池会立即按minPoolSize创建连接于是实际已经建立的连接数为单个集群总连接数 应用实例数 × 该集群内物理 DB 数量 × minPoolSize 350 × 6 × 5 10,500maxPoolSize30是峰值上限但由于业务并发尚未打满连接池实际保持在minPoolSize5的水平。即便如此10,500 已经远超 MySQL 集群常见的max_connections通常设为 4000~8000告警立即触发。而两个集群各自独立因此同时收到两条“超过 10000”的告警。如果把maxPoolSize30用满单个集群连接数将高达 63,000那将是灾难性的——但正是minPoolSize这个“温和”的参数已经让系统在扩容瞬间就突破了警戒线。二、根本原因分库分表下的“保底连接”爆炸 过度分片很多团队在配置连接池时只关注maxPoolSize却忽略了minPoolSize或initialPoolSize在多实例多分片场景下的乘积效应。通用公式有两个保底连接数稳定态 应用实例数 × 分片数 ×minPoolSize峰值连接数极限 应用实例数 × 分片数 ×maxPoolSize在这个案例中minPoolSize5导致每一个物理数据库都被每个应用服务器长期占用至少 5 条连接无论有没有请求。当应用实例数膨胀到 350 时单个集群的保底连接数就破万了。这解释了为什么告警不是发生在业务高峰而是在扩容后立刻出现——因为连接池初始化时就会按照initialPoolSize/minPoolSize创建连接与业务流量无关。 深度反思6 个分库的设计是否合理随着排查深入我发现一个更根本的问题为什么每个集群要分 6 个库了解业务数据后得知每个集群总数据量约 1.25T写入 QPS 峰值不超过 8000。在这样的数据量下单库 1.25T 虽然偏大但在 PCIE-SSD 140GB 内存的配置下仍可承受主要挑战是备份和 DDL 时间。写入 QPS 8000一个主库完全能轻松应对MySQL 单库写入能力可达 1.5 万。也就是说6 个分库并非出于容量或性能硬性需求而是早期设计者“为了分片而分片”或者为了未来的增长预留。但代价是连接数被乘以 6在 350 台实例下直接引爆。正确的分片数选择逻辑写入 QPS 8000 → 单库足够 → 分片数 1~2 即可。数据量 1.25T → 可接受 2 个分库每库 625GB或 3 个分库每库 416GB。6 个分库严重过度直接导致了连接数爆炸。如果当初设计为 2 个分库保底连接数仅为 350 × 2 × 5 3,500远低于告警阈值甚至可能永远不会出问题。这次经历让我深刻体会到分片数量不是越多越好每增加一个分片连接数、运维成本、故障半径都会成倍增加。分片决策必须基于真实的数据量和 QPS而不是凭空猜测。三、不是连接泄露而是“过度连接”很多同学第一反应是“连接泄露”。但从我们抓取的information_schema.processlist看SELECTSUBSTRING_INDEX(HOST,:,1)ASip,COUNT(*)FROMinformation_schema.processlistGROUPBYipORDERBYCOUNT(*)DESCLIMIT10;结果中每个 IP 在该集群内的连接数稳定在6 × 5 30左右因为实际活跃池大小接近minPoolSize且状态多为SleepTIME普遍小于 10 秒。这不是连接泄露而是连接池主动维持的空闲连接。换句话说该集群内的每一个物理 DB 都被每个应用服务器占用了至少 5 条“备勤”连接无论是否有业务请求。四、解决方案四个层次的优化策略作为临时接手排查的负责人我需要给出快速止血和长期治理的方案。最终我们从架构层、配置层、路由层、代码层逐级优化。1. 架构层长期减少分片数或引入代理最根本的解决方式是减少分片数。经评估业务写入 QPS ≤ 8000总数据量 1.25T/集群完全可以将 6 个分库合并为 2~3 个。如果合并保底连接数将降至350 × 2 × 2 1,400假设 min2问题彻底消失。如果无法快速合并需要数据迁移则引入数据库代理如 ShardingSphere-Proxy让应用只连代理代理连接后端 6 个分片。连接数变为350 × 1 × 每池连接数同样大幅下降。2. 配置层紧急止血调整initialPoolSize与minPoolSize在等待架构调整的同时我紧急修改了 C3P0 配置经过压测验证# 原配置每个物理 DB 的连接池 initialPoolSize5 minPoolSize5 maxPoolSize30 # 优化后配置 initialPoolSize2 # 启动时创建 2 个连接加快启动速度但不过度 minPoolSize2 # 最小空闲连接降为 2保底连接数下降 60% maxPoolSize20 # 峰值上限根据压测结果从 30 降至 20 maxIdleTime600 # 空闲 10 分钟回收 maxIdleTimeExcessConnections300 checkoutTimeout3000为什么选择 minPoolSize2 而不是 1压测表明在单机并发 100 TPS 时连接池的活跃连接数中位数在 2~4 之间若minPoolSize1会出现频繁的连接创建与销毁反而增加数据库负担。设置minPoolSize2恰好覆盖大部分静默流量且保底连接数从 5 降到 2单集群总连接数从 10,500 降至350 × 6 × 2 4,200降幅 60%低于 MySQLmax_connections8000的 60%安全可控。同时maxPoolSize从 30 降到 20进一步削峰。3. 路由层中期Zebra 分组路由Zebra 支持通过配置namespace或hint实现“应用分组直连部分分片”。我将 350 台服务器分为 6 组每组约 58 台通过 Zebra 的dbGroup特性让 Group0 只连接db_0Group1 只连接db_1依此类推。这样每个数据库的连接数降至58 × 2 116集群总连接数 6 × 116 696极大幅度降低。4. 代码层拒绝长事务 异步任务滥用我们在排查中发现业务代码中存在多处类似模式TransactionalpublicvoidbatchSave(ListSessionAssignassigns){// 分批 循环 单条兜底for(ListSessionAssignbatch:batches){mapper.batchInsert(batch);}}并且在某些异步场景中被CompletableFuture.runAsync()并发调用。这导致单个事务持有连接时间长达数秒连接池利用率低下间接推高了所需的maxPoolSize。优化建议拆分事务边界每批数据一个独立事务使用TransactionTemplate。异步任务必须配置有界线程池控制并发度。添加Transactional(timeout 3)强制短事务。五、每新增一个数据库连接究竟耗费哪些资源很多开发者对“连接数”的代价没有感性认识。下面从多个维度量化每增加一个 MySQL 连接带来的系统开销以 MySQL 5.7 / 8.0 为例结合我们 24核/140GB 的数据库容器配置资源类型每连接开销说明内存线程私有~256KB – 3MB每个连接对应一个线程线程栈thread_stack默认 256KB加上网络缓冲区net_buffer_length默认 16KB、临时表等实际常驻内存约 2-3MB。10000 个连接 ≈ 20-30GB 内存占 140GB 的 15-20%虽未触顶但已显著影响 buffer pool 可用空间。CPU 上下文切换随连接数线性增长大量空闲连接会导致 MySQL 的pthread调度开销增加。在 24 核机器上10000 连接时上下文切换次数可达到每秒数十万次CPU 的sy系统态占用超过 30%即使没有业务查询。InnoDB 内部结构每连接约 4KB – 10KB事务对象、锁结构、事务隔离信息等在trx_sys中维护。10000 连接额外占用约 40-100MB。文件描述符每个连接占用一个 socket操作系统每个进程能打开的文件描述符有限需要调大ulimit -n我们设为 65535。网络资源TCP 缓冲区 端口范围每个连接占用一个本地端口客户端侧服务端每个连接占用一个 socket双向缓冲区net_buffer_length等。性能衰减连接数 5000 时吞吐量明显下降实测 MySQL 在 10000 连接时的 TPS 比 1000 连接时下降 40%60%因为锁竞争LOCK_thread_count和上下文切换成为瓶颈。在我们的 24 核机器上10000 连接时 TPS 从峰值 8,000 降至 3,500。结论每个连接都不是免费的。当Threads_connected超过 10000数据库已经处于重压状态即使这些连接全是Sleep空闲连接也会消耗大量内存和 CPU导致正常查询响应变慢甚至超时。六、配置数据库连接数应该考虑哪些内容——必须经过压测验证配置连接池大小包括minPoolSize和maxPoolSize绝不是凭经验或简单公式拍板而应该遵循“压测驱动 指标闭环”的原则。以下是完整的配置决策流程并重点分析Threads_running与 CPU 核数的关系。1. 收集基础约束数据库侧max_connections上限如 8000预留 20-30% 给管理、备份、监控。应用侧最大实例数考虑扩容到极限每个实例需要连接的分片数。网络/OS文件描述符限制、内存上限。2.Threads_running与 CPU 核数的关系核心指导原则Threads_running是 MySQL 中当前正在执行查询的线程数不同于Threads_connected包含空闲。真正消耗 CPU 的是正在运行的线程而非空闲连接。经验公式在典型 OLTP 场景下最佳Threads_running约为 CPU 核数的 1.5 3 倍。当Threads_running≈ CPU 核数时CPU 利用率可达到 100%无等待。当Threads_running超过 CPU 核数的 35 倍操作系统会频繁进行上下文切换吞吐量开始下降平均响应时间急剧增加。当Threads_running超过 CPU 核数的 10 倍系统进入“活锁”状态TPS 几乎不再增长RT 飙升。如何利用这个关系配置连接池首先通过压测确定在目标数据库服务器24核上业务 SQL 的平均执行时间例如 5ms。那么单核每秒约能处理 200 个查询1000ms/5ms。其次设定目标 CPU 使用率如 70%则允许的全局并发查询数Threads_running≈ 24 × 0.7 / (查询耗时占比) ≈ 约 3050。然后maxPoolSize的总和所有应用服务器连接池上限之和应该控制在使数据库的Threads_running不超过这个范围。因为每个maxPoolSize连接可能同时发出查询所以所有应用实例的maxPoolSize之和 × 平均活跃率 ≤ 目标 Threads_running在我们的场景中350 台实例每台maxPoolSize20理论并发查询能力为 7000远超 CPU 处理能力。因此必须降低maxPoolSize或引入排队机制。实际做法压测时在数据库端监控Threads_running和 CPU 利用率。调整应用并发数直到Threads_running达到 CPU 核数的 2 倍左右此时整体吞吐量最高。将此并发数除以应用实例数得到单实例合理的maxPoolSize。3. 单实例压测确定单池maxPoolSize在预发环境对单个应用实例 单个数据库进行梯度压力测试从maxPoolSize5,10,15,20,30逐步增加。监控指标应用的 TPS、平均 RT、99 线 RT、数据库 CPU 使用率、Threads_running、连接池等待次数。选择拐点当继续增大maxPoolSize时TPS 不再提升甚至下降因上下文切换开销该点即为最佳maxPoolSize。例如实测发现maxPoolSize12时性能最好再增大反而 RT 上升则确定 12。4. 多实例压测确定minPoolSize与保底策略模拟 350 台实例同时启动可用容器批量拉起观察数据库连接数增长速度。测试不同minPoolSize1/2/5/10下数据库内存和 CPU 占用以及应用启动耗时。选择原则minPoolSize应尽可能低但要避免因空闲连接被回收而频繁重建导致性能波动。通常minPoolSize2~3能覆盖绝大多数低峰期流量。我们通过压测发现minPoolSize2时空闲连接回收频率与创建频率平衡且数据库Threads_connected稳定在 4000 左右CPU 无异常。5. 全链路压测验证全局连接数与Threads_running使用生产规模实例数350 台和真实流量模型压测。观察数据库的Threads_connected、Threads_running、max_connections_used、CPU 利用率等指标。核心目标保证压测过程中Threads_running始终不超过 CPU 核数的 34 倍即 24核 × 3 72否则说明连接池的maxPoolSize总和过大需要进一步降低。在我们的压测中当maxPoolSize20时峰值Threads_running达到约 1205倍 CPU 核数RT 明显上升。最终我们将maxPoolSize降到 12此时峰值Threads_running≈ 703倍TPS 反而提升了 15%。6. 设置动态告警与自动降级当Threads_connected超过max_connections的 70% 时发出预警。当Threads_running持续超过 CPU 核数的 4 倍时触发紧急告警并可能限流。当超过 85% 时禁止应用继续扩容通过服务注册中心暂缓注册。我们的实践通过上述压测流程最终确定每个 Zebra 连接池的minPoolSize2, maxPoolSize12。在 350 台实例下单集群保底连接数为 4,200峰值连接数为 8,400峰值Threads_running控制在 70 以内数据库 CPU 利用率稳定在 65%整体 TPS 相比最初配置提升了 15%同时 RT 降低了 30%。七、最终效果 经验总结我们按照上述四层方案逐步实施后单个数据库集群的连接数变化如下阶段每台服务器对集群内单库的连接数保底/峰值集群总连接数保底峰值 Threads_running数据库 CPU 利用率扩容后原始状态min5, max305 / 3010,50012085%过载仅调整 C3P0min2, max202 / 204,200~9070% 再调整至 max12压测优化2 / 124,200~7065% ✅ Zebra 分组路由min2, max122 / 12只连1个分片≈700~3535% ✅最终我们选择了Zebra 分组路由 连接池优化minPoolSize2, maxPoolSize12数据库连接使用率稳定在 50% 以下Threads_running健康系统吞吐量相比原始配置提升了 15%。核心经验供所有稳定性负责人参考作为故障排查者要保持对“历史设计”的批判性思考不要因为服务不是自己设计的就接受所有现状。本次如果我不质疑“为什么一定要 6 个分库”就无法找到根本的架构缺陷。分片数量不是越多越好必须基于真实的数据量和 QPS写入 QPS 8000 完全不需要 6 个分库2~3 个足矣。过度分片是连接数爆炸的元凶之一。中间件并不是银弹Zebra 这类轻量级框架虽然方便但“每分片独立连接池”的设计在超大规模实例下会放大连接数。务必评估架构上限。计算连接数必须考虑分片倍数与保底参数保底连接数 实例数 × 分片数 × minPoolSize峰值连接数 实例数 × 分片数 × maxPoolSize这是架构级约束不能仅靠调参解决。Threads_running比Threads_connected更能反映数据库真实压力配置连接池时应以Threads_running不超过 CPU 核数的 3~4 倍为核心目标。minPoolSize和initialPoolSize必须按压测结果设置过高浪费数据库连接过低会导致频繁连接重建。我们的案例中从 5 降到 2既节省 60% 连接又不影响性能。每个连接都有成本内存、CPU、文件描述符、锁竞争。超过 10000 连接时数据库性能会急剧下降即使在 140GB 内存的机器上也是如此。配置必须由压测验证任何连接池参数包括maxPoolSize都应该通过全链路压测确定拐点而不是凭经验或默认值。八、附不同规模下的连接池参考配置MySQL Zebra / C3P0应用规模集群内分片数单池 minPoolSize单池 maxPoolSize集群总连接数保底按200台算建议小型10台1~2520~302000标准配置即可中型10~50台2~6315~206000开始需要关注乘积大型50~200台6~12210~152400~7200必须使用分组路由或代理超大型200台121~28~122400~9600分组路由 代理 动态扩缩容控制最后提醒Threads_connected超过 10000 对 MySQL 来说极其危险。除了引发 CPU 上下文切换暴增外还会导致 InnoDB 内部锁竞争加剧、内存占用过高。请务必将峰值Threads_running控制在 CPU 核数的 3 倍以内所有配置必须经过压测验证。希望这次复盘能让更多团队意识到连接池配置不是点石成金的银弹它只是系统容量规划中一个需要被量化的变量。真正的解法往往在架构层 严谨的压测体系。而作为稳定性负责人即使是临时接手也要敢于质疑历史设计才能彻底解决问题。作者某大厂技术专家本次告警排查中临时担任稳定性负责人原服务非本人设计与维护。原文首发 CSDN转载请注明出处。
连接池设置的艺术:从一次“Threads_connected 超 10000”的线上告警说起
发布时间:2026/6/9 1:17:09
350 台应用服务器2 个数据库集群每个集群 6 个分库每个分库minPoolSize5……当这些数字叠加在一起时每个数据库集群的Threads_connected轻松突破 10000数据库瞬间进入高压模式。这次告警我作为稳定性负责人临时接手排查——服务并非我设计日常也非我维护但正是这种“旁观者”视角让我看清了连接池配置与分库数量之间被长期忽视的乘积炸弹。一、事故现场还原某天下午监控系统突然爆出两条告警数据库连接使用率超过 70%两个 MySQL 集群各自的Threads_connected均突破 10000具体数值集群 A 约 10,500集群 B 约 10,200双双超过预设的 10,000 告警阈值当时我作为稳定性负责人被拉进紧急会议开始排查这套我从未参与设计、日常也不负责维护的服务。初步了解架构后发现应用使用了分库分表数据访问层采用美团开源的 Zebra 数据中间件。Zebra 会为配置的每个物理数据库每个分片独立创建并维护一个连接池应用服务器直连物理 DB 的数量等于分片数。架构细节2 个数据库集群cluster_0 / cluster_1每个集群下各有6 个物理数据库db_0 ~ db_5。Zebra 配置了这两组数据源每个应用服务器启动后会建立2 × 6 12 个独立的 C3P0 连接池。每个连接池C3P0的配置为initialPoolSize5 minPoolSize5 # 关键每个连接池启动后会至少保持 5 个空闲连接 maxPoolSize30数据库服务器配置每个集群的数据库实例运行在独享容器中规格为24核 / 140GB内存 / PCIE-SSD 2900G / 千兆网卡。这样的配置在业界已属中高端但在 10,000 连接面前仍然捉襟见肘。为什么告警值是 10,000而不是350 × 6 × 30 63,000因为连接池的minPoolSize5意味着每个应用服务器对每个物理数据库会维持至少 5 个常驻连接。扩容后连接池会立即按minPoolSize创建连接于是实际已经建立的连接数为单个集群总连接数 应用实例数 × 该集群内物理 DB 数量 × minPoolSize 350 × 6 × 5 10,500maxPoolSize30是峰值上限但由于业务并发尚未打满连接池实际保持在minPoolSize5的水平。即便如此10,500 已经远超 MySQL 集群常见的max_connections通常设为 4000~8000告警立即触发。而两个集群各自独立因此同时收到两条“超过 10000”的告警。如果把maxPoolSize30用满单个集群连接数将高达 63,000那将是灾难性的——但正是minPoolSize这个“温和”的参数已经让系统在扩容瞬间就突破了警戒线。二、根本原因分库分表下的“保底连接”爆炸 过度分片很多团队在配置连接池时只关注maxPoolSize却忽略了minPoolSize或initialPoolSize在多实例多分片场景下的乘积效应。通用公式有两个保底连接数稳定态 应用实例数 × 分片数 ×minPoolSize峰值连接数极限 应用实例数 × 分片数 ×maxPoolSize在这个案例中minPoolSize5导致每一个物理数据库都被每个应用服务器长期占用至少 5 条连接无论有没有请求。当应用实例数膨胀到 350 时单个集群的保底连接数就破万了。这解释了为什么告警不是发生在业务高峰而是在扩容后立刻出现——因为连接池初始化时就会按照initialPoolSize/minPoolSize创建连接与业务流量无关。 深度反思6 个分库的设计是否合理随着排查深入我发现一个更根本的问题为什么每个集群要分 6 个库了解业务数据后得知每个集群总数据量约 1.25T写入 QPS 峰值不超过 8000。在这样的数据量下单库 1.25T 虽然偏大但在 PCIE-SSD 140GB 内存的配置下仍可承受主要挑战是备份和 DDL 时间。写入 QPS 8000一个主库完全能轻松应对MySQL 单库写入能力可达 1.5 万。也就是说6 个分库并非出于容量或性能硬性需求而是早期设计者“为了分片而分片”或者为了未来的增长预留。但代价是连接数被乘以 6在 350 台实例下直接引爆。正确的分片数选择逻辑写入 QPS 8000 → 单库足够 → 分片数 1~2 即可。数据量 1.25T → 可接受 2 个分库每库 625GB或 3 个分库每库 416GB。6 个分库严重过度直接导致了连接数爆炸。如果当初设计为 2 个分库保底连接数仅为 350 × 2 × 5 3,500远低于告警阈值甚至可能永远不会出问题。这次经历让我深刻体会到分片数量不是越多越好每增加一个分片连接数、运维成本、故障半径都会成倍增加。分片决策必须基于真实的数据量和 QPS而不是凭空猜测。三、不是连接泄露而是“过度连接”很多同学第一反应是“连接泄露”。但从我们抓取的information_schema.processlist看SELECTSUBSTRING_INDEX(HOST,:,1)ASip,COUNT(*)FROMinformation_schema.processlistGROUPBYipORDERBYCOUNT(*)DESCLIMIT10;结果中每个 IP 在该集群内的连接数稳定在6 × 5 30左右因为实际活跃池大小接近minPoolSize且状态多为SleepTIME普遍小于 10 秒。这不是连接泄露而是连接池主动维持的空闲连接。换句话说该集群内的每一个物理 DB 都被每个应用服务器占用了至少 5 条“备勤”连接无论是否有业务请求。四、解决方案四个层次的优化策略作为临时接手排查的负责人我需要给出快速止血和长期治理的方案。最终我们从架构层、配置层、路由层、代码层逐级优化。1. 架构层长期减少分片数或引入代理最根本的解决方式是减少分片数。经评估业务写入 QPS ≤ 8000总数据量 1.25T/集群完全可以将 6 个分库合并为 2~3 个。如果合并保底连接数将降至350 × 2 × 2 1,400假设 min2问题彻底消失。如果无法快速合并需要数据迁移则引入数据库代理如 ShardingSphere-Proxy让应用只连代理代理连接后端 6 个分片。连接数变为350 × 1 × 每池连接数同样大幅下降。2. 配置层紧急止血调整initialPoolSize与minPoolSize在等待架构调整的同时我紧急修改了 C3P0 配置经过压测验证# 原配置每个物理 DB 的连接池 initialPoolSize5 minPoolSize5 maxPoolSize30 # 优化后配置 initialPoolSize2 # 启动时创建 2 个连接加快启动速度但不过度 minPoolSize2 # 最小空闲连接降为 2保底连接数下降 60% maxPoolSize20 # 峰值上限根据压测结果从 30 降至 20 maxIdleTime600 # 空闲 10 分钟回收 maxIdleTimeExcessConnections300 checkoutTimeout3000为什么选择 minPoolSize2 而不是 1压测表明在单机并发 100 TPS 时连接池的活跃连接数中位数在 2~4 之间若minPoolSize1会出现频繁的连接创建与销毁反而增加数据库负担。设置minPoolSize2恰好覆盖大部分静默流量且保底连接数从 5 降到 2单集群总连接数从 10,500 降至350 × 6 × 2 4,200降幅 60%低于 MySQLmax_connections8000的 60%安全可控。同时maxPoolSize从 30 降到 20进一步削峰。3. 路由层中期Zebra 分组路由Zebra 支持通过配置namespace或hint实现“应用分组直连部分分片”。我将 350 台服务器分为 6 组每组约 58 台通过 Zebra 的dbGroup特性让 Group0 只连接db_0Group1 只连接db_1依此类推。这样每个数据库的连接数降至58 × 2 116集群总连接数 6 × 116 696极大幅度降低。4. 代码层拒绝长事务 异步任务滥用我们在排查中发现业务代码中存在多处类似模式TransactionalpublicvoidbatchSave(ListSessionAssignassigns){// 分批 循环 单条兜底for(ListSessionAssignbatch:batches){mapper.batchInsert(batch);}}并且在某些异步场景中被CompletableFuture.runAsync()并发调用。这导致单个事务持有连接时间长达数秒连接池利用率低下间接推高了所需的maxPoolSize。优化建议拆分事务边界每批数据一个独立事务使用TransactionTemplate。异步任务必须配置有界线程池控制并发度。添加Transactional(timeout 3)强制短事务。五、每新增一个数据库连接究竟耗费哪些资源很多开发者对“连接数”的代价没有感性认识。下面从多个维度量化每增加一个 MySQL 连接带来的系统开销以 MySQL 5.7 / 8.0 为例结合我们 24核/140GB 的数据库容器配置资源类型每连接开销说明内存线程私有~256KB – 3MB每个连接对应一个线程线程栈thread_stack默认 256KB加上网络缓冲区net_buffer_length默认 16KB、临时表等实际常驻内存约 2-3MB。10000 个连接 ≈ 20-30GB 内存占 140GB 的 15-20%虽未触顶但已显著影响 buffer pool 可用空间。CPU 上下文切换随连接数线性增长大量空闲连接会导致 MySQL 的pthread调度开销增加。在 24 核机器上10000 连接时上下文切换次数可达到每秒数十万次CPU 的sy系统态占用超过 30%即使没有业务查询。InnoDB 内部结构每连接约 4KB – 10KB事务对象、锁结构、事务隔离信息等在trx_sys中维护。10000 连接额外占用约 40-100MB。文件描述符每个连接占用一个 socket操作系统每个进程能打开的文件描述符有限需要调大ulimit -n我们设为 65535。网络资源TCP 缓冲区 端口范围每个连接占用一个本地端口客户端侧服务端每个连接占用一个 socket双向缓冲区net_buffer_length等。性能衰减连接数 5000 时吞吐量明显下降实测 MySQL 在 10000 连接时的 TPS 比 1000 连接时下降 40%60%因为锁竞争LOCK_thread_count和上下文切换成为瓶颈。在我们的 24 核机器上10000 连接时 TPS 从峰值 8,000 降至 3,500。结论每个连接都不是免费的。当Threads_connected超过 10000数据库已经处于重压状态即使这些连接全是Sleep空闲连接也会消耗大量内存和 CPU导致正常查询响应变慢甚至超时。六、配置数据库连接数应该考虑哪些内容——必须经过压测验证配置连接池大小包括minPoolSize和maxPoolSize绝不是凭经验或简单公式拍板而应该遵循“压测驱动 指标闭环”的原则。以下是完整的配置决策流程并重点分析Threads_running与 CPU 核数的关系。1. 收集基础约束数据库侧max_connections上限如 8000预留 20-30% 给管理、备份、监控。应用侧最大实例数考虑扩容到极限每个实例需要连接的分片数。网络/OS文件描述符限制、内存上限。2.Threads_running与 CPU 核数的关系核心指导原则Threads_running是 MySQL 中当前正在执行查询的线程数不同于Threads_connected包含空闲。真正消耗 CPU 的是正在运行的线程而非空闲连接。经验公式在典型 OLTP 场景下最佳Threads_running约为 CPU 核数的 1.5 3 倍。当Threads_running≈ CPU 核数时CPU 利用率可达到 100%无等待。当Threads_running超过 CPU 核数的 35 倍操作系统会频繁进行上下文切换吞吐量开始下降平均响应时间急剧增加。当Threads_running超过 CPU 核数的 10 倍系统进入“活锁”状态TPS 几乎不再增长RT 飙升。如何利用这个关系配置连接池首先通过压测确定在目标数据库服务器24核上业务 SQL 的平均执行时间例如 5ms。那么单核每秒约能处理 200 个查询1000ms/5ms。其次设定目标 CPU 使用率如 70%则允许的全局并发查询数Threads_running≈ 24 × 0.7 / (查询耗时占比) ≈ 约 3050。然后maxPoolSize的总和所有应用服务器连接池上限之和应该控制在使数据库的Threads_running不超过这个范围。因为每个maxPoolSize连接可能同时发出查询所以所有应用实例的maxPoolSize之和 × 平均活跃率 ≤ 目标 Threads_running在我们的场景中350 台实例每台maxPoolSize20理论并发查询能力为 7000远超 CPU 处理能力。因此必须降低maxPoolSize或引入排队机制。实际做法压测时在数据库端监控Threads_running和 CPU 利用率。调整应用并发数直到Threads_running达到 CPU 核数的 2 倍左右此时整体吞吐量最高。将此并发数除以应用实例数得到单实例合理的maxPoolSize。3. 单实例压测确定单池maxPoolSize在预发环境对单个应用实例 单个数据库进行梯度压力测试从maxPoolSize5,10,15,20,30逐步增加。监控指标应用的 TPS、平均 RT、99 线 RT、数据库 CPU 使用率、Threads_running、连接池等待次数。选择拐点当继续增大maxPoolSize时TPS 不再提升甚至下降因上下文切换开销该点即为最佳maxPoolSize。例如实测发现maxPoolSize12时性能最好再增大反而 RT 上升则确定 12。4. 多实例压测确定minPoolSize与保底策略模拟 350 台实例同时启动可用容器批量拉起观察数据库连接数增长速度。测试不同minPoolSize1/2/5/10下数据库内存和 CPU 占用以及应用启动耗时。选择原则minPoolSize应尽可能低但要避免因空闲连接被回收而频繁重建导致性能波动。通常minPoolSize2~3能覆盖绝大多数低峰期流量。我们通过压测发现minPoolSize2时空闲连接回收频率与创建频率平衡且数据库Threads_connected稳定在 4000 左右CPU 无异常。5. 全链路压测验证全局连接数与Threads_running使用生产规模实例数350 台和真实流量模型压测。观察数据库的Threads_connected、Threads_running、max_connections_used、CPU 利用率等指标。核心目标保证压测过程中Threads_running始终不超过 CPU 核数的 34 倍即 24核 × 3 72否则说明连接池的maxPoolSize总和过大需要进一步降低。在我们的压测中当maxPoolSize20时峰值Threads_running达到约 1205倍 CPU 核数RT 明显上升。最终我们将maxPoolSize降到 12此时峰值Threads_running≈ 703倍TPS 反而提升了 15%。6. 设置动态告警与自动降级当Threads_connected超过max_connections的 70% 时发出预警。当Threads_running持续超过 CPU 核数的 4 倍时触发紧急告警并可能限流。当超过 85% 时禁止应用继续扩容通过服务注册中心暂缓注册。我们的实践通过上述压测流程最终确定每个 Zebra 连接池的minPoolSize2, maxPoolSize12。在 350 台实例下单集群保底连接数为 4,200峰值连接数为 8,400峰值Threads_running控制在 70 以内数据库 CPU 利用率稳定在 65%整体 TPS 相比最初配置提升了 15%同时 RT 降低了 30%。七、最终效果 经验总结我们按照上述四层方案逐步实施后单个数据库集群的连接数变化如下阶段每台服务器对集群内单库的连接数保底/峰值集群总连接数保底峰值 Threads_running数据库 CPU 利用率扩容后原始状态min5, max305 / 3010,50012085%过载仅调整 C3P0min2, max202 / 204,200~9070% 再调整至 max12压测优化2 / 124,200~7065% ✅ Zebra 分组路由min2, max122 / 12只连1个分片≈700~3535% ✅最终我们选择了Zebra 分组路由 连接池优化minPoolSize2, maxPoolSize12数据库连接使用率稳定在 50% 以下Threads_running健康系统吞吐量相比原始配置提升了 15%。核心经验供所有稳定性负责人参考作为故障排查者要保持对“历史设计”的批判性思考不要因为服务不是自己设计的就接受所有现状。本次如果我不质疑“为什么一定要 6 个分库”就无法找到根本的架构缺陷。分片数量不是越多越好必须基于真实的数据量和 QPS写入 QPS 8000 完全不需要 6 个分库2~3 个足矣。过度分片是连接数爆炸的元凶之一。中间件并不是银弹Zebra 这类轻量级框架虽然方便但“每分片独立连接池”的设计在超大规模实例下会放大连接数。务必评估架构上限。计算连接数必须考虑分片倍数与保底参数保底连接数 实例数 × 分片数 × minPoolSize峰值连接数 实例数 × 分片数 × maxPoolSize这是架构级约束不能仅靠调参解决。Threads_running比Threads_connected更能反映数据库真实压力配置连接池时应以Threads_running不超过 CPU 核数的 3~4 倍为核心目标。minPoolSize和initialPoolSize必须按压测结果设置过高浪费数据库连接过低会导致频繁连接重建。我们的案例中从 5 降到 2既节省 60% 连接又不影响性能。每个连接都有成本内存、CPU、文件描述符、锁竞争。超过 10000 连接时数据库性能会急剧下降即使在 140GB 内存的机器上也是如此。配置必须由压测验证任何连接池参数包括maxPoolSize都应该通过全链路压测确定拐点而不是凭经验或默认值。八、附不同规模下的连接池参考配置MySQL Zebra / C3P0应用规模集群内分片数单池 minPoolSize单池 maxPoolSize集群总连接数保底按200台算建议小型10台1~2520~302000标准配置即可中型10~50台2~6315~206000开始需要关注乘积大型50~200台6~12210~152400~7200必须使用分组路由或代理超大型200台121~28~122400~9600分组路由 代理 动态扩缩容控制最后提醒Threads_connected超过 10000 对 MySQL 来说极其危险。除了引发 CPU 上下文切换暴增外还会导致 InnoDB 内部锁竞争加剧、内存占用过高。请务必将峰值Threads_running控制在 CPU 核数的 3 倍以内所有配置必须经过压测验证。希望这次复盘能让更多团队意识到连接池配置不是点石成金的银弹它只是系统容量规划中一个需要被量化的变量。真正的解法往往在架构层 严谨的压测体系。而作为稳定性负责人即使是临时接手也要敢于质疑历史设计才能彻底解决问题。作者某大厂技术专家本次告警排查中临时担任稳定性负责人原服务非本人设计与维护。原文首发 CSDN转载请注明出处。