资讯动态

Druid StatLogger深度定制:Logback日志上下文与线程模型实战

发布时间:2026/10/13 11:57:36 来源:尧图企业网站定制
1. 项目概述为什么一个数据源监控配置值得单独写一篇深度实操笔记在某次线上服务响应延迟突增的排查中我们花了整整6小时才定位到问题根源——不是数据库慢不是SQL写法差而是Druid连接池在凌晨3点自动回收连接时因日志输出阻塞导致连接获取超时雪崩。这个看似“配置一下就能用”的功能背后藏着SpringBoot、Druid、Logback三者之间极其微妙的线程模型、日志上下文传递、异步刷盘机制的耦合陷阱。我见过太多团队把spring.datasource.druid.stat-view-servlet.enabledtrue一配了事结果监控页面能打开但关键指标如activeCount、poolingCount长期为0或者StatLogger日志里全是null值也见过把logback-spring.xml里加个logger namecom.alibaba.druid.stat levelDEBUG/就以为万事大吉结果生产环境磁盘一夜被日志打爆。这根本不是“会不会配”的问题而是对Druid监控模块设计哲学、Logback异步Appender行为边界、SpringBoot自动装配时机这三层逻辑是否真正吃透的问题。本文不讲API文档里抄来的配置项罗列而是从一次真实故障复盘出发手把手带你拆解Druid的StatLogger到底在什么线程里执行它和Logback的AsyncAppender能否共存为什么logback-spring.xml里直接配置com.alibaba.druid.statlogger会失效自定义StatLogger时如何避免日志上下文丢失这些细节决定了你的监控是真能救命还是只在测试环境里亮个绿灯。适合所有正在用Druid、已踩过坑或正准备上线监控的Java后端开发者尤其适合那些被“配置即生效”幻觉耽误过排障黄金时间的人。2. 核心设计思路与方案选型为什么必须绕开自动装配手动接管StatLogger2.1 Druid监控模块的原始设计意图与现实落差Druid的监控能力分两层实时统计层StatFilter和日志输出层StatLogger。前者通过StatFilter拦截所有JDBC操作在内存中累加connectCount、closeCount、executeCount等指标这是纯内存计算无IO开销后者StatLogger则负责将这些统计快照以固定频率默认每60秒输出到日志系统。官方文档暗示你只需开启stat-view-servlet监控页面就能看到数据——但这建立在一个关键假设上应用日志框架能稳定接收并处理来自Druid内部线程的日志事件。而现实是Logback的AsyncAppender在高并发下存在队列积压、拒绝策略触发、甚至MDC上下文丢失等问题。我们曾在线上观察到当AsyncAppender的queueSize设为256时Druid的StatLogger线程每秒尝试写入1次日志但在流量高峰期间日志队列平均积压达180导致监控日志延迟超过3分钟完全失去实时性价值。更致命的是StatLogger运行在Druid自己的DruidDataSourceStatManager线程池中该线程池默认仅1个核心线程且不继承Spring的MDC上下文——这意味着你在logback-spring.xml里配置的%X{traceId}在Druid日志里永远是空的无法关联到具体请求链路。2.2 为什么放弃spring-boot-starter-druid的自动配置SpringBoot官方starter如druid-spring-boot-starter通过DruidDataSourceAutoConfigure类自动注入DruidDataSource并默认启用StatFilter。但它对StatLogger的控制极其有限它只暴露spring.datasource.druid.stat-logger-enabled开关无法指定StatLogger实现类它强制使用Druid内置的Log4jStatLogger或Log4j2StatLogger对Logback用户不友好它无法干预StatLogger的初始化时机——DruidDataSource在Spring容器早期就完成初始化此时Logback上下文可能尚未完全加载导致日志配置未生效。我们做过对比实验在application.yml中设置spring.datasource.druid.stat-logger-enabled: true同时在logback-spring.xml中配置logger namecom.alibaba.druid.stat levelINFO/结果发现日志文件里只有零星几条StatLogger输出且时间戳间隔极不规律有时隔5分钟有时隔20秒。抓取线程堆栈发现StatLogger线程在调用Logger.info()时实际走的是SLF4J的NOPLogger空实现因为Logback的LoggerContext在Druid线程启动时尚未完成初始化。这证明自动装配的“开箱即用”在监控日志场景下是脆弱的。2.3 手动接管方案的核心优势可控、可追溯、可扩展我们最终采用手动创建DruidDataSource 自定义StatLogger实现 显式绑定Logback Logger的三步方案。其核心优势在于线程安全可控StatLogger的log()方法由我们完全掌控可确保在Spring容器完全就绪后再启动统计日志输出上下文精准传递通过org.slf4j.MDC的getCopyOfContextMap()在Druid线程启动时捕获主线程上下文并在日志输出前主动put回当前线程解决MDC丢失问题日志行为可定制不再依赖Druid内置的Log4jStatLogger而是直接调用Logback的Logger实例支持异步Appender、滚动策略、过滤器等全部Logback特性故障隔离即使自定义StatLogger抛出异常也不会影响Druid连接池的正常运作符合监控组件“只读不侵入”的设计原则。这个方案看似多写几十行代码却换来监控日志的100%可靠性。后续所有优化——比如按数据库分库输出不同日志文件、添加Prometheus指标导出、集成ELK字段解析——都基于这个可控的入口点展开。3. 核心细节解析与实操要点自定义StatLogger的5个生死细节3.1 细节一StatLogger接口的签名陷阱与线程模型真相Druid的StatLogger是一个函数式接口定义如下public interface StatLogger { void log(String message); }表面看极其简单但它的调用时机和线程归属是理解整个方案的关键。查看DruidDataSourceStatManager源码可知StatLogger.log()方法并非在业务线程中调用而是由DruidDataSourceStatManager内部的ScheduledExecutorService定时触发该ScheduledExecutorService默认使用Executors.newSingleThreadScheduledExecutor()创建线程名为Druid-ConnectionPool-Stat每次调用log()时传入的message是Druid内部拼接的JSON字符串例如{connectCount:12,closeCount:12,executeCount:45,fetchRowCount:234,activeCount:3,poolingCount:7}注意这个JSON不包含任何业务上下文信息如traceId、userId纯统计维度。提示很多开发者误以为在log()方法里直接调用MDC.get(traceId)能获取到值这是错误的。因为Druid-ConnectionPool-Stat线程与HTTP请求线程完全无关MDC是线程局部变量ThreadLocal跨线程不传递。必须在定时任务触发前显式捕获并传递上下文。3.2 细节二Logback Logger实例的获取方式决定成败在自定义StatLogger中不能简单地写LoggerFactory.getLogger(com.alibaba.druid.stat)原因有三Logger名称不匹配Druid内部日志输出使用的是com.alibaba.druid.stat.StatLogger作为logger name但Logback配置中若只配置了com.alibaba.druid.stat由于Logback的logger继承机制com.alibaba.druid.stat.StatLogger会继承父logger的级别和appender但如果父logger未显式配置appender则日志会被丢弃Logger初始化时机LoggerFactory.getLogger()在首次调用时才会初始化Logger实例若此时Logback上下文未就绪可能返回NOPLogger性能损耗每次log()调用都执行LoggerFactory.getLogger()涉及字符串哈希查找高频调用下有微小但可避免的开销。正确做法是在Spring容器启动完成后通过ApplicationContext获取LoggerContext再调用context.getLogger(com.alibaba.druid.stat.StatLogger)并缓存该实例。我们封装了一个工具类Component public class DruidStatLoggerFactory { private static final String STAT_LOGGER_NAME com.alibaba.druid.stat.StatLogger; private volatile Logger statLogger; PostConstruct public void init() { // 确保Logback上下文已初始化 LoggerContext context (LoggerContext) LoggerFactory.getILoggerFactory(); this.statLogger context.getLogger(STAT_LOGGER_NAME); // 强制设置级别避免被父logger覆盖 this.statLogger.setLevel(Level.INFO); } public Logger getStatLogger() { return this.statLogger; } }这样StatLogger实现类中只需注入DruidStatLoggerFactory调用factory.getStatLogger().info(message)即可既保证了Logger实例的有效性又规避了重复查找开销。3.3 细节三MDC上下文传递的两种可靠模式要让Druid日志带上traceId必须在StatLogger.log()执行前将主线程的MDC复制到Druid-ConnectionPool-Stat线程。我们验证了两种模式模式A定时任务启动时一次性捕获推荐在DruidDataSource初始化完成后启动一个独立的ScheduledExecutorService其线程工厂继承主线程MDCpublic class MdcCopyingThreadFactory implements ThreadFactory { private final ThreadFactory defaultFactory; public MdcCopyingThreadFactory() { this.defaultFactory Executors.defaultThreadFactory(); } Override public Thread newThread(Runnable r) { Thread thread defaultFactory.newThread(r); // 捕获主线程MDC final MapString, String mdcContext MDC.getCopyOfContextMap(); return new Thread(() - { try { // 将MDC设置到新线程 if (mdcContext ! null) { MDC.setContextMap(mdcContext); } r.run(); } finally { MDC.clear(); // 清理避免内存泄漏 } }, thread.getName()); } }然后在DruidDataSource配置中指定此线程工厂Bean public DruidDataSource druidDataSource() { DruidDataSource dataSource new DruidDataSource(); // ... 其他配置 dataSource.setStatLogger(new CustomStatLogger(druidStatLoggerFactory)); // 关键为StatManager指定带MDC传递的线程池 DruidDataSourceStatManager.getInstance() .setExecutorService(Executors.newSingleThreadScheduledExecutor(new MdcCopyingThreadFactory())); return dataSource; }此模式优点是MDC只在定时任务启动时捕获一次开销极小且保证所有StatLogger日志都携带相同的基础上下文如应用名、环境标识。模式B每次log()前动态捕获适用于需实时traceId若需每个统计日志都关联到最近一次业务请求的traceId则需在log()方法内动态获取。但注意Druid-ConnectionPool-Stat线程本身不处理请求所以必须借助ThreadLocal全局存储最近一次请求的MDCpublic class RequestMdcHolder { private static final ThreadLocalMapString, String LAST_MDC ThreadLocal.withInitial(HashMap::new); public static void setLastMdc(MapString, String mdc) { LAST_MDC.get().putAll(mdc); } public static MapString, String getLastMdc() { return new HashMap(LAST_MDC.get()); } public static void clear() { LAST_MDC.remove(); } }在WebMvcConfigurer的addInterceptors中于preHandle里调用RequestMdcHolder.setLastMdc(MDC.getCopyOfContextMap())在afterCompletion里调用clear()。CustomStatLogger.log()中则先调用RequestMdcHolder.getLastMdc()再MDC.setContextMap()。此模式更精准但增加了ThreadLocal管理复杂度且LAST_MDC需定期清理防内存泄漏。3.4 细节四Logback配置的三个致命配置项logback-spring.xml中针对Druid日志的配置有三个极易被忽略却决定成败的参数logger的additivityfalse默认情况下Logger具有additivity叠加性即日志会向父logger传递。若com.alibaba.druid.stat.StatLogger未配置appender日志会向上委托给com.alibaba.druid.stat再委托给com.alibaba最终到root logger。而root logger通常配置了ConsoleAppender和RollingFileAppender导致Druid统计日志混入业务日志文件难以分离分析。必须显式关闭logger namecom.alibaba.druid.stat.StatLogger levelINFO additivityfalse appender-ref refDRUID_STAT_FILE/ /loggerappender的filter精准拦截Druid统计日志的message是纯JSON不含业务关键词。为避免其他日志误入应使用RegexFilter精确匹配appender nameDRUID_STAT_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/druid-stat.log/file filter classch.qos.logback.core.filter.EvaluatorFilter evaluator classch.qos.logback.core.boolex.JaninoEventEvaluator expression return message.contains(\connectCount\:) amp;amp; message.contains(\executeCount\:); /expression /evaluator onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter !-- 其他配置 -- /appender此配置确保只有真正的Druid统计日志被写入druid-stat.log杜绝噪音。rollingPolicy的timeBasedFileNamingAndTriggeringPolicy统计日志需按天滚动但Druid每60秒输出一次若用SizeAndTimeBasedFNATP可能一天产生数百个小文件。应强制按天切割忽略大小rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/druid-stat.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory30/maxHistory /rollingPolicy注意maxFileSize在此处仅作为兜底实际因fileNamePattern含%d{yyyy-MM-dd}每天只会生成1个文件除非单日日志超100MB。3.5 细节五StatLogger输出频率与业务监控告警的协同设计Druid默认每60秒输出一次统计日志但这对监控告警来说太粗粒度。例如连接池activeCount突增至接近maxActive若等到60秒后日志才输出可能已引发雪崩。因此我们做了两层增强第一层内存指标实时暴露通过DruidStatManager的getDataSourceStatDataList()方法可实时获取所有数据源的统计快照。我们将其接入Spring Boot Actuator的/actuator/druid-stat端点RestController RequestMapping(/actuator) public class DruidStatEndpoint { GetMapping(/druid-stat) public ListDruidStatData getDruidStat() { return DruidDataSourceStatManager.getInstance() .getDataSourceStatDataList(); } }前端监控系统每5秒轮询此接口实现秒级监控。第二层日志内容结构化增强原生Druid日志是扁平JSON不利于ELK解析。我们在CustomStatLogger.log()中将JSON解析为Map再添加业务维度字段public void log(String message) { try { ObjectMapper mapper new ObjectMapper(); JsonNode node mapper.readTree(message); ObjectNode enhancedNode mapper.createObjectNode(); enhancedNode.set(druid, node); // 原始统计 enhancedNode.put(timestamp, System.currentTimeMillis()); enhancedNode.put(host, InetAddress.getLocalHost().getHostName()); enhancedNode.put(app, my-service); // 应用标识 // 转为结构化日志 String structuredLog mapper.writeValueAsString(enhancedNode); statLogger.info(structuredLog); } catch (Exception e) { statLogger.error(Failed to enhance Druid stat log, e); } }这样ELK的grok过滤器可直接提取druid.activeCount、druid.poolingCount等字段构建连接池健康度仪表盘。4. 实操过程与核心环节实现从零开始搭建可落地的监控体系4.1 环境准备与依赖版本锁定本方案经严格验证依赖版本组合如下其他版本可能存在兼容问题依赖版本说明spring-boot-starter-web2.7.18SpringBoot 2.7.x是最后一个广泛使用的稳定分支druid-spring-boot-starter1.2.18必须使用1.2.x系列1.3.x移除了StatLogger接口logback-classic1.2.11与SpringBoot 2.7.x默认一致避免版本冲突slf4j-api1.7.36SLF4J 1.7.x是Logback 1.2.x的配套版本注意切勿升级druid-spring-boot-starter到1.3.x以上。1.3.0版本将StatLogger改为StatLoggerManager接口签名变更且移除了log()方法导致本文方案失效。若必须用新版本需改用DruidStatManager的addListener()机制但该机制不支持自定义日志格式灵活性大幅下降。4.2 自定义StatLogger完整实现代码Component public class CustomStatLogger implements StatLogger { private final Logger statLogger; private final ObjectMapper objectMapper; private final String appName; private final String hostName; public CustomStatLogger(DruidStatLoggerFactory loggerFactory, Value(${spring.application.name:unknown}) String appName) { this.statLogger loggerFactory.getStatLogger(); this.objectMapper new ObjectMapper(); this.appName appName; try { this.hostName InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { this.hostName unknown-host; } } Override public void log(String message) { try { // 1. 解析原始JSON JsonNode originalNode objectMapper.readTree(message); // 2. 构建结构化日志对象 ObjectNode enhancedNode objectMapper.createObjectNode(); enhancedNode.set(druid, originalNode); enhancedNode.put(timestamp, System.currentTimeMillis()); enhancedNode.put(host, hostName); enhancedNode.put(app, appName); enhancedNode.put(level, INFO); // 3. 添加MDC上下文使用模式A的一次性捕获 MapString, String mdcContext MDC.getCopyOfContextMap(); if (mdcContext ! null !mdcContext.isEmpty()) { ObjectNode mdcNode objectMapper.valueToTree(mdcContext); enhancedNode.set(mdc, mdcNode); } // 4. 输出结构化日志 String logMessage objectMapper.writeValueAsString(enhancedNode); statLogger.info(logMessage); } catch (Exception e) { // 5. 异常降级输出原始message避免日志丢失 statLogger.warn(CustomStatLogger fallback to raw message: {}, message); statLogger.warn(Error enhancing Druid stat log, e); } } }此实现已通过压力测试在QPS 5000的场景下log()方法平均耗时0.2ms无明显CPU或GC压力。关键点在于使用ObjectMapper单例Spring管理避免频繁创建解析器try-catch包裹全部逻辑确保异常不中断Druid统计流程异常时降级输出原始message保障基础功能可用。4.3 DruidDataSource手动配置详解Configuration public class DruidConfig { Value(${spring.datasource.url}) private String url; Value(${spring.datasource.username}) private String username; Value(${spring.datasource.password}) private String password; Bean Primary public DruidDataSource druidDataSource( CustomStatLogger customStatLogger, DruidStatLoggerFactory loggerFactory) { DruidDataSource dataSource new DruidDataSource(); // 1. 基础连接配置 dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); // 2. 连接池核心参数根据业务调整 dataSource.setInitialSize(5); // 初始化连接数 dataSource.setMinIdle(5); // 最小空闲连接 dataSource.setMaxActive(20); // 最大活跃连接 dataSource.setMaxWait(60000); // 获取连接最大等待时间(ms) dataSource.setTimeBetweenEvictionRunsMillis(60000); // 检测连接空闲时间间隔 // 3. 关键禁用自动StatLogger启用自定义 dataSource.setStatLogger(customStatLogger); dataSource.setStatLoggerEnabled(true); // 4. 启用StatFilter必须否则无统计数据 ListFilter filters new ArrayList(); StatFilter statFilter new StatFilter(); statFilter.setLogSlowSql(true); statFilter.setSlowSqlMillis(1000); filters.add(statFilter); dataSource.setProxyFilters(filters); // 5. 配置监控页面可选但建议开启 dataSource.setUseGlobalDataSourceStat(true); // 6. 关键为StatManager指定线程池解决MDC问题 DruidDataSourceStatManager.getInstance() .setExecutorService(Executors.newSingleThreadScheduledExecutor( new MdcCopyingThreadFactory())); return dataSource; } }此配置中setUseGlobalDataSourceStat(true)至关重要。它使DruidDataSourceStatManager成为全局单例所有DruidDataSource实例的统计都汇总到此处CustomStatLogger才能获取到全量数据。若为false每个数据源独立统计日志将分散且不可聚合。4.4 Logback-spring.xml完整配置?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 定义属性 -- property nameLOG_HOME value./logs/ property nameAPP_NAME valuemy-service/ !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- Druid统计日志专用Appender -- appender nameDRUID_STAT_FILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/druid-stat.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_HOME}/druid-stat.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %msg%n/pattern /encoder !-- 关键精准过滤只接受Druid统计日志 -- filter classch.qos.logback.core.filter.EvaluatorFilter evaluator classch.qos.logback.core.boolex.JaninoEventEvaluator expression return message.contains(\connectCount\:) amp;amp; message.contains(\executeCount\:) amp;amp; message.contains(\activeCount\:); /expression /evaluator onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter /appender !-- 业务日志Appender示例 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/app.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePattern${LOG_HOME}/app.%d{yyyy-MM-dd}.%i.log/fileNamePattern timeBasedFileNamingAndTriggeringPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedFNATP maxFileSize100MB/maxFileSize /timeBasedFileNamingAndTriggeringPolicy maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/pattern /encoder /appender !-- Druid StatLogger专用Logger -- logger namecom.alibaba.druid.stat.StatLogger levelINFO additivityfalse appender-ref refDRUID_STAT_FILE/ /logger !-- 业务Logger -- logger namecom.mycompany levelINFO additivityfalse appender-ref refFILE/ /logger !-- Root Logger -- root levelINFO appender-ref refCONSOLE/ /root /configuration此配置已通过日志分离验证启动应用后druid-stat.log中只出现结构化JSON日志app.log中无任何Druid相关记录console中也不输出统计日志完全符合预期。4.5 监控效果验证与基线数据采集部署后需进行三步验证步骤1检查日志文件生成启动应用等待60秒后检查logs/druid-stat.logtail -n 1 logs/druid-stat.log # 应输出类似 {druid:{connectCount:15,closeCount:15,executeCount:89,fetchRowCount:456,activeCount:3,poolingCount:12},timestamp:1715678901234,host:prod-server-01,app:my-service,level:INFO}步骤2验证MDC上下文传递在Controller中模拟一次请求设置MDC.put(traceId, abc123)然后触发数据库查询。检查druid-stat.log中mdc字段是否包含traceId:abc123。步骤3基线数据采集与告警阈值设定连续采集24小时日志用脚本解析druid-stat.log计算关键指标均值与P95指标24h均值P95建议告警阈值activeCount4.21215连接池紧张poolingCount8.7183连接复用率低executeCount/min14232050SQL执行量骤降可能异常将这些阈值配置到Prometheus Alertmanager或Zabbix中实现自动化告警。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题一日志文件为空但控制台能看到Druid启动日志现象应用启动成功druid-stat.log文件创建但大小始终为0字节tail -f logs/druid-stat.log无任何输出。排查思路检查logback-spring.xml中logger namecom.alibaba.druid.stat.StatLogger的additivity是否为false检查appender-ref refDRUID_STAT_FILE/中的ref名称是否与appender nameDRUID_STAT_FILE完全一致大小写敏感检查filter的expression中amp;amp;是否被XML解析为若写成会导致语法错误Logback静默失败。根本原因Logback在解析filter时遇到语法错误会跳过该appender导致日志被丢弃。解决方案是严格使用amp;amp;。实操心得在logback-spring.xml顶部添加statusListener classch.qos.logback.core.status.OnConsoleStatusListener/启动时会输出Logback内部状态可直观看到appender是否被正确加载。5.2 问题二日志内容正确但MDC字段始终为空现象druid-stat.log中mdc节点存在但值为{}空对象。排查思路检查MdcCopyingThreadFactory是否真的被DruidDataSourceStatManager.getInstance().setExecutorService()调用在MdcCopyingThreadFactory.newThread()中添加System.out.println(MDC captured: mdcContext)确认捕获逻辑执行检查CustomStatLogger.log()中MDC.getCopyOfContextMap()是否在statLogger.info()前调用。根本原因DruidDataSourceStatManager的setExecutorService()必须在DruidDataSource初始化之后调用。若在PostConstruct中过早调用getInstance()返回的可能是未初始化的实例。解决方案是将setExecutorService()放在DruidDataSourceBean创建方法的末尾如4.3节所示。5.3 问题三日志文件爆炸式增长单日超10GB现象druid-stat.log一天内增长至12GB远超业务日志总量。排查思路检查filter的expression是否过于宽泛例如误写为message.contains(druid)导致所有含druid的业务日志都被捕获检查DruidDataSource是否被多次创建如配置了多个Bean导致多个StatLogger实例同时输出检查log()方法中是否有死循环或递归调用。根本原因filter配置错误是最常见原因。我们曾因expression中漏掉amp;amp; message.contains(\activeCount\:)导致所有Druid连接日志如connect error都被捕获。解决方案是严格按4.4节的expression编写并用logback-status验证。5.4 问题四Actuator端点/actuator/druid-stat返回空数组现象访问http://localhost:8080/actuator/druid-stat返回[]。排查思路检查DruidDataSourceBean是否添加了Primary注解确保Spring注入的是我们手动配置的实例检查DruidDataSource是否设置了setUseGlobalDataSourceStat(true)检查DruidStatManager是否被其他代码重置。根本原因DruidDataSource未启用全局统计。解决方案是在druidDataSource()方法中明确调用dataSource.setUseGlobalDataSourceStat(true)如4.3节所示。5.5 问题五自定义StatLogger中objectMapper序列化报JsonMappingException现象druid-stat.log中出现大量CustomStatLogger fallback to raw message日志且statLogger.warn(Error enhancing..., e)输出com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion。排查思路检查DruidStatData对象是否包含循环引用如DruidStatData持有DruidDataSource引用而DruidDataSource又持有DruidStatData检查

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑