在 Log4j2 中,若不考虑 Rolling(支持滚动和压缩)类文件 Appender,则包含以下三种文件 Appender:FileAppender、RandomAccessFileAppender 和 MemoryMappedFileAppender。接下来将介绍这三种 Appender 的功能、优缺点以及在实际研发中应如何选择与使用。

一、三种 Appender 简介

1. FileAppender

FileAppender 是 Log4j2 中最基础的文件 Appender,它基于传统的 java.io.FileOutputStream 将日志写入文件,简单可靠。

2. RandomAccessFileAppender

RandomAccessFileAppender 基于 ByteBuffer 和 RandomAccessFile 实现,与标准的 FileAppender 类似,不过它始终使用由 bufferSize 参数指定大小的内存缓冲区。从官方文件介绍来看,与使用默认配置的 FileAppender 相比,性能可提高 20% 至 200%。然而,由于使用了内存缓冲区,增加了内存开销,同时也提高了日志丢失的风险,若程序崩溃,缓冲区中的日志将会丢失。

3. MemoryMappedFileAppender

MemoryMappedFileAppender 基于内存映射文件(Memory-Mapped File)实现,文件直接映射到内存中,将日志直接写入这块内存,并依赖操作系统的虚拟内存管理器持久化到存储设备,使得能够在不经过操作系统缓存的情况下直接将日志写入磁盘,减少了 I/O 开销,进而性能有明显提升。然而,该 Appender 内存占用较多,且依赖于底层操作系统和硬件支持,若程序崩溃,未持久化的日志可能丢失。

默认内存映射区域大小为 32M,可通过“regionLength”参数进行调整;该 Appender 无对应的 Rolling 类,无法支持日志滚动切换和备份。

二、功能对比

特性 FileAppender RandomAccessFileAppender MemoryMappedFileAppender
底层实现 java.io.FileOutputStream java.io.RandomAccessFile 内存映射文件(Memory-Mapped File)
可靠性 较高(缓冲区未刷新时) 略低(内存映射未刷新时)
性能 略低 略高
平台兼容性 略低,依赖操作系统和硬件支持
内存占用 较低 中等 较高
无 GC(Garbage-free)模式 支持 支持 支持
文件 Rolling 支持 支持 不支持
日志输出顺序性 有序 有序 有序

三、同步性能对比

1. 基准环境

(1)硬件:Windows 笔记本,配置为 I5-1350P CPU、32G DDR5 5200 内存以及三星 MZVL4512HBLU-00BLL 512G SSD(顺序写入速度为 2430MB/s)。

(2)软件:基于 JDK 1.8.171,使用 1.37 版 JMH 和 2.24.3 版 Log4j2。

(3)配置:三种 Appender 均采用默认配置(append 和 immediateFlush 都为 true),使用同步 Logger 进行压测。

(4)参考实际使用情况,输出长度为 100 个的固定字符串,同时,有 20% 的概率输出包含 100 层堆栈的异常,日志布局为:%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %pid %t %C.%M:%L - %msg %ex{full} %n

(5)压测三次,取结果平均值。

2. 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="log4j2AppenderTest" status="error">
<Properties>
<Property name="log.pattern">
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %pid %t %C.%M:%L - %msg %ex{full} %n
</Property>
</Properties> <Appenders>
<Console name="Console">
<PatternLayout pattern="${log.pattern}"/>
</Console>
<File name="File"
fileName="target/test-output/log4j2-file.log">
<PatternLayout pattern="${log.pattern}"/>
</File>
<RandomAccessFile name="RandomAccessFile"
fileName="target/test-output/log4j2-random-access-file.log" >
<PatternLayout pattern="${log.pattern}"/>
</RandomAccessFile>
<MemoryMappedFile name="MemoryMappedFile"
fileName="target/test-output/log4j2-memory-mapped-file.log" >
<PatternLayout pattern="${log.pattern}"/>
</MemoryMappedFile>
</Appenders> <Loggers>
<Root level="debug">
<AppenderRef ref="Console" />
</Root>
<Logger name="FileLogger" level="debug" additivity="false">
<AppenderRef ref="File" />
</Logger>
<Logger name="RandomAccessFileLogger" level="debug" additivity="false">
<AppenderRef ref="RandomAccessFile" />
</Logger>
<Logger name="MemoryMappedFileLogger" level="debug" additivity="false">
<AppenderRef ref="MemoryMappedFile" />
</Logger>
</Loggers>
</Configuration>

3. 压测代码

@State(Scope.Benchmark)
public class Log4J2FileAppenderBenchmark { static Logger fileLogger;
static Logger randomAccessLogger;
static Logger memoryMappedLogger;
static final Random RANDOM= new Random();
static final double OUTPUT_EXCEPTION_PROBABILITY = 0.2; @Setup(Level.Trial)
public void setUp() throws Exception {
System.setProperty("log4j.configurationFile", "log4j2-appender.xml");
fileLogger = LogManager.getLogger("FileLogger");
randomAccessLogger = LogManager.getLogger("RandomAccessFileLogger");
memoryMappedLogger = LogManager.getLogger("MemoryMappedFileLogger");
} @TearDown
public void tearDown() {
System.clearProperty("log4j.configurationFile");
} public void outputLog(Logger logger){
if (RANDOM.nextDouble() < OUTPUT_EXCEPTION_PROBABILITY) {
logger.debug(Const.MSG_HAVE_100_CHARS, Const.THROWABLE_HAVE_100_STACK);
} else {
logger.debug(Const.MSG_HAVE_100_CHARS);
}
} @BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Benchmark
public void syncFileLogger() {
outputLog(fileLogger);
} @BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Benchmark
public void syncRandomAccessLogger() {
outputLog(randomAccessLogger);
} @BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Benchmark
public void syncMemoryMappedAppendLogger() {
outputLog(memoryMappedLogger);
}
}

4. JMH 参数

JMH 执行的参数为:-jvmArgs "-Xmx2g -Xms2g" -f 2 -t 4 -w 10 -wi 2 -r 30 -i 2 -to 300 -prof gc -rf json,即设置 JVM 参数为 -Xmx2g -Xms2g(堆内存最大和最小均为 2GB),使用 2 个 fork(-f 2),每个 fork 使用 4 个线程(-t 4),预热阶段每次运行 10 秒(-w 10),预热迭代 2 次(-wi 2),正式测试每次运行 30 秒(-r 30),正式测试迭代 2 次(-i 2),超时时间为 300 秒(-to 300),启用 GC 性能分析(-prof gc),并将测试结果输出为 JSON 格式(-rf json)。

5. 压测结果

Appender 类型 平均吞吐量 内存分配速率(MB/sec) 垃圾回收次数
FileAppender 42.5 ops/ms 1328.1 MB/sec 235
RandomAccessFileAppender 46.6 ops/ms 1498.4 MB/sec 266
MemoryMappedFileAppender 90.9 ops/ms 3162.5 MB/sec 561

6. 压测结论

基于上述基准环境及压测结果,可得出结论:

(1)MemoryMappedFileAppender 性能比 RandomAccessFileAppender 和 FileAppender 高约一倍。

(2)RandomAccessFileAppender 性能稍优于 FileAppender,吞吐量高 8.8%。

此外,从其余 CASE 的压测来看:

(1)日志字段越少、单条日志体积越小,MemoryMappedFileAppender 性能优势越明显,最多可高于 FileAppender 10 倍。

(2)在相同基准环境下,增加压测线程数,三类 Appender 性能均有所提升,RandomAccessFileAppender 和 MemoryMappedFileAppender 提升更为显著。将压测线程增加到 64 时,FileAppender、RandomAccessFileAppender 和 MemoryMappedFileAppender 吞吐量分别为 46.6、56.3 和 138.4 ops/ms,FileAppender 略有提升,相比之下,RandomAccessFileAppender 和 MemoryMappedFileAppender 领先 FileAppender 的优势分别扩大 20.8% 和 197%。

(3)使用 JDK21 对 MemoryMappedFileAppender 进行压测时,会抛出 IllegalAccessException 异常。原因是:自 Java9 起引入了模块系统(JPMS),限制了外部模块访问内部 API 的能力。解决方法:在运行时为 JVM 添加参数--add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/jdk.internal.ref=ALL-UNNAMED

(4)在相同基准环境下,发现使用 JDK21 压测,三种 Appender 的性能均有较大幅度下降,原因不明,已向社区反馈

五、使用建议

1. 如何选择

(1)常规情况下,推荐优先使用 FileAppender。其性能良好,每毫秒可达 42.5ops(实际生产环境中部署服务器大部分使用 HD 硬盘,其 IO 性能远不如 SSD,因此吞吐量可能会下降),能满足绝大部分业务需求,且简单可靠,支持 Rolling(文件切分、滚动和备份);RandomAccessFileAppender 性能仅比 FileAppender 略高,但存在丢失日志的风险,不太推荐使用;实际使用中,如果采用 FileAppender 或 RandomAccessFileAppender,建议使用其 Rolling 类,即 RollingFileAppender 和 RollingRandomAccessFileAppender,以满足文件切分、轮转和压缩等必备需求。

(2)若对性能要求较高,且能接受可能发生的较多日志丢失,与其考虑采用 MemoryMappedFileAppender,不如考虑使用异步日志(AsyncLogger)加消息队列 Appender 的组合方式,后者性能更好,且内存占用小,与系统平台耦合低。

(3)从压测结果来看,不同配置参数和使用环境,性能会存在一定波动。【特别建议】:根据业务实际使用场景、功能需求以及日志配置等情况,提前做好性能测试与分析,以便选择合适的 Appender。

2. 配置建议

实际生产环境中,【禁止】将 append 和 immediateFlush 参数设置为 false。因为前者会使原日志被覆盖,后者可能导致无法查看实时日志以及日志丢失。

六、参考文章

(1)log4j2.x

(2)log4j2 2.12.x

Log4j2 中三种常见 File 类 Appender 对比与选择的更多相关文章

  1. Android应用开发中三种常见的图片压缩方法

    Android应用开发中三种常见的图片压缩方法,分别是:质量压缩法.比例压缩法(根据路径获取图片并压缩)和比例压缩法(根据Bitmap图片压缩). 一.质量压缩法 private Bitmap com ...

  2. 详解C#中System.IO.File类和System.IO.FileInfo类的用法

    System.IO.File类和System.IO.FileInfo类主要提供有关文件的各种操作,在使用时需要引用System.IO命名空间.下面通过程序实例来介绍其主要属性和方法. (1) 文件打开 ...

  3. java中三种常见内存溢出错误的处理方法

    更多 10   相信有一定java开发经验的人或多或少都会遇到OutOfMemoryError的问题,这个问题曾困扰了我很长时间,随着解决各类问题经验的积累以及对问题根源的探索,终于有了一个比较深入的 ...

  4. java中三种常见内存溢出错误的处理方法(good)

    相信有一定java开发经验的人或多或少都会遇到OutOfMemoryError的问题,这个问题曾困扰了我很长时间,随着解决各类问题经验的积累以及对问题根源的探索,终于有了一个比较深入的认识. 在解决j ...

  5. Java中三种常见的注释(注解) Annotation

    Java为我们提供了三种Annotation方便我们开发. 1 Override-函数覆写注解 如果我们想覆写Object的toString()方法,请看下面的代码: class Annotation ...

  6. Java中如何利用File类递归的遍历指定目录中的所有文件和文件夹

    package cuiyuee; import java.io.File; import java.util.ArrayList; import java.util.List; public clas ...

  7. File类基础

    File类的作用: Java的io包中定义了File类,用于对文件或文件夹的管理操作. File类只能够用于表示文件或文件夹的信息(属性)和对该文件或文件夹的删除创建操作 (不能对内容进行访问) 通过 ...

  8. 第二十天File类、字节流

    File类.字节流 File类 File类介绍 File:它是描述持久设备上的文件或文件夹的.只要是在Java程序中操作文件或文件夹肯定需要使用File类完成. File类构造方法 /* * File ...

  9. java学习笔记IO之File类

    File类总结 p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Times } p.p2 { margin: 0.0px 0.0px 0.0p ...

  10. 2017.12.20 Java中的 IO/XML学习总结 File类详细

    IO / XML 一.File类 1.定义/概念 Java是面向对象的语言,要想把数据存到文件中,就必须要有一个对象表示这个文件.File类的作用就是代表一个特定的文件或目录,并提供了若干方法对这些文 ...

随机推荐

  1. AT_kupc2019_g ABCのG問題题解

    这题的难度不怎么好说,不过我认为还是挺简单的. 我们可以把答案看成由多个子图构成的图,这样我们只需要手打一个小子图,从中推出完整的答案. - 把小于子图范围的地方填上子图的字母 - 如果这个点的横坐标 ...

  2. M1芯片pod问题

    M1芯片pod问题 换了M1芯片的mac后,在Xcode跑项目报pod错误,提示run pod install更新pod,但是去终端跑命令时又报错 然后在github上看到一个老哥的方法 https: ...

  3. 给我2分钟,保证教会你在Vue3中实现一个定高的虚拟列表

    前言 虚拟列表对于大部分一线开发同学来说是一点都不陌生的东西了,有的同学是直接使用第三方组件.但是面试时如果你简历上面写了虚拟列表,却给面试官说是通过三方组件实现的,此时空气可能都凝固了.所以这篇文章 ...

  4. PG 实现 Dynamic SQL

    CREATE OR REPLACE FUNCTION public.exec( text) RETURNS SETOF RECORD LANGUAGE 'plpgsql' AS $BODY$ BEGI ...

  5. 查看MySQL数据库所有的表名、表注释、字段名称、类型、长度、备注,一键导出生成数据库字典

    一.先了解下INFORMATION_SCHEMA1.在MySQL中,把INFORMATION_SCHEMA看作是一个数据库,确切说是信息数据库.其中保存着关于MySQL服务器所维护的所有其他数据库的信 ...

  6. Qt编写地图综合应用23-标注点交互

    一.前言 地图项目应用中,标注点的交互使用频率非常高,这应该是最常用的场景,比如从数据库中读取出来设备的信息包括经纬度坐标,然后需要在地图上显示对应的设备,这就需要用addMarker函数来动态添加标 ...

  7. UML之模型、包及包的版型(构造型)

    包是UML模型的组织结构,也是UML项目的配置管理结构.包存在多个层级,除了顶层包,所有包隶属于一个且仅隶属于一个上层包.在项目不同阶段实际推进与配置过程中,通常以不同层级的包为单位进行check-i ...

  8. 使用Vue+ElementUI实现前端分页

    背景 项目中要做一个公共的附件展示列表,针对某个模块某条记录展示,因此附件不会是大数据量,采用前端分页,使用Vue.JS+ElementUI布局展示,axios请求数据. 步骤 一.Html页面中引入 ...

  9. C# WinForm 检查目标主机的端口是否可连接

    一个小工具. namespace IPPort_CheckTool { partial class MainForm { /// <summary> /// 必需的设计器变量. /// & ...

  10. [megatron代码阅读] 1. 初始化和组网

    以pretrain_gpt.py为例, 看megatron的整体逻辑. 本章主要包括megatron初始化相关逻辑, 核心函数为initialize_megatron, setup_model_and ...