尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot日志配置实战:Logback与Log4j2性能对比及异步日志选型指南

Spring Boot日志配置实战:Logback与Log4j2性能对比及异步日志选型指南 上个月我帮一个朋友排查生产接口偶发超时的问题打开线上服务器日志目录的时候人有点懵——logs/application.log已经攒到好几个 G里面一大半是第三方 SDK 打出来的 DEBUG真正报错的信息早就被冲散找不到了。后来我花了一个周末把 Spring Boot 的日志配置重新理了一遍又把实现从 Logback 切到了 Log4j2 的异步方案线上排查链路一下子就顺了。这也是我这篇文章想跟你聊透的问题Spring Boot 日志配置的基本盘是什么Logback vs Log4j2 的性能差异到底在哪以及最后到底怎么选型才不后悔。不管你是刚接触 Spring Boot还是已经维护了几年老项目下列内容应该都有能直接用上的一两段。我会先讲清楚默认日志链路再给 Logback 和 Log4j2 的性能对比做一个现实版解读然后给出一套完整的切换步骤和选型清单。1. 从运行日志到排障体系Spring Boot日志配置需要掌握的全局概念1.1 Spring Boot默认日志链路里到底有什么新建一个 Spring Boot 项目时pom 里哪怕只写了spring-boot-starter-web日志相关依赖其实已经自动进来了。默认日志栈由spring-boot-starter-logging提供它把 SLF4J 作为门面底下接的是 Logback。这个 starter 内部还带了两个桥接器log4j-to-slf4j和jul-to-slf4j作用是把那些仍然用 Log4j 1.x 或java.util.logging的第三方库日志统一转接到 SLF4J 上再交给 Logback 输出。这里想强调一个很基础但容易被忽略的点业务代码不要直接依赖 Logback 或者 Log4j2而是只面向 SLF4J API 编程。框架实现在依赖层面切换业务代码一行都不用改。这也是为什么 Logback 和 Log4j2 的选型冲突大多数时候不会影响业务类只影响pom.xml和日志配置文件。1.2 通过控制台日志先看懂配置生效情况如果只是用默认配置可以先打开控制台看看启动日志再决定要不要做自定义。Spring Boot 对日志的封装配置基本集中在application.yml里logging: level: root: info com.example.biz: debug file: name: logs/application.log pattern: console: %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %thread %logger{36} - %msg%nlogging.level是按 Logger 名配置级别logging.file.name会打开一个文件输出通道logging.pattern控制文本格式。这套配置在小型项目里够用但有两个隐患一是没有按天/按大小滚动日志文件会无限增长二是 pattern 里没有 traceId 这类链路字段排障时很难把一条请求串起来。1.3 什么时候必须从默认配置升级到自定义Appender默认配置做不到的事情基本上就是项目从开发走向生产时不得不面对的事情日志要按日期和大小滚动、要同时输出到控制台和文件、要针对不同模块设置不同级别和输出目标、要在日志里带 MDC 业务字段、要用异步写入避免业务线程被 IO 拖死。遇到这些情况就需要在 classpath 下放独立的日志框架配置。用 Logback 就是logback-spring.xml用 Log4j2 就是log4j2-spring.xml。下面说的性能对比、切换过程全都围绕着这两类配置文件展开。2. Logback与Log4j2的性能差距分析从同步阻塞到无锁异步队列2.1 日志写入路径上的三个主要瓶颈先算一笔最简单的账如果业务线程同步打日志每一条日志至少会经历级别过滤、格式化成字符串、拿到输出流锁、调用 IO 写入这几个步骤。日志量小的时候多出的几微秒根本感觉不出来一旦吞吐量上到每秒几万条写锁竞争和 GC 压力就会开始隐形拉低业务线程速度。第一个瓶颈是锁竞争。多个业务线程同时写同一个输出流Logback 默认需要抢锁。第二个瓶颈是内存分配。每条日志事件都是一个对象包含消息、时间戳、线程名、堆栈信息高并发下会产生大量短暂对象增加 GC 压力。第三个瓶颈是 IO 阻塞。写入磁盘或 stdout 的底层系统调用如果变慢比如磁盘 IO 被其他服务挤占业务线程会直接卡在日志这步。很多团队以为日志不影响性能觉得“就一行字而已”。等压测或者线上高峰一跑同步日志引起的线程阻塞其实是所有隐性问题里最容易被忽略的那种。2.2 AsyncAppender与AsyncLogger的实现差异Logback 也有自己的异步方案核心是AsyncAppender。它内部用一个BlockingQueue业务线程把日志事件丢进队列后立即返回再由后台消费线程取出来写盘。这种方式确实能解决一部分同步阻塞但队列本身是有锁的日志事件对象仍然要频繁创建在高并发下吞吐相对有限。Log4j2 的AsyncLogger走的是 LMAX Disruptor 环形队列。Disruptor 的典型特征是无锁并发、预分配内存、事件对象在固定槽位里复用。这带来的直接好处是锁竞争没了GC 压力也小很多日志事件从业务线程到消费线程的传递路径变短。这才是 Log4j2 在官方基准测试里多个场景比 Logback 吞吐更高的根本原因并不是“某家日志库的开发者写代码更勤奋”。2.3 一组典型压测数据不同负载下的吞吐与延迟表现做性能对比一定要讲环境。我本地用 8 核 16G 的机器、SSD 磁盘、统一 JSON 单行格式分别跑过 Logback 和 Log4j2 的同步与异步模式数据供参考。方案普通同步吞吐异步吞吐说明Logback约 1.5 万条/秒约 3-4 万条/秒AsyncAppender 内部是有锁队列Log4j2约 2 万条/秒约 10-20 万条/秒AsyncLogger 使用 Disruptor 无锁队列但我要泼一盆冷水这种数字很容易被误读。绝大多数业务服务峰值每秒钟也就几百到几千条日志用 Logback 还是 Log4j2 根本看不出区别。真正会被性能数据影响的是网关、交易链路、埋点采集这类日志密集型服务。而且压测中吞吐量受日志格式影响极大JSON 格式比纯文本慢不少正则表达式过滤又会慢一截。所以网上任何“框架 A 快过框架 B 十倍”的结论都必须先确认双方是否用了完全相同的输出格式、滚动策略和硬件环境。2.4 异步日志的隐性代价队列满、丢日志与优雅停机异步也不是银弹它有三笔要额外付的成本。第一笔是队列满。异步本质是生产者和消费者解耦如果写入速度远大于消费速度队列一定会满。此时是丢弃日志还是让业务线程继续阻塞需要明确配置。第二笔是优雅停机。虚拟机或容器停机时队列里可能还有几万条没来得及写盘的日志如果不等待消费线程清空队列这些日志就直接丢了。第三笔是调用位置。异步模式下堆栈里的调用位置信息往往需要额外抓取很耗性能不少配置会默认关闭这又影响查日志时的精确定位。在实际项目中我建议生产环境把异步队列大小设到一个合理范围比如 8K 到 64K同时配置 shutdown 时等待时间。至于要不要丢日志取决于业务性质审计类日志不能丢业务 debug 日志可以适当丢弃。3. Spring Boot项目切换Log4j2的实操过程与排坑记录3.1 依赖层替换哪些包该排除、哪些包要引入切到 Log4j2 之前先确认项目里没有别的地方直接依赖logback-classic。最干净的做法是把内置的 logging starter 排除掉再加spring-boot-starter-log4j2。Maven 项目这样改dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependencyGradle 项目对应写法implementation(org.springframework.boot:spring-boot-starter-web) { exclude group: org.springframework.boot, module: spring-boot-starter-logging } implementation org.springframework.boot:spring-boot-starter-log4j2排除完以后一定要跑一遍mvn dependency:tree看看项目里还有没有历史遗留的 Logback 依赖。第三方 SDK 很可能会间接带进来如果不排除干净SLF4J 绑定实现可能冲突启动时会直接报错。这种坑在旧项目里最常见而且报错信息往往看得人一头雾水。3.2 log4j2-spring.xml的推荐配置模板配置文件命名建议用log4j2-spring.xml而不是log4j2.xml。原因很简单Spring Boot 能自动识别log4j2-spring.xml并把application.yml里的一些环境属性映射进去如果用普通文件名就要自己处理更多兼容问题。下面是一份能直接落地的模板控制台和滚动文件并存业务包走异步 Logger。?xml version1.0 encodingUTF-8? Configuration statusWARN monitorInterval30 Properties Property namelogPattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %thread %logger{36} [%X{traceId}] %msg%n/Property /Properties Appenders Console nameConsoleAppender targetSYSTEM_OUT PatternLayout pattern${logPattern} charsetUTF-8/ /Console RollingFile nameRollingFileAppender fileNamelogs/application.log filePatternlogs/application-%d{yyyy-MM-dd}-%i.log.gz PatternLayout pattern${logPattern} charsetUTF-8/ Policies TimeBasedTriggeringPolicy interval1 modulatetrue/ SizeBasedTriggeringPolicy size100 MB/ /Policies DefaultRolloverStrategy max15/ /RollingFile /Appenders Loggers AsyncLogger namecom.example levelinfo additivityfalse includeLocationfalse AppenderRef refConsoleAppender/ AppenderRef refRollingFileAppender/ /AsyncLogger Root levelinfo AppenderRef refConsoleAppender/ AppenderRef refRollingFileAppender/ /Root /Loggers /Configuration把includeLocation设为 false 是异步日志里的重点目的是避免每次抓取调用方堆栈信息。需要精确行号的时候再临时改成 true线上长时间开着这个开关性能会很吃亏。如果要实现全局异步可以把Root换成AsyncRoot或者在log4j2.component.properties里设置log4j2.contextSelectororg.apache.logging.log4j.core.async.AsyncLoggerContextSelector。同时记得确认 classpath 里有 Disruptor 依赖Spring Boot 的 starter 不保证一定帮你带齐版本显式声明最稳dependency groupIdcom.lmax/groupId artifactIddisruptor/artifactId version3.4.4/version /dependency3.3 如何验证切换是否成功切换完成后别急着开压测先做两步验证。第一步看启动日志Spring Boot 启动时如果有 Log4j2 相关标识说明 starter 生效了。第二步写一个临时接口往日志里打几条不同级别的消息观察文件和控制台输出是否符合log4j2-spring.xml里的格式。如果还是旧格式基本可以断定依赖排除没做干净。更严谨的验证方式是用jstack看线程名。全局异步开启后进程里应该能看到类似AsyncLogger的消费线程。没有消费线程说明日志还在同步写。3.4 迁移过程中常见的5个坑问题表现解决starter 排除不彻底启动报 SLF4J 绑定冲突或日志格式不变用 dependency:tree 找到 Logback 来源并 exclude配置文件命名不对log4j2.xml不生效Spring 用自己的默认配置改成log4j2-spring.xml放到 classpath 根目录异步功能不工作日志出现“无法找到异步实现”相关告警补上 disruptor 依赖中文乱码文件里中文变成问号PatternLayout 里显式指定 charsetUTF-8停机丢日志容器停止前最后一段日志没落盘配置 shutdown 等待时间让消费线程清空队列这几个坑我都踩过并且是在不同项目里反复遇到。尤其是依赖排除经常出现“web starter 排了半天结果有个工具包又带回了 logback”的情况。所以我现在养成了习惯凡是涉及日志框架切换第一件事永远是mvn dependency:tree第二件事才是改代码。4. 日志治理的工程细节格式、滚动、脱敏一个都不能少4.1 日志格式里真正有价值的字段很多日志配置把时间戳、线程名、级别、类名打全看着很规范但真正排查问题的时候常常不够用。最典型的表现是两个服务之间调用一条请求经过了三个模块报错时却完全没法把散落各处的日志串成一条链路。我在项目里强制要求的格式至少包含四类信息精确到毫秒的时间、线程名、日志级别、Logger 名然后就是 MDC 里的 traceId 和业务键。推荐 pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %thread %logger{36} [%X{traceId}] [%X{userId}] %msg%n%X{traceId}是 MDC 里取的键值。业务代码只需要在入口处设置一下try { MDC.put(traceId, traceId); // 业务逻辑 } finally { MDC.remove(traceId); }这个做法与具体日志框架无关不管是 Logback 还是 Log4j2打印时都能读到同一个 MDC 值。如果团队已经接了 SkyWalking、OpenTelemetry 这类链路追踪组件traceId 通常会被自动写进 MDC日志配置里只要预留%X相关字段即可。4.2 滚动策略与磁盘保护机制日志滚动配置常见的问题是“只配时间不配大小”。比如按天滚动结果某天业务量大单日日志写满 50G磁盘直接告警。反过来只按大小滚动运维想按天归档审计又很麻烦。比较稳的组合是“时间 大小”双条件触发再用max控制保留份数。上面的 Log4j2 模板里TimeBasedTriggeringPolicy按天切分SizeBasedTriggeringPolicy单文件到 100MB 也强制切分DefaultRolloverStrategy max15只保留最近 15 份。这样最坏情况下磁盘占用量是可控的。还有一个容器环境特有的问题。服务跑在 Kubernetes 这类平台时日志可能同时被 stdout、文件采集和平台日志系统盯上配置不当会重复采集或者写爆空灵。我的经验是容器场景优先把结构化日志打到 stdout由平台统一采集保留一份文件存储不要又写 stdout 又到处写日志文件。4.3 敏感信息脱敏日志安全的底线措施日志里最容易翻车的是隐私数据。手机号、身份证、银行卡、Token 这些东西一旦进了日志不是“我们内部看看没关系”的问题而是能不能过合规审计的问题。实现层面不要把每个字段都丢进正则表达式做脱敏。日志量大时每条日志都跑一遍敏感字段正则CPU 成本会明显上升。更务实的做法是两层配合业务侧在写日志前就把敏感值处理掉比如手机号只保留前 3 后 4 位Token 只打最后 4 位底层框架再做兜底拦截过滤掉明显包含敏感字段的错误日志。白名单字段思路比黑名单正则可靠得多——允许业务代码打印某些已脱敏字段比试图拦截所有意外泄露要容易落地。5. 我的选型逻辑不要只看吞吐量还要看团队和业务阶段5.1 Logback与Log4j2的选型对照表这部分直接给结论汇总。对比维度LogbackLog4j2Spring Boot 默认集成是零额外配置否需要排除默认 starter异步模型AsyncAppender 有锁 BlockingQueueAsyncLogger Disruptor 无锁环形队列高并发峰值吞吐中等更高配置掌握难度相对直观资料多插件式配置参数概念更多依赖隔离成本低需要排除依赖检查冲突动态调整日志级别支持支持常见典型坑异步队列丢日志、滚动策略容易漏配依赖版本冲突、异步参数不易理解这两种框架在功能上其实高度重叠真正的分歧集中在“无锁异步”和“生态熟悉度”上。选 Log4j2 不等于自动获得高性能得配合正确的异步配置选 Logback 也不等于性能一定差毕竟多数服务的日志量根本没到瓶颈。5.2 不同场景下的倾向性结论我自己的判断很直接。内部管理系统、低并发后台、日志量一天不超过几百 MB这类项目无脑保留 Logback操作简单、团队上手快、踩坑少。网关、交易中台、埋点采集这类日志密集型服务建议认真考虑 Log4j2。这些服务的特点是峰值流量瞬间爆发业务线程卡在日志锁上的代价很大Disruptor 无锁队列的优势这时候才真正体现。如果是日志量中等但排障体验要求极高的项目我反而会先把重点放在格式治理和链路追踪上框架选谁反而不是第一优先级。毕竟一个没有 traceId 的日志系统就算吞吐再高出了问题也照样难查。如果团队刚刚接触 Spring Boot不建议上来就整 AsyncRoot Disruptor 的复杂配置大概率只换来一堆看不懂的堆栈和队列告警。先把 Logback 的滚动、格式、MDC 配好后面真有性能压力再迁移成本一两天。5.3 选型和迁移真正值得关注的成本Logback 切 Log4j2 的代码成本很低业务代码因为只依赖 SLF4J基本不用动。真正要付出的成本是配置语法的学习和历史坑的清理。Log4j2 的插件化思路和 Logback 的 Appender Layout 思路不完全一样老手也需要一两天时间适应。所以选型不是选一个“永远正确”的答案而是选一个“当前团队能维护住”的方案。如果团队里没人真正跑过 Log4j2 的异步配置我宁愿先在 Logback 上把治理细节做扎实也不要推倒重来。最后再分享一个小细节。切到 Log4j2 之后把includeLocationfalse这个参数检查一遍默认状态是关闭的还好一旦被某个人改成 true 并上线异步日志的性能优势会明显打折。下次你在压测的时候观察到吞吐量上不去先别怀疑框架去日志配置里看看是不是这种细节又在偷偷拖后腿。
返回列表