资讯中心

Druid连接池故障排查实战:从超时到监控告警与加密

📅 2026/9/28 13:35:08
Druid连接池故障排查实战:从超时到监控告警与加密
凌晨 1 点 47 分运维群里突然炸出一句话Druid 挂了线上全在报错紧接着我的手机开始连环震动——订单接口、支付回调、用户查询全部超时。登录服务器一看日志里密密麻麻全是GetConnectionTimeoutException数据库连接池被彻底打满。Druid 是 Java 生态里用得最广的数据库连接池之一Spring Boot 项目里几乎都有它的身影。它一旦崩了表象是连接耗尽本质却往往藏着一连串配置、代码和运维问题。这篇文章就拿一次真实事故当引子把 Druid 从连接池炸锅到定位根因再到用监控页面做排查的完整链路讲清楚最后结合若依框架顺手把数据库密码加密也落地。整个过程对后端开发、DBA、运维同学都有参考价值尤其是那些线上跑了很久、从没认真看过连接池参数的团队这篇文章值得反复看。1. 事故现场连接池耗尽到底长什么样1.1 报错特征与第一反应先说说当天的现象。线上的错误日志堆积速度肉眼可见地疯涨核心报错就两类第一类是这样的com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 10000, active 20, maxActive 20, creating 0第二类是各种业务方法的DataAccessException和CannotGetJdbcConnectionException。第一眼看到active 20, maxActive 20就明白了不是 SQL 写错不是数据库宕机而是连接池里所有连接都被占满新的请求拿不到连接等满默认的 10 秒maxWait直接超时。这里有个很重要的认知Druid 本身不会崩溃它是被业务流量和资源耗尽拖垮的。连接池是个有上限的资源池当并发请求超过池子上限或者池子里的连接被人为借走不还后续请求就只能排队、超时、报错。我当时的判断顺序是确认数据库本身活着——查库负载、慢查询、CPU、连接数。确认应用进程没挂——JVM 内存、GC 情况、线程数。重点看 Druid 连接池的运行指标——活跃连接数、空闲连接数、等待线程数。第 1、2 步通常很快就能排除掉真正的戏都在第 3 步里。1.2 为什么重启大法只能救急当时的第一反应肯定是重启应用。重启确实有效连接池被清空重建服务三五分钟内就恢复了业务看起来正常了。但所有人都知道这只是把病压下去病灶还在。重启之所以只能救急是因为连接池的状态是 JVM 内存里的运行时数据。一重启所有连接断开重连之前的连接占用混乱被抹掉了。但如果引发占用混乱的代码缺陷没修、参数不合理没调流量一上来同样的故障必然复现。那次事故更典型重启后大概四个小时连接池又满了而且这次崩得更彻底连监控页面都打不开——因为打开监控页面的请求也要走 Tomcat 线程而 Tomcat 线程全堵在等待数据库连接上。到这一步必须静下来做正经排查了。2. 定位根因把 Druid 监控页面真正用起来2.1 监控页面怎么开很多项目里 Druid 依赖早就加上了但监控页面从没开过或者开了也没人看。这其实是把最好用的诊断工具白白浪费了。Druid 内置的StatViewServlet提供了完整的 Web 监控页面Spring Boot 项目配合 Druid Starter 只需要在配置文件里加一段spring: datasource: druid: stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: 你的强密码 allow: 127.0.0.1,内网IP段 deny: web-stat-filter: enabled: true url-pattern: /* exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*这里必须提醒一句监控页面绝不能裸奔在公网。Druid 监控页面能直接看到所有 SQL 和连接池状态这就是数据库的一张透视 X 光片泄露出去等于把库表结构、慢 SQL、连接池水位全部暴露给攻击者。allow一定要限制内网 IP 或本机login-username和login-password不要用弱口令更不要用默认的 admin/admin。加上配置后重启应用浏览器访问http://服务IP:端口/druid/index.html输入账号密码就能进监控页。这个页面里的信息密度极高也极容易被看花眼关键要抓的指标其实就那几个。2.2 三个关键指标怎么读连接池信息页签里有大量数字逻辑连接打开次数、逻辑连接关闭次数、活跃连接数、空闲连接数、等待线程数、初始连接数、最大活跃连接数等等。排查事故时我不建议逐个看先抓住三组指标含义危险信号活跃连接数当前被业务占用的连接数量持续高位长期 maxActive 的 80%空闲连接数当前空置、可被分配出去的连接趋近于 0且活跃连接在高位等待线程数请求在队列里等连接的线程数量长期 0说明连接不够用了事故当天的数据非常典型活跃连接数稳定在 20等于 maxActive空闲连接数 0等待线程数一直在 15 到 40 之间抖动。看到这个组合拳基本可以断定是连接只借不还加池子太小两个问题叠加。但光看这几个数字还不够得继续往下钻。2.3 SQL 统计里藏着真凶监控页面的 SQL 监控页签才是这次排查里真正立功的地方。这里能看到每条 SQL 的执行次数、总执行时间、最大执行时间、平均执行时间、错误次数。事故发生时我按执行时间最大值倒序排了一遍有两类 SQL 浮出水面一类是报表查询单条 SQL 最大执行时间到了 8 秒多执行次数还不少。这类 SQL 本身就是慢 SQL在连接池资源紧张时雪上加霜每执行一次就占住连接 8 秒20 个连接很快就被这类查询消耗干净。另一类更隐蔽是一条简单的SELECT看起来人畜无害执行次数却异常高而且在连接获取的记录里它对应的物理连接打开次数远大于逻辑连接打开次数的合理范围。顺着代码一查发现有个定时任务的finally块里漏写了connection.close()每个任务跑一次就泄漏一个连接。定时任务每 5 分钟跑一次跑一晚上泄漏的连接数量可想而知。到这里事故的全貌基本浮出水面了慢 SQL 拉长单连接占用时间代码泄漏不断蚕食可用连接连接池参数又没扛住三者叠加直接把 Druid 打崩。3. 最常见的几种Druid 崩溃原因3.1 连接泄漏最隐蔽的杀手连接泄漏是生产环境最常见、也最讨厌的问题。它的特点是单个连接占用时间不长但泄漏是持续性的池子里的连接被一点一点抽干等发现时已经晚了。典型的代码写法是这样的// 错误示范finally 块里忘了 close Connection conn null; try { conn dataSource.getConnection(); // 业务逻辑 } catch (Exception e) { log.error(查询失败, e); } // 完全没有归还连接正确写法其实很简单优先用 try-with-resources// 正确示范自动归还连接 try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 业务逻辑 } catch (Exception e) { log.error(查询失败, e); }Java 7 开始就支持 try-with-resources只要Connection、Statement、ResultSet都实现了AutoCloseable就能在代码块结束后自动关闭省掉手写finally的繁琐也彻底避开忘记 close 的坑。我们团队现在 code review 时凡是手写conn.close()的地方都会多看两眼能用 try-with-resources 就统一用。如果代码里连接的使用特别分散还可以考虑动态代理的方式统一拦截但这种改造量较大。更现实的做法是把removeAbandoned打开让 Druid 在连接被借出超过设定时间后强制回收removeAbandoned: true removeAbandonedTimeout: 180 logAbandoned: trueremoveAbandonedTimeout单位是秒180 表示一个连接被借出超过 180 秒就强制回收。这个配置是双刃剑后面我会专门讲它误伤长事务的坑。3.2 参数配置不合理资源明明够却被参数锁死另一类崩溃纯粹是人祸。很多 Spring Boot 项目里的 Druid 参数是从网上抄的抄的时候没理解含义线上环境一换就出事。比如maxActive默认只有 8对一些稍微有点流量的服务完全不够用。而maxWait默认是 -1也就是不等待、立即报错有些项目把它设成 1000010 秒看似给了缓冲但当连接长期排队时10 秒的等待时间又让接口响应变得极慢前端超时重试重试又加重连接池压力形成恶性循环。参数配置问题引发的崩溃有非常明显的特征活跃连接数在高峰期顶到maxActive但数据库本身 CPU、内存、连接数都低得很。排查时用监控页面一眼就能看出来瓶颈不在数据库也不在应用代码里就是池子参数把并发量锁死了。3.3 数据库主动断连连接变成了僵尸MySQL 服务端有个wait_timeout参数默认 8 小时。如果一条连接空闲超过 8 小时MySQL 会在服务端把它断开。但应用侧不知道连接池里的连接还活着直到某次请求真的用到它才发现连接不可用。这种情况的表现是系统平稳运行了很久某天突然出现大量CommunicationsException或Connection is not available, request timed out然后连接池开始疯狂重建连接数据库压力骤增进而拖垮服务。缓解思路是让 Druid 自动维护空闲连接几项关键配置组合起来testWhileIdle: true testOnBorrow: false testOnReturn: false validationQuery: SELECT 1 timeBetweenEvictionRunsMillis: 60000 keepAlive: truetestWhileIdle会在连接空闲时用validationQuery验证连接是否还活着keepAlive保证池子里的最小连接数一直有鲜活的连接补充。这套组合也是长期稳定运行的标配。3.4 慢 SQL 拖垮连接池慢 SQL 是连接池的慢性毒药。数据库连接是稀有资源一条 SQL 执行 5 秒就相当于把连接占住 5 秒。同一时刻有 10 条这样的 SQL20 个连接就去了一半。排查慢 SQLDruid 监控页面的 SQL 监控页签结合慢 SQL 统计非常好用。如果开了druid.stat.slowSqlMillis配置执行时间超过阈值的 SQL 会被单独统计出来。配合数据库端的慢查询日志基本能锁定是哪条 SQL 出了问题。慢 SQL 的治理是个大话题常见思路无外乎加索引、改写 SQL、拆大查询、引入缓存。但落到连接池层面至少要把慢 SQL 监控阈值设好让它成为日常巡检的一部分而不是等事故发生了再回头翻。4. 根治实践参数调整 代码层防治 告警闭环4.1 一套可参考的连接池参数与计算逻辑那次事故之后我把核心服务的连接池参数整体重调了一遍。这里不能直接照抄别人的值因为每个服务的 QPS、单请求数据库耗时、业务峰谷都不一样但计算逻辑可以复用估算方式maxActive约等于高峰期QPS × 单次业务平均占用连接秒数再乘 2~3 倍余量。比如高峰期 QPS 200平均每个请求占用连接 50ms理论并发连接数约 10乘余量后maxActive定 30 比较稳。我当时给其中一个核心服务定的参数如下参数推荐值说明initialSize5启动时预创建的连接数minIdle10最小空闲数防止突发流量没连接可用maxActive30根据压测和 QPS 估算出来maxWait30000获取连接最大等待 30 秒超过即报错timeBetweenEvictionRunsMillis60000每 60 秒扫描一次空闲连接minEvictableIdleTimeMillis300000空闲 5 分钟以上的连接才考虑回收validationQuerySELECT 1轻量探活 SQLtestWhileIdletrue空闲时探活testOnBorrowfalse获取连接时不额外探活省开销testOnReturnfalse归还时不探活keepAlivetrue保持最少连接数存活这里要注意testOnBorrow设为 true 虽然能保证拿到的连接一定可用但每次拿连接都多一次探活高并发下对数据库是额外压力。testWhileIdle加keepAlive的组合对绝大多数场景已经足够。4.2 连接泄漏的代码层防治光调参数不行代码里的泄漏源必须堵上。那次事故后我们做了三件事第一全面排查项目中所有手写 JDBC 的地方能改 try-with-resources 的全改掉。这个工作量大但收益直接。第二打开logAbandoned: true。这样当 Druid 强制回收连接时会把当时的调用栈打出来方便定位是哪段代码长期占用连接。这个日志在排查泄漏时价值巨大相当于 Druid 帮你把谁借了不还给标记了出来。第三引入连接池使用率的监控告警。Druid 的监控数据可以通过StatFilter的统计数据对外暴露接入 Prometheus 的druid插件后把活跃连接数 / maxActive配成一个指标。我定的告警规则是活跃连接占比连续 5 分钟超过 80% 就发警告超过 95% 直接进入 P1 告警。这样再也不用等业务方先发现系统卡了我们能在连接池崩溃前 10 到 20 分钟收到预警。4.3 建设监控告警闭环这里多说一句排查 Druid 事故千万不要只盯着应用日志。数据库端的连接数趋势、网络抖动情况、应用所在机器的文件句柄数都可能跟连接池异常有关。我遇到过一次很隐蔽的情况应用所在容器的文件句柄数满格导致新建数据库连接时无法创建 socketDruid 的连接数骤降服务同样报获取连接失败。当时监控页面里连池子大小都不正常后来一查是日志文件没做轮转把文件句柄耗尽了。这类问题跟连接池参数没关系属于运维层面但也侧面说明故障排查要有多维度视角。那次事故的完整修复方案包括参数调优、泄漏代码修复、慢 SQL 治理、监控告警上线。做完这四件事之后同一个服务再没有因为连接池问题崩过。5. 进阶实践结合若依框架做数据库密码加密5.1 为什么要在配置层加密数据库密码事故过后团队复盘提了一个很尖锐的问题现在生产环境的数据库密码是明文放在配置中心里的如果代码仓库泄露或者服务器被入侵数据库直接裸奔要不要顺便把密码加密做了很多项目的application.yml或application-druid.yml里数据库密码就是一串明文。稍微好一点的会放到配置中心或环境变量里但本质上只要拿到服务器权限就能从配置文件里捞出密码。Druid 官方提供了ConfigTools工具类可以对数据库密码做非对称加密私钥只保存在服务器本地配置里只放公钥和密文即使配置泄露没有私钥也解不出原始密码。对使用若依框架的项目来说这件事尤其顺手。若依框架本身就把 Druid 集成得很完整数据源配置集中在application-druid.yml支持多数据源改造起来不需要动业务代码。5.2 ConfigTools 生成密钥的操作过程具体操作分三步。第一步拿到 Druid 的 jar 包执行命令行生成密钥对和密文java -cp druid-1.2.20.jar com.alibaba.druid.filter.config.ConfigTools 你的数据库密码命令执行后控制台会输出三样东西privateKey: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQ... publicKey: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... password: OKb8o1w4iHj3kI9H8h/Y0A7uD8H6KbO5qC7y8x9s...第二步把publicKey和password配置到若依框架的application-druid.yml里。注意privateKey千万不要放到配置文件或代码仓库里它只应该以环境变量的形式存在于服务器上。第三步修改 Druid 的过滤器配置把解密开关打开并指定公钥。在若依框架里配置大致是这样的spring: datasource: druid: # 主库数据源 master: url: jdbc:mysql://localhost:3306/ry?useUnicodetruecharacterEncodingutf8 username: root password: OKb8o1w4iHj3kI9H8h/Y0A7uD8H6KbO5qC7y8x9s... driver-class-name: com.mysql.cj.jdbc.Driver # 公钥配置 public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... # 开启 config 过滤器 filter: config: enabled: true connection-properties: config.decrypttrue;config.decrypt.key${spring.datasource.druid.public-key}这里有个容易踩坑的地方若依框架的数据源是通过DruidDataSourceBuilder.create().build()创建的配置项前缀是spring.datasource.druid.masterpublic-key如果放在 master 节点外面不一定能被自动绑定。稳妥做法是把公钥作为环境变量注入然后通过${}占位符引用例如connection-properties: config.decrypttrue;config.decrypt.key${DRUID_PUBLIC_KEY}然后在服务器环境变量里设置DRUID_PUBLIC_KEY。这样配置文件和代码仓库里就只有一段占位符解密密钥只在运行环境里存在。5.3 加密后如何验证改完配置重启应用先看日志里有没有报ConfigFilter相关的异常。如果公钥配错或密文不完整Druid 启动时就会报解密失败根本不会等到第一个请求。更直观的验证方式是打开 Druid 监控页面进数据源信息页签。如果能看到数据源正常初始化、连接池成功建连说明解密链路是通的。再用一个需要查数据库的接口跑一遍确认业务没受影响整个加密改造就算完成了。留一个额外的安全建议如果项目里用了配置中心比如 Apollo、Nacos加密改造后不要把privateKey也放进配置中心。原则是——配置中心可以存公钥、密文、连接地址但私钥永远只存在于服务器本地文件或环境变量里。6. 踩坑实录与日常巡检清单6.1 我踩过的三个典型的坑第一个坑是removeAbandonedTimeout设太小。当时为了尽快清理连接泄漏把超时时间设成了 60 秒结果有一些正常的业务长事务比如批量导入、报表导出被 Druid 误判成泄漏直接回收连接导致事务中途失败数据回滚业务方投诉了好几次。这个参数默认 300 秒是有道理的除非确认线上没有长事务否则不要盲目调小。先定位泄漏代码、修复泄漏源头再考虑用强制回收兜底。第二个坑是开启监控页面的url-pattern配置错了。有一次把stat-view-servlet的路径配成了/*导致所有请求都走了 Druid 的 servlet filter静态资源加载变慢接口响应也多了额外耗时。正确做法是/druid/*这种独立路径并且web-stat-filter的exclusions要把静态资源后缀排除干净。第三个坑是升级 Druid 版本后没有回归测试。低版本升级到 1.2.x 时默认参数有变化比如keepAlive的默认行为、Wall Filter 的拦截规则都可能调整。直接上生产后出现了一批 SQL 被 wall 过滤器拦截的报错。后续我再升级版本都会先在测试环境跑一遍完整的 SQL 回归用例重点看慢 SQL 和异常 SQL 的表现。6.2 一套实用的日常巡检做法经历过那次事故后我整理了一套连接池巡检习惯放在这里供参考每天早上看一眼 Druid 监控页面的活跃连接曲线重点看夜间是否有异常爬升。每周检查一次 SQL 监控里的慢 SQL 列表新增的慢 SQL 要及时处理。每周抽查代码仓库里新增的 JDBC 操作确认都走了 try-with-resources。每月检查一次连接池参数的合理性结合业务量变化做微调。每个季度至少做一次故障演练人为制造连接泄漏验证告警和快速定位能力。数据库密码加密改造越早做越好等出事了再补代价远大于提前预防。这里额外说一句Druid 监控页面的数据是进程内统计应用一重启数据就清零了。如果团队需要长期保存 SQL 性能数据做对比分析可以考虑用 Prometheus 采集 Druid 的指标或者定期导出监控数据到日志系统不然历史趋势丢了一些问题很难复盘。6.3 说到底Druid 崩了不是 Druid 的错回到开头那句话Druid崩了的瞬间确实吓人但事后复盘会发现几乎所有连接池事故都逃不出三个原因代码泄漏、参数失配、慢 SQL 拖累。Druid 本身只是忠实地执行了配置——你把池子设成 20到了 20 就线程排队排队超时就报错这套行为逻辑是清晰且符合预期的。真正要反思的是我们有没有给它配一个合理的池子有没有盯紧池子里的水位变化。那次事故给我最深的体会是连接池参数不是配一次就完事的配置项它跟业务 QPS、慢 SQL、数据库负载强相关需要持续观察、持续微调。而 Druid 监控页面就是观察这口池子最好的窗口。打开它读懂它配合告警和加密加固Druid 才能从背锅侠变成真正可靠的基础设施。希望这篇文章能帮你在下次线上炸锅之前先把池子看住。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案