从P99超时风暴到80ms:Druid连接池性能优化与故障排查实战手册 从P99超时风暴到80msDruid连接池性能优化与故障排查实战手册【免费下载链接】druid阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品为监控而生的数据库连接池项目地址: https://gitcode.com/gh_mirrors/druid/druid凌晨3点下单接口P99从80ms飙到1.2s日志刷满GetConnectionTimeoutException——这是最典型的连接池耗尽现场。Druid 是一个为监控而生的数据库连接池它把全链路SQL统计直接做进了池子里排查慢SQL、连接等待不用额外埋点。按下面的顺序动手多数场景能把接口P99压回毫秒级连接获取稳定在亚毫秒。原理速览 把连接池想成餐厅的后厨出菜口池子里堆着的是备好的盘子poolingCount被服务员拿走正在用的是在途盘子activeCountCreateThread 负责持续刷盘子补货。应用调getConnection()时的决策链在 DruidDataSource.java 的pollLast里// DruidDataSource#pollLast节选自 L2225-L2260 while (poolingCount 0) { emptySignal(); // 通知CreateThread去建新连接 ... notEmptyWaitThreadCount; // 等待队列长度MBean可直接读 try { notEmpty.await(estimate, TimeUnit.MILLISECONDS); // 睡在notEmpty上 } finally { notEmptyWaitThreadCount--; notEmptyWaitNanos awaitNanos; // 累计等待纳秒数算分位数就靠它 } }三个关键变量决定了你拿到连接要多快connections[]poolingCount空闲连接库存有货则直接出队无锁路径activeCount maxActive没货且没到上限时当前线程同步建连这就是偶发首请求偏慢的来源notEmpty条件变量库存和上限都撞墙时才排队maxWait到点没等到就抛GetConnectionTimeoutException。所以连接获取慢从来不是一个指标它至少对应三种病库存不足、上限太小、排队超时。后面排障全部围绕这三条走。SQL侧的统计载体是 JdbcSqlStat.java每个SQL模板一个实例字段全靠AtomicLongFieldUpdater原子更新热路径无锁// JdbcSqlStat 核心字段节选自 L43-L47 private volatile long executeSuccessCount; private volatile long executeSpanNanoTotal; // 总执行耗时(ns)求均值用 private volatile long executeSpanNanoMax; // 最慢一次的耗时 private volatile int concurrentMax; // 这条SQL最大并发执行数指标与观测 先看哪几个数。这张表按影响业务什么排序不是按字典序指标业务影响健康阈值观测入口waitThreadCount等待队列长度连接排队请求线程被挂起持续0即告警数据源页等待队列notEmptyWaitNanos折算的分位耗时用户等待时间的组成部分P99 50ms数据源页 JSON APIactiveCount / maxActive池子利用率逼近1随时雪崩常态 80%数据源页executeSpanNanoTotal / executeSuccessCountSQL平均执行时间按业务定通用 100msSQL监控页按模板聚合最小可运行的监控接入Spring Boot 环境一个 yaml 就够starter 属性前缀spring.datasource.druid见 DruidStatProperties.javaspring: datasource: druid: filters: stat # 不开stat上面四个数全是0 stat-view-servlet: enabled: true url-pattern: /druid/* login-username: druid login-password: 换成强密码 reset-enable: false # 生产禁用页面重置按钮配完访问/druid数据源页、SQL监控页、JSON API 三件套齐了。注意filters: stat这一行很多团队配了监控页却看不到数据九成是这里漏了。不走 Web 页的话JMX 也可以数据源注册为com.alibaba.druid:typeDruidDataSource,idname的 MBeanactiveCount、poolingCount、notEmptyWaitNanos都是标准属性直接挂到你现有的监控采集里。优化实操 ️按优先级切三段事故现场先止血常态再调优最后把坑填死。3.1 先止血事故进行中先做这两件事动作一临时上调 maxActive 争取时间原理把排队线程放回能建连/能出队的状态先让业务恢复原因分析放后面。spring: datasource: druid: max-active: 100 # 临时值事故后必须回滚 max-wait: 5000 # 等待上限5s防止请求线程无限堆积拖垮Tomcat预期效果排队队列在分钟级内清空P99 先回落一个量级。 坑只改池子不改库——max-active总和不能超过 MySQL 的max_connections否则数据库端直接拒绝建连比池子耗尽更难看。动作二打开 removeAbandoned 抓泄露栈原理连接被占用的时长超过removeAbandonedTimeout后强制回收并打印创建该连接时的代码栈泄露在哪一行代码里直接可见。remove-abandoned: true remove-abandoned-timeout: 300 # 秒超时强制回收预期效果日志里出现removeAbandoned相关记录栈帧直指未 close 的位置。 坑源码在 init 时就会打 warnremoveAbandoned is true, not use in productionDruidDataSource.java L1185它是诊断开关不是常驻配置抓完就关。3.2 再调优把参数调到够用且有冗余参数一maxActive 用公式算别拍脑袋原理真正需要的并发连接数 QPS × 单条SQL平均耗时(秒)。例200 QPS × 10ms 2 条加突发冗余 5 倍也就是 10~20配 200 只是给 MySQL 制造压力还占着线程。initial-size: 5 # 启动即建连避免冷启动首波请求全部同步建连 min-idle: 10 max-active: 20 # 按 QPS×耗时×冗余 推算压测校准预期效果低峰期不浪费连接高峰期排队概率显著下降。参数二PSCache 开起来原理预编译语句按连接缓存省掉 DB 侧重复 prepare/parse 的往返。pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 50预期效果参数化写多Oracle 收益最大的负载单条执行时间普遍降 1~5ms。 坑MySQL 上如果 SQL 字面量多样性高比如大量不同 ID 拼进语句命中率很低缓存白占内存这类负载关掉即可。参数三慢SQL阈值收紧到业务容忍线原理StatFilter 默认slowSqlMillis 3000见 StatFilter.java L71生产上 3 秒的慢SQL早就被用户投诉了阈值必须自己定。filter: stat: slow-sql-millis: 200 # 超过200ms记慢SQL log-slow-sql: true # 打日志含参数与耗时 merge-sql: true # 按模板归并避免每条不同参数的SQL各占一行预期效果慢SQL列表从几百条字面量收敛到几十个模板executeSpanNanoMax和lastSlowParameters直接指向最坏现场。3.3 后加固让同类问题不再复发加固一keepAlive 防止库侧断连原理MySQL 的wait_timeout默认 28800s会主动杀掉空闲连接而池子里的引用不知道。开启keepAlive后空闲连接按keepAliveBetweenTimeMillis间隔主动探活默认值是驱逐间隔的 2 倍120s远小于wait_timeout库侧就轮不到它动手了。keep-alive: true time-between-eviction-runs-millis: 60000注意源码有硬校验keepAliveBetweenTimeMillis必须大于timeBetweenEvictionRunsMillis否则会直接抛SQLExceptionDruidDataSource.java L738。加固二批处理负载物理隔离原理报表、导出、对账这类长SQL和在线接口抢同一个池子一次全表扫描就能把activeCount打满。正确做法是独立DruidDataSource实例各配各的max-active在线池的排队曲线从此不受批任务影响。这条没有配置捷径是架构层面的止血针后文案例一就是它。事故复盘 案例一晨报任务吃掉整个池子现象每天上午9:30报表调度启动后全量接口P99从80ms涨到1s以上持续约8分钟监控面板waitThreadCount从0爬到120。定位链路打开/druid数据源页等待队列非空、activeCount贴住maxActive20→ 转SQL监控页Top1模板是一条跨三表聚合executeSpanNanoMax4秒级concurrentMax15——15路报表并发把20个连接基本占空了。止血动作DB侧KILL掉报表会话调度并发从15降到1接口P99 90秒内恢复。根治方案报表负载迁到独立数据源maxActive5在线池彻底解耦同时给那条聚合SQL补了组合索引单条耗时从4s降到180ms。案例二低峰期冷启动风暴血泪教训现象每天凌晨4点到6点流量爬升的头几分钟首波请求RT 200~300ms偶发CommunicationsException白天一切正常按偶发观察了两周。定位链路面板上connectErrorCount与物理建连数呈现每天凌晨定时尖峰错误详情是连接已被服务端关闭——库wait_timeout28800池里挂了一夜的连接早就死了早高峰第一波请求直接踩上去。止血动作test-while-idle: true validation-query: SELECT 1借出前探活把用户踩到死连接降级为单请求多5ms。根治方案按加固一开keep-alive: true120s 的探活间隔远小于库侧 8 小时超时连接永远不会被库静默杀掉。观察一周凌晨尖峰归零。持续优化 闭环流程每轮一次、留痕对比观测/druidJSON API 定时拉waitThreadCount、notEmptyWaitNanos、慢SQL TopN落盘到现有监控分析慢SQL按merge-sql模板聚合后看executeSpanNanoMax与concurrentMax两个数结合 DB 侧执行计划判断是索引问题还是SQL写法问题优化一次只动一个变量加索引、改参数、隔离负载方便归因验证同流量回放对比变更前后连接获取P99与慢SQL条数写进变更单。上线前 checklistmax-wait已显式设置不是默认值 -1slow-sql-millis已收紧到 200~500ms 并开启log-slow-sqlmerge-sql: true慢SQL列表按模板聚合而非按字面量activeCount/maxActive比值与waitThreadCount配了告警阈值keep-alive已开且探活间隔 库侧wait_timeoutremove-abandoned未在 production 常驻开启报表/批处理已迁到独立数据源每次发布记录连接获取P99基线与上一次对比下次发版前先打开/druid面板把等待队列和慢SQL两个页签截个图存档——有了基线你才能在下一次报警时30秒内分清是变差了还是一直就这样。【免费下载链接】druid阿里云计算平台DataWorks(https://help.aliyun.com/document_detail/137663.html) 团队出品为监控而生的数据库连接池项目地址: https://gitcode.com/gh_mirrors/druid/druid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考