ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL连接池不是越大越好:从Druid配置看1000个连接的崩溃陷阱

MySQL连接池不是越大越好:从Druid配置看1000个连接的崩溃陷阱 凌晨一点接到值班电话说核心订单接口的耗时从 80ms 涨到了 8 秒数据库 CPU 直接被打到 98%。我第一反应是慢 SQL登录数据库一看threads_connected稳定在 900 多而应用配置里 Druid 连接池的maxActive1000。那个夜晚我带着一肚子火改配置冷静下来后想通的道理值得所有自嘲连接池当然是越大越好的开发者看看。MySQL 数据库连接池并不是越大越稳恰恰相反超过某个临界点之后它就是系统崩溃的加速器。这篇文章围绕连接池数量这个话题展开结合 Druid 连接池的配置流程和原理讲讲为什么 1000 是个危险的数字以及到底该怎么定连接池大小。1. 1000 这个数字是从哪来的误解源头复盘先把话说清楚没人天生想把maxActive设成 1000。这个数字出现在生产环境往往不是因为精密的容量评估而是几个朴素到近乎危险的直觉凑在一起。1.1 量大管饱的朴素直觉很多团队早期压测时发现连接池从 20 调到 50QPS 确实涨了再从 50 调到 100发现还能涨一点。于是脑子里形成了一个线性外推既然 100 比 20 好那 1000 一定比 100 好这叫量大管饱。这种直觉在单机、低并发、没有慢查询的测试环境里可能是成立的因为瓶颈根本不在连接数而在于应用线程、网络带宽、CPU 核心数。可一旦上了生产数据库的会话、锁、事务、IO 全都纠缠在一起连接数就不再是独立变量了。实际现场里把连接池从 100 提到 1000 之后吞吐量往往会出现一个平台期然后急剧下跌表现就是响应时间从几百毫秒飙升到几秒。1.2 MySQL 侧的连接上限远比你想的小MySQL 数据库连接池数量这个话题永远绕不开数据库自身的max_connections。很多常见的 MySQL 版本这个参数默认在 151 左右。也就是说你在应用层把连接池设成 1000意味着你期望一个只有 151 个连接空闲额度可用的数据库同时被 1000 个客户端使用。这还没算上慢查询工具、运维后台、BI 提取报表这些同样在吃连接数的角色。一旦应用层瞬间发起 1000 个连接请求数据库侧会出现两种情况要么Too many connections直接拒绝新连接要么数据库守护线程在审计、认证、线程栈分配上消耗大量 CPU。这里有个很反直觉的点连接数并不是资源分配而是一笔资源买卖。每个连接落在 MySQL 里都是一个独立的线程每个线程要有自己的 thread cache、排序缓冲区、临时表空间。连接数翻 10 倍数据库的内存和 CPU 调度开销不是线性增长而是超线性增长因为大量的并发线程在互相竞争锁和 CPU 时间片。1.3 应用层明明在复用连接为什么还会溢出有人会问连接池不是复用的吗1000 个maxActive不等于 1000 个连接同时被使用。这种说法理论上是对的但生产环境永远不讲理论上。连接池的连接长期不归还常见原因包括事务忘提交、Transactional在异常路径上没有正确回滚、getConnection()之后没有放入finally块、或者某个慢查询独占连接几十秒。一旦代码里藏着连接泄漏你设置 1000 只是在延迟暴露问题而不是解决问题。等到高峰期线程一个个占住连接不放连接池就会逐渐把 1000 个连接全部建立起来数据库瞬间被击穿。这个链条我后面专门讲排障时会展开。2. 连接池过大如何一步步拖垮系统三重压力拆解我不喜欢只丢一句别设太大得把崩溃链条掰开揉碎你才知道这 1000 到底是怎么杀死系统的。2.1 应用侧每个连接背后都站着一条线程以 Tomcat 为例默认最大线程数是 200你给 Druid 配了 1000 个连接Tomcat 根本抽不出 1000 个线程去同时持有这些连接。即便你同步把 Tomcat 线程数调到 1000那你的应用服务器要同时维护 1000 个业务线程 连接池的守护线程 Netty/其他框架的 IO 线程。Java 线程默认栈大小是 1MB1000 个线程光栈内存就占用约 1GB这还不包括线程切换时的上下文开销。CPU 大量时间花在保存和恢复线程状态上真正干活的指令被挤到了一边系统吞吐量自然往下掉。这里还有一个被忽视的点连接数是和业务线程绑定的。一个请求在业务线程里要先从连接池借连接、执行 SQL、释放连接然后线程才能继续处理下一个请求。如果应用服务器只有 200 个业务线程那么真正同时需要连接的请求最多也就是 200连接池设成 1000 完全是在养闲人。连接池不是越大约好而是应该和业务线程数 × 每个线程同时持有的连接数匹配。2.2 数据库侧连接多不代表快锁和事务才是关键数据库的连接越多并发事务越多InnoDB 的行锁、间隙锁、意向锁冲突越激烈。你想象一个场景1000 个线程同时对一张热门订单表执行UPDATE即便每行锁只持有一瞬间锁等待队列也会排成长龙。MySQL 里出现大量threads_running飙升、lock wait timeout报错时几乎都不是连接不够而是连接太多了。每个连接都带着一个未提交事务持有锁的窗口被无限拉长。数据库连接池数量的本质其实是同时触发多少个事务的节流阀。另外MySQL 有个参数叫innodb_thread_concurrency它限制的是内核并行执行线程数一般默认是 0不限制或者按文档推荐设置成 CPU 核心数的 2 倍左右。当连接数暴涨MySQL 内部的调度队列会迅速堆满新到达的请求都要排队表现就是 CPU 不高但是请求特别慢很有迷惑性。2.3 网络和中间件连接池炸了最先死的是防火墙和代理连接池设成 1000不只是 MySQL 和应用之间的问题中间还隔着数据库网关、防火墙、云厂商的连接数限流。很多云的数据库产品为了防攻击会限制单 IP 的连接数比如 800。你应用侧 1000 个连接朝数据库一打还没到 MySQL 就被网络层切了连接。应用侧的连接池反复重连每次重连都要走 TCP 三次握手失败后又有新连接进来连接池在持续抖动中把负载打得更满。更麻烦的是 TIME_WAIT。短连接频繁创建、销毁应用服务器上会出现大量 TIME_WAIT 状态端口等到 2MSL 超时之前端口资源被占满新连接都无法建立。于是应用 logs 里全是连接超时数据库侧还在辛勤处理老连接的事务两边都不挨着系统处于一种假死状态。2.4 雪崩是怎么发生的把以上几条连起来看就清晰了高峰期请求增多部分接口出现慢查询连接被长时间占用连接池开始快速增长连接数据库连接数和事务并发数跟着上升。锁等待加剧慢查询变多连接的占用时间又变长连接池继续加大连接数形成一个正反馈循环。数据库 CPU 或 IO 先被打满紧接着连接数到达上限开始拒绝新连接应用侧等待超时请求失败开始堆积。运气好点只是接口熔断运气差直接拖垮依赖数据库的所有服务。我见过一个案例数据库一重启1000 个连接同时重连直接把 MySQL 打挂这就是连接风暴。3. 连接池大小到底怎么定数学推导与压测验证讲了这么多坏处总得给个能用的答案。连接池大小不存在万能常数但有可靠的推导路径。3.1 先从 HikariCP 的公式说起HikariCP 官方文档里给过一个经验公式connections ((core_count * 2) effective_spindle_count)翻译过来是 CPU 核心数乘以 2再加上磁盘个数SSD 环境下按 1 算。为什么这么定核心逻辑是一个 CPU 密集型或锁竞争为主的数据库操作并发线程一旦超过 CPU 核心数的 2 倍线程切换就开始蚕食 CPU 有效算力。对于常规 8 核 16 线程的机器这个公式给出16 1 17个连接。很多团队第一次看到这个数字都难以置信从 1000 砍到 20 左右数据库压力反而大幅下降这就是回归合理并发值的效果。这个公式的隐含前提是假设每个连接都在高效工作。如果应用里存在大量慢查询或长事务那每个连接被占用的时间边长确实需要多一些连接来保持吞吐。但正确的应对方式是消灭慢 SQL而不是无限增加连接池。公式是起点不是终点。3.2 根据 QPS 和 RT 反推连接数一个更实用的办法不需要猜直接把需求指标代进来算。假设你有压测数据或者业务目标平均每秒需要处理 QPS 1000单个请求在数据库上平均耗时 RT 20ms包括网络、SQL 执行、锁等待和事务提交时间。那么单个连接每秒能处理的请求数 1000ms / RT 1000 / 20 50 并发连接数 QPS / 单连接吞吐 1000 / 50 20也就是说在这个假设下20 个连接就够支撑 1000 QPS。如果 RT 到了 200ms那需要的连接数就是 200。你看到没有真正推动连接数上涨的不是夸大的并发而是数据库响应时间。响应时间越差你需要的连接越多而连接越多响应时间又更差这就是恶性循环。把这个公式做成一张速查表方便大家直接对照平均响应时间 (RT)单连接每秒处理请求数支撑 1000 QPS 所需连接数5 ms200510 ms1001020 ms502050 ms2050100 ms10100200 ms5200这张表只做估算使用实际生产还要留 30% 左右的余量。如果是读多写少的业务建议把读库连接池稍微放大一点但也不要超过这个公式计算值的 1.5 到 2 倍。3.3 别忽略磁盘类型的影响同样的公式在机械硬盘和 NVMe SSD 上结果完全不同。机械盘的寻道时间在毫秒级一个连接在执行随机 IO 时会把磁盘卡死并发一旦超过磁盘队列深度IO 延迟立刻爆表所以机械盘环境下连接数要压得更低。而 SSD 没有寻道时间队列深度的承受力更强可以按照公式的上限来设置。混合部署的环境里我更倾向于先按机械盘的低值去配置再通过压测逐步往上调整。3.4 用压测验证而不是拍脑袋最终定值一定要靠压测。我的做法是先按 HikariCP 公式算出基础值然后以 50% 增幅逐档压测比如 10、15、22、33每档压 10 分钟记录 QPS、RT、数据库threads_running、CPU、IO 等待。画出曲线找到一个点再往上加连接数QPS 几乎不涨RT 却开始缓慢上升这个点再回退一档就是连接池上限。听起来繁琐但实际做一次最多半天却能帮你省掉无数个报警电话。压测工具我用过 JMeter、Sysbench、甚至是简单的go-wrk重要的是施压端要多线程并发跑不要只开单线程加循环次数否则压出来的结果全无参考价值。4. Druid 连接池配置一套接近生产级的参数模板说到 Druid 连接池配置流程和原理光讲maxActive是不够的这一套参数要搭配起来才有意义。Druid 的功能比 HikariCP 重但也正因为重它提供了很多诊断和防护能力这里给出一份我这边生产环境常用的配置并逐个参数讲清楚它解决的痛点。4.1 核心参数逐个拆解initialSize启动时预建的物理连接数。我一般设 5超出也没意义因为应用刚启动时请求量还没起来。minIdle连接池保持的最小空闲连接数。设 5 到 10防止突发流量时才临时建连也避免空跑浪费数据库资源。maxActive连接池最大连接数按第三节的公式来不要抄别人的。8 核 16 线程机器常见配置是 20 到 50如果你压测后需要 100可以设 100但务必确认数据库max_connections和应用所在内网防火墙能撑住。maxWait获取连接的最长等待时间单位毫秒。生产环境务必设置比如5000。如果不设线程池借不到连接时会无限等请求队列越堆越长雪崩就是这么来的。timeBetweenEvictionRunsMillis间隔多久检测一次空闲连接单位毫秒。默认值 60000 有点长我习惯设 30000。minEvictableIdleTimeMillis连接最少闲置多久才被回收单位毫秒。设 300000 是为了让连接在数据库端空闲超时比如 wait_timeout 默认 8 小时之前被回收重建避免被数据库踢掉后应用还在使用坏连接。validationQuery检测连接是否有效的 SQL比如SELECT 1。配合testWhileIdle使用。testWhileIdle空闲检测时是否执行 validationQuery设为 true。testOnBorrow获取连接时是否检测有效性设为 false。开启后每次拿连接都会多一次网络往返性能受损所以依赖 testWhileIdle 就够了。testOnReturn归还连接时是否检测建议 false同样是为了性能。removeAbandoned是否回收被遗弃的连接设为 true配合removeAbandonedTimeoutMillis比如 200 秒能兜底连接泄漏。但是要注意这个机制是靠应用栈快照来识别谁拿了连接没还误杀长事务的风险存在生产环境需谨慎评估。logAbandoned发现遗弃连接时打印栈日志必须 true否则你都不知道连接被谁拿走了。4.2 一份可直接抄的 YAML 模板以 Spring Boot 集成 Druid 为例配置长这样spring: datasource: druid: initial-size: 5 min-idle: 10 max-active: 50 max-wait: 5000 validation-query: SELECT 1 validation-query-timeout: 3 test-while-idle: true test-on-borrow: false test-on-return: false time-between-eviction-runs-millis: 30000 min-evictable-idle-time-millis: 300000 max-evictable-idle-time-millis: 900000 # 连接泄漏兜底 remove-abandoned: true remove-abandoned-timeout-millis: 200 remove-abandoned-timeout: 200 log-abandoned: true # 监控 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: your-password web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.css,/druid/*顺带提一句removeAbandonedTimeoutMillis和removeAbandonedTimeout别同时用不同单位混淆它们一个是毫秒、一个是秒我曾经因为两个都写导致秒数被当成毫秒误回收了大量正常连接。4.3 不同业务场景的差异化配置连接池没有一套配置走天下的说法。读多写少的门户查询类接口可以把maxActive上调到公式值的 1.5 到 2 倍同时把maxWait缩短到 2000 毫秒因为这种场景追求的是快速失败和自动重试。账务类、库存类强一致业务宁可让请求排队也不要让并发事务数太高连接数可以取公式值甚至略低maxWait可以放到 2000 到 5000 毫秒让请求等待而不是爆事务。批处理任务和消息消费任务大多不需要从连接池借连接而是自己使用独立的短连接或者独占连接从来不建议和在线接口共用同一个连接池否则大促扫描任务会把在线连接全部吃光。有几个配置很容易被忽略maxEvictableIdleTimeMillis设成 900000是为了避免连接一直空闲不被回收长期占用数据库会话phyTimeoutMillis可以控制物理连接的最大存活时间比如 3600000让 MySQL 侧连接不会因为长时间复用而残留状态异常。Druid 是个坑很多但又很值得研究的组件建议新项目先花半天读一遍官方 wiki 的参数说明再上线。4.4 为什么说默认配置救不了你HikariCP 默认maximumPoolSize10Druid 默认maxActive8这些默认值对小型应用是安全的但对生产环境未必合理。反过来也别太激进。我见过不少团队把maxActive设成 500结果压测时发现数据库中threads_running一直不高但应用大量请求在等待maxWait超时说明连接池数量本身没让数据库更快反而把 CPU 浪费在线程切换上。连接池的特性决定了它是一个爽约型资源池设置太小会让请求并发排队设置太大又会让系统核心被无谓的资源管理耗尽。理解了这一点你再回头看 1000 这个数字应该明白它既不代表吞吐也不代表可靠性。5. 排障实录连接池出问题时如何一步步定位配置是调完了但线上连接池问题从来不只有一个形态。这里把最常见的几个现象和排查链路完整走一遍全是实战里反复遇到的场景。5.1 现象一应用报获取连接等待超时日志里出现wait connection millis 5000, active 1000, maxActive 1000这类信息说明连接池已经被打满新请求借不到连接。这时先别急着调大连接池反而要问几个问题当前活跃连接数是不是一直高有没有慢查询连接池满是因为什么我的排查第一步是登进 Druid 监控页就是配置里stat-view-servlet暴露的那个/druid页面看ActiveCount、PoolingCount、WaitThreadCount再打开 SQL 监控列表按执行时间排序找最慢的 SQL。第二步是连上 MySQL执行SHOW FULL PROCESSLIST看State一栏是不是大量Sleep或者Sending data。如果是Sleep说明连接被借出去后没及时归还优先排查事务路径如果是Sending data一堆说明确实有慢 SQL 在拖数据库。5.2 现象二数据库Too many connections这个报错说明数据库侧的连接数已经超过了max_connections。此时光看应用已经不够了先执行SHOW VARIABLES LIKE %connections%;确认上限再统计一下到底是谁把连接打满了SELECT user, host, db, command, count(*) FROM information_schema.processlist GROUP BY user, host, db, command ORDER BY count(*) DESC;通常你会看到某个应用账号占了 400 多个连接而真正执行 SQL 的Command为Query的却没几个大部分是Sleep。这说明连接池在疯狂养连接。有经验的 DBA 会直接检查应用侧的maxActive和initialSize再去连接池监控里看空闲连接占比。碰到这种情况先临时把应用连接池maxActive调小并重启应用让数据库喘口气然后再定位谁在泄漏连接。5.3 现象三数据库 CPU 高但慢日志很少这是最迷惑人的一种。连接数多、CPU 高、慢 SQL 日志却寥寥无几。排查思路要从SQL 执行转向数据库内部调度。看SHOW GLOBAL STATUS LIKE Threads_running;如果这个值经常超过 CPU 核心数说明并发事务线程太多大量线程在排队抢 CPU。再看SHOW GLOBAL STATUS LIKE Threads_connected;确认连接数是否已经逼近上限。如果两个值都高基本坐实了连接池过大的锅。这时你可以做一个实验连接池从 1000 临时改到 50重启应用观察 30 分钟。如果Threads_running明显回落、QPS 不降反升那原因就实锤了。这套验证方法比任何理论都更有说服力。5.4 连接泄漏比配置更要命的隐藏杀手很多时候连接池设 1000 不是拍脑袋而是被连接泄漏搞怕了。代码里漏了conn.close()连接被借走不还连接池只能不断创建新连接去满足新请求久而久之就涨到了maxActive。这个场景光调参数没用。我的经验是三条线一起抓一是把removeAbandoned和logAbandoned打开让连接池自己抓偷懒的连接二是用Druid的 SQL 监控看PreparedStatement的打开和关闭是否对称三是重点审查Transactional的边界所有对外部 API 的调用、文件上传、消息发送都要放到事务里面的话你就有好戏看了。除此之外getConnection必须写在try-with-resources或 finally 块里老生常谈但永远有人在踩。6. 我的踩坑总结关于连接池配置的几条反直觉经验踩过的坑比看过的文档实在最后分享几条花真金白银买来的心得。6.1 宁可短暂的排队也不要无限的并发一个秒杀系统数据库能抗 50 个并发事务你给它 100 个并发事务它不会更快只会更慢而且慢得让你怀疑数据库坏了。连接池的本质是一个节流器它的道德不是尽量多放行而是保护下游不被打爆。短时间的请求排队可以通过maxWait和超时重试来消化比数据库崩了之后几个小时的黑夜要划算得多。6.2 把连接获取超时设成显式参数maxWait不要留空白。不设置超时意味着请求线程会无限期等待连接这在高峰期等于让所有线程堆积成山。设成5000、3000、甚至1000都行关键是让失败快速暴露出来。很多系统死掉不是因为没有容量而是因为没有快速失败机制。快速失败 客户端重试 熔断这组组合能拦住绝大多数雪崩。6.3 定期主动销毁和重建连接MySQL 默认的wait_timeout是 8 小时连接池里的连接被闲置超过这个时间后数据库侧会主动断开而应用侧如果不知道拿到的就是一条僵尸连接。所以testWhileIdle和timeBetweenEvictionRunsMillis必须配套设置。我见过一个诡异故障每天晚上固定时间接口全部报Communications link failure排查到最后就是连接被数据库断开后连接池里残留的坏连接被凌晨定时任务借出去执行 SQL。把timeBetweenEvictionRunsMillis调成30000并打开testWhileIdle这个故障再也没出现过。6.4 真正的瓶颈往往是慢 SQL 和锁不是连接数这句话我说过很多次但每次排查还是有人不信。应用报连接池满你急着把连接数翻倍结果只是把更多的事务送进数据库去排队最后数据库更满。正确闭环应该是连接池满 - 不扩容 - 抓慢 SQL - 优化索引或拆分事务 - 连接池占用率自然下降。MySQL 数据库连接池数量只是一个可见的症状而病灶在业务代码和数据模型里。我最近处理过一个连接池从 50 改到 500 都救不回来的案例最后发现罪魁祸首是一条全表扫描的报表 SQL每天凌晨两点准时把数据库 IO 打满。改完这条 SQL连接池 30 都戳戳有余。6.5 压测数据记得留余量连接池大小定好了别卡着临界值跑生产。我习惯在压测得出的连接池上限上打八折跑日常再留一个独立的备用配置可以在应急时一键切换。这就像开车表速 180 并不代表你该一直开 180留出余量的人才不会一头栽进沟里。Druid 的配置流程并不复杂复杂的是你得理解每个参数在压力下真实扮演的角色。我自己经历过的生产事故但凡连接池设得离谱的几乎都不是需求导向而是想当然。如果你现在打算把连接池改成 1000先回头看一眼数据库的max_connections、应用服务器的业务线程数、以及最近一周的监控数据然后再把第三节的公式拿过来算一遍。算完之后你会发现1000 这个数字大概率会和崩溃两个字永远解绑。
返回列表