log4j2最佳实践3
高效使用 Log4j API
Log4j API 捆绑了一组丰富的功能,以尽可能完全避免或最小化昂贵的计算。我们将通过示例引导您了解这些功能。
请记住,日志记录 API 和日志记录实现是两个不同的东西。 您可以将 Log4j API 与 Log4j Core 以外的日志记录实现(例如 Logback)结合使用。 本节中共享的技巧与日志记录实现无关。 |
不要使用字符串连接
如果您在记录时使用String
连接,那么您正在做一些非常错误和危险的事情!
-
不要使用
String
连接来格式化参数!这避免了按消息类型和布局处理参数。更重要的是,这种方式很容易受到攻击! 想象一下用户提供的userId
包含以下内容:placeholders for non-existing args to trigger failure: {} {} {dangerousLookup}
/* BAD! */ LOGGER.info("failed for user ID: " + userId);
-
使用消息参数。参数化消息允许对参数进行安全编码,并在消息被过滤时完全避免格式化。例如,如果丢弃记录器的关联级别,则不会进行任何格式化。
/* GOOD */ LOGGER.info("failed for user ID `{}`", userId);
使用Supplier
传递计算成本较高的参数
如果日志语句的一个或多个参数在计算上是昂贵的,那么在知道它们的结果可以被丢弃的情况下评估它们是不明智的。考虑以下示例:
/* BAD! */ LOGGER.info("failed for user ID `{}` and role `{}`", userId, db.findUserRoleById(userId));
如果创建的日志事件无论如何都会被丢弃,那么数据库查询(即db.findUserNameById(userId)
)可能会成为一个重要的瓶颈 - 也许该包不接受INFO
级别或关联的标记,或者由于某些其他过滤。
-
解决这个问题的传统方法是对日志语句进行级别保护:
/* BAD! */ if (LOGGER.isInfoEnabled()) { LOGGER.info(...); }
虽然这适用于由于级别不足而可能丢弃消息的情况,但此方法仍然容易出现其他过滤情况;例如,关联的标记可能不被接受。
-
使用
Supplier
传递包含计算上昂贵的项目的参数:/* GOOD */ LOGGER.info("failed for user ID `{}` and role `{}`", () -> userId, () -> db.findUserRoleById(userId));
-
使用
Supplier
传递包含计算上昂贵的项目的消息及其参数:/* GOOD */ LOGGER.info(() -> new ParameterizedMessage("failed for user ID `{}` and role `{}`", userId, db.findUserRoleById(userId)));
调整 Log4j 核心的性能
以下部分将引导您了解一组可能对 Log4j Core 的性能产生重大影响的功能。
对任何应用程序进行额外调整都会使您偏离默认值并增加维护负担。 强烈建议您测量应用程序的整体性能,然后,如果发现 Log4j 是重要的瓶颈因素,请仔细调整它。 |
请记住,日志记录 API 和日志记录实现是两个不同的东西。 您可以将Log4j Core 与Log4j API 以外的日志记录API(例如SLF4J、JUL、JPL)结合使用。 本节中分享的技巧与日志记录 API 无关。 |
布局
布局负责以某种格式(人类可读的文本、JSON 等)对日志事件进行编码,它们会对您的整体日志记录性能产生重大影响。
位置信息
一些布局提供了包含位置信息的指令:调用者类、方法、文件和行。 Log4j 拍摄堆栈快照,并遍历堆栈跟踪以查找位置信息。 这是一项昂贵的操作,在性能敏感的设置中应该避免。
请注意,位置信息的调用者类和记录器名称是两个不同的东西。在大多数设置中,仅使用记录器名称- 在记录时不会产生任何开销! – 是调用者类的充分且零成本的替代品。 |
在上面的示例中,如果在布局中省略调用者类(计算成本很高!),则生成的日志行仍然可能包含足够的信息,只需查看记录器名称即可追溯源。 |
异步记录器在将日志消息传递给另一个线程之前需要捕获位置信息;否则该点之后位置信息将丢失。由于相关的性能影响,异步记录器和异步附加器默认情况下不包含位置信息。您可以覆盖异步记录器或异步附加程序配置中的默认行为。
异步日志记录
异步日志记录对于处理突发事件非常有用。
其工作原理是,应用程序线程完成最少量的工作来捕获日志事件中的所有所需信息,然后将该日志事件放入队列中,供后台线程稍后处理。
只要队列足够大,应用程序线程就应该能够在日志记录调用上花费很少的时间并很快返回到业务逻辑。
权衡
异步日志记录存在某些权衡:
好处
- 更高的峰值吞吐量
-
偶尔需要记录突发消息的应用程序可以利用异步日志记录。 它可以通过缩短记录下一条消息之前的等待时间来防止或抑制延迟峰值。
如果队列大小足以处理突发,异步日志记录将防止您的应用程序在活动突然增加期间落后。 - 更低的日志记录延迟
-
Logger
方法调用返回速度更快,因为大部分工作是在 I/O 线程上完成的。
缺点
- 可持续吞吐量较低
-
如果应用程序记录消息的持续速率快于底层追加器的最大持续吞吐量,则队列将填满,并且应用程序最终将以最慢的追加器的速度进行日志记录。
如果发生这种情况,请考虑选择更快的附加程序,或减少日志记录。 如果这两种方法都不可行,您可以通过同步日志记录获得更好的吞吐量和更少的延迟峰值。 - 错误处理
-
如果在日志记录过程中发生问题并引发异常,则异步设置不太容易向应用程序发出此问题的信号。通过配置异常处理程序可以部分缓解这个问题,但这可能仍然无法涵盖所有情况。
如果日志记录是您业务逻辑的一部分,例如您使用 Log4j 作为审核日志记录框架,我们建议同步记录这些审核消息。
看 混合同步/异步记录器如何同步记录一些消息。
异步日志记录策略
Log4j 提供了以下策略供用户选择来进行异步日志记录:
异步记录器
异步记录器使用LMAX Disruptor消息传递库来消费日志事件。他们的目标是尽快从log()
调用返回到应用程序。
异步追加器
异步附加程序接受对其他附加程序的引用,并导致日志事件在单独的线程上写入它们。默认情况下,后备队列使用ArrayBlockingQueue
,但可以将其替换为适合您的用例的性能更好的队列。
无垃圾日志记录
垃圾收集暂停是延迟峰值的常见原因,对于许多系统来说,控制这些暂停需要花费大量精力。 Log4j 在稳态日志记录期间分配临时LogEvent
、 String
、 char[]
、 byte[]
等对象。这会增加垃圾收集器的压力,并增加垃圾收集暂停发生的频率。在无垃圾模式下,Log4j 缓冲并重用对象来减轻这种压力。
有关详细信息,请参阅无垃圾日志记录。
异步记录器
异步日志记录是一种通过在单独的线程中执行所有 I/O 操作来提高应用程序日志记录性能的技术。
Log4j 提供了两种开箱即用的异步日志记录解决方案:
- 异步追加器
-
经典的基于队列的异步附加器,自 Log4j 1 起可用。
有关更多详细信息,请参阅异步附加程序。
- 异步记录器
-
异步记录器是 Log4j 2 以来提供的新功能。 它们基于 LMAX Disruptor是一个无锁线程间通信库,代替队列,从而获得更高的吞吐量和更低的延迟。
本章的其余部分专门讨论这个新组件。
日志记录性能在很大程度上取决于应用程序的体系结构和使用日志记录的方式。 应使用针对您自己的应用程序的基准来评估本章提供的解决方案。 |
权衡
异步日志记录存在某些权衡:
好处
- 更高的峰值吞吐量
-
偶尔需要记录突发消息的应用程序可以利用异步日志记录。 它可以通过缩短记录下一条消息之前的等待时间来防止或抑制延迟峰值。
如果队列大小足以处理突发,异步日志记录将防止您的应用程序在活动突然增加期间落后。 - 更低的日志记录延迟
-
Logger
方法调用返回速度更快,因为大部分工作是在 I/O 线程上完成的。
缺点
- 可持续吞吐量较低
-
如果应用程序记录消息的持续速率快于底层追加器的最大持续吞吐量,则队列将填满,并且应用程序最终将以最慢的追加器的速度进行日志记录。
如果发生这种情况,请考虑选择更快的附加程序,或减少日志记录。 如果这两种方法都不可行,您可以通过同步日志记录获得更好的吞吐量和更少的延迟峰值。 - 错误处理
-
如果在日志记录过程中发生问题并引发异常,则异步设置不太容易向应用程序发出此问题的信号。通过配置异常处理程序可以部分缓解这个问题,但这可能仍然无法涵盖所有情况。
如果日志记录是您业务逻辑的一部分,例如您使用 Log4j 作为审核日志记录框架,我们建议同步记录这些审核消息。
看 混合同步/异步记录器如何同步记录一些消息。
- 有状态消息
-
最多
Message
实现在调用线程上拍摄格式化消息的快照(参见log4j2.formatMsgAsync
)。即使稍后修改日志记录调用的参数,日志消息也不会更改。此规则有一些例外。
MapMessage
和StructuredDataMessage
例如,设计上是可变的:可以在创建消息对象后将字段添加到这些消息中。 这些消息在使用异步记录器或异步附加程序记录后不应进行修改。同样,定制
Message
实现的设计应考虑到异步使用,并且要么在构造时获取其参数的快照,要么记录其线程安全特性(请参阅AsynchronouslyFormattable
)。 - 计算开销
-
如果您的应用程序在 CPU 资源稀缺的环境中运行(例如具有单个 vCPU 的虚拟机),则启动另一个线程不太可能提供更好的性能。
安装
为了使用异步记录器,您需要将 LMAX Disruptor 添加到应用程序的依赖项中,方法是将以下依赖项添加到构建工具中:
runtimeOnly 'com.lmax:disruptor:4.0.0'
配置
在 Log4j 中可以通过两种方式使用异步记录器。你可以:
-
使所有记录器异步,从而提供更好的性能,
-
混合同步和异步记录器,这提供了更大的灵活性。
这些方法在底层使用不同的 Log4j 插件,但也共享一个 一组通用配置属性。
使所有记录器异步
这是最简单的配置并提供最好的性能:要使所有记录器异步,您只需设置 log4j2.contextSelector
异步记录器上下文选择器之一的属性:
org.apache.logging.log4j.core.async.BasicAsyncLoggerContextSelector
-
这将为 JVM 中的所有类创建一个记录器上下文和干扰器,
org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
-
这将为 JVM 中的每个类加载器创建不同的记录器上下文和干扰器。
当使用异步记录器上下文时,您应该仅使用 如果您使用
|
调整完全异步配置
由于 Disruptor 与记录器上下文同时初始化并且在加载任何 Log4j 配置文件之前,因此只能通过配置属性来调整异步记录器。
除了常见的配置属性之外,还可以配置以下附加元素:
log4j2.asyncLoggerExceptionHandler
log4j2.asyncLoggerWaitStrategy
环境。多变的 |
|
---|---|
类型 |
预定义常数 |
默认值 |
|
指定 LMAX Disruptor 使用的WaitStrategy
。
该值必须是预定义常量之一:
- 堵塞
-
对等待日志事件的 I/O 线程使用锁和条件变量的策略。当吞吐量和低延迟不像 CPU 资源那么重要时,可以使用块。建议用于资源受限/虚拟化环境。这种等待策略并不是没有垃圾的。
- 暂停
-
Block
策略的一种变体,它将定期从锁定条件await()
调用中唤醒。这确保了如果以某种方式错过通知,消费者线程不会被卡住,而是会以较小的延迟延迟恢复,请参阅log4j2.asyncLoggerTimeout
。这种等待策略是无垃圾的。 - 睡觉
-
一种策略,首先旋转,然后使用
Thread.yield()
,并最终在 I/O 线程等待日志事件时停放操作系统和 JVM 允许的最小纳秒数(请参阅log4j2.asyncLoggerRetries
和log4j2.asyncLoggerSleepTimeNs
)。睡眠是性能和 CPU 资源之间的一个很好的折衷方案。此策略对应用程序线程的影响非常小,但会在实际记录消息时产生一些额外的延迟。这种等待策略是无垃圾的。 - 屈服
-
是一种将使用
100%
CPU 的策略,但如果其他线程需要 CPU 资源,则会放弃 CPU。这种等待策略是无垃圾的。
混合同步和异步记录器
同步和异步记录器可以组合在一个配置中。这为您提供了更大的灵活性,但代价是性能略有下降(与使所有记录器异步相比)。
为了使用此配置,您需要保留 log4j2.contextSelector
为其默认值并使用其中之一 AsyncRoot
和AsyncLogger
配置元素用于指定您想要异步的记录器。
混合异步记录器的配置可能如下所示:
-
XML
-
JSON
-
YAML
-
特性
log4j2.xml
的片段<Loggers>
<Root level="INFO">
<AppenderRef ref="AUDIT">
<MarkerFilter marker="AUDIT" onMatch="ACCEPT" onMismatch="DENY"/>
</AppenderRef>
</Root>
<AsyncLogger name="com.example" level="TRACE">
<AppenderRef ref="DEBUG_LOG"/>
</AsyncLogger>
</Loggers>
Root 和Logger 引用的所有appender 都是同步调用的。这对于审计日志记录尤其重要,因为异常可以转发给调用者。 |
|
AsyncRoot 和AsyncLogger 引用的所有追加器都是异步调用的。这些日志语句将为调用者带来较小的延迟。 |
调整混合同步/异步配置
由于所有AsyncRoot
和AsyncLogger
组件共享同一个 Disruptor 实例,因此可以通过配置属性进行配置。
除了常见的配置属性之外,还可以配置以下附加元素:
log4j2.asyncLoggerConfigExceptionHandler
对任何应用程序进行额外调整都会使您偏离默认值并增加维护负担。 强烈建议您测量应用程序的整体性能,然后,如果发现 Log4j 是重要的瓶颈因素,请仔细调整它。 |
快速启动
如果您想要一个无垃圾的 Log4j 设置,但不想花时间处理相关细节,您可以按照以下说明快速开始:
-
将以下系统属性设置为
true
: -
使用无垃圾
这对于大多数用例来说应该足够了。如果不适合您,请继续阅读。
Log4j API 使用
Log4j API包含多个有助于无垃圾日志记录的功能:
参数化消息参数
Logger
接口包含最多 10 个参数的参数化消息的方法。记录超过 10 个参数会创建vararg 数组。
编码自定义对象
当消息参数包含布局未知的类型时,它将通过对这些对象调用toString()
进行编码。大多数对象没有无垃圾的toString()
方法。对象本身可以通过扩展Java 的CharSequence
或Log4j 的StringBuilderFormattable
来实现自己的无垃圾编码器。
避免自动装箱
我们努力在不需要代码的情况下使日志记录无垃圾 现有应用程序的变化,但有一个领域是这样的 不可能。
记录原始值(即 int、double、boolean、 等等)JVM 自动将这些原始值装箱到它们的对象包装器中 等价物,制造垃圾。
Log4j 提供了Unbox
实用程序来防止原语自动装箱 参数。该实用程序包含一个重用的线程本地池 StringBuilder`s. The `Unbox.box(primitive)
方法直接写入 StringBuilder,结果文本将被复制到最终的日志消息文本中,而无需创建临时对象。
import static org.apache.logging.log4j.util.Unbox.box;
LOGGER.debug("Prevent primitive autoboxing {} {}", box(10L), box(2.6d));
该实用程序包含一个重用StringBuilder
的ThreadLocal
池。池大小由log4j2.unboxRingbufferSize
系统属性配置。 Unbox.box(primitive)
方法直接写入StringBuilder
,结果文本将被复制到最终的日志消息文本中,而不创建临时对象。
局限性
并非所有 Log4j API 功能集都是无垃圾的,具体来说:
-
ThreadContext
映射(又名 MDC)默认情况下不是无垃圾的,但可以通过设置log4j2.garbagefreeThreadContextMap
系统属性为true
。 -
ThreadContext
堆栈(又名 NDC)不是无垃圾的。 -
记录非常大的消息(即超过
log4j2.maxReusableMsgSize
个字符,默认为 518),当所有记录器都是异步记录器时,将导致内部StringBuilder
RingBuffer
将被修剪回其配置的最大大小。 -
包含
${variable}
替换的日志消息会创建临时对象。 -
将 lambda 记录为参数:
LOGGER.info("lambda value is {}", () -> callExpensiveMethod());
创建一个可变参数数组。单独记录 lambda 表达式:
LOGGER.debug(() -> callExpensiveMethod());
没有垃圾。
-
traceEntry()
和traceExit()
方法创建临时对象。 -
当
log4j2.usePreciseClock
系统属性(默认为false
)设置为true
时,时间计算不是无垃圾的。
迁移
如果您的应用程序或库使用 SLF4J 进行日志记录,您可以将其迁移到 Log4j API,如下所示:
-
删除
org.slf4j:slf4j-api
依赖 -
按照“入门”页面中共享的说明安装
log4j-api
并使用它 -
在您的项目中搜索
org.slf4j
用法并将其替换为 Log4j API 等效项有关需要执行的代码更改的详尽列表,请参阅SLF4J 到 Log4j API 迁移 OpenRewrite 配方。你可以用这个
-
手动遵循所描述的迁移,
-
或者运行 OpenRewrite 自动迁移代码。
org.slf4j.LoggerFactory
-
将其用法替换为
org.apache.logging.log4j.LogManager
。注意LogManager.getLogger(Foo.class)
如果Foo
是字段的封闭类,则可以简化为LogManager.getLogger()
。 org.slf4j.Logger
-
将其用法替换为
org.apache.logging.log4j.Logger
。由于 SLF4J 的Logger
几乎是 Log4j 的Logger
的父级,因此大多数方法应该无需任何更改即可工作。 org.slf4j.MDC
-
将其用法替换为
org.apache.logging.log4j.ThreadContext
。
-
-
如果您使用Lombok 中的
@Slf4j
,则需要将其替换为@Log4j2
。
春季查找
Spring Lookup 允许配置文件从 Log4j 配置文件引用 Spring 配置文件中定义的属性。例如:
<property name="applicationName">${spring:spring.application.name}</property>
会将 Log4j applicationName 属性设置为 Spring 配置中设置的 spring.application.name 的值。
Spring 属性源
Log4j 在解析其内部使用的属性时使用属性源。 此支持允许大多数 Log4j 配置属性 在 Spring 配置中指定。 但是,仅在第一次 Log4j 初始化期间引用的某些属性(例如 Log4j 用于允许选择默认 Log4j 实现的属性)将不可用。
弹簧轮廓仲裁器
Log4j 2.15.0 的新增功能是“仲裁器”,它们是可以导致包含或排除 Log4j 配置的一部分的条件。 log4j-spring-boot 提供了一个仲裁器,允许将 Spring 配置文件值用于此目的。下面是一个例子:
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
<Appenders>
<SpringProfile name="dev | staging">
<Console name="Out">
<PatternLayout pattern="%m%n"/>
</Console>
</SpringProfile>
<SpringProfile name="prod">
<List name="Out">
</List>
</SpringProfile>
</Appenders>
<Loggers>
<Logger name="org.apache.test" level="trace" additivity="false">
<AppenderRef ref="Out"/>
</Logger>
<Root level="error">
<AppenderRef ref="Out"/>
</Root>
</Loggers>
</Configuration>
要求
Log4j 2 Spring Cloud 配置集成依赖于 Log4j 2 API、Log4j 2 Core 和 Spring Boot 版本 2.0.3.RELEASE 或 2.1.1.RELEASE 或发布系列的更高版本。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南