4、Log4j 2 进阶配置:Async Logger、Routing Appender、Failover 机制

性能瓶颈,是生产系统绕不开的坎。
我记得刚接手一个高并发支付服务,日志打印就把接口耗时直接拉高了 15%。
排查下来,发现全是同步写日志的锅。
这章我就把 Log4j 2 里三个压箱底的武器讲透:异步日志器、路由附加器和故障转移机制。

4.1 Async Logger —— 让日志写入真正「零阻塞」

Async Logger 不是 Async Appender 的替代品,它更底层。
说白了,它维护一个环形缓冲区(RingBuffer),业务线程只管扔日志事件,消费线程异步刷盘。
我个人习惯在压测阶段,把异步日志的等待策略设成 sleep,而不是默认的 busy-spin,后者太吃 CPU。

# 启用 Async Logger(log4j2.component.properties)
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector

# 或者用 AsyncRoot 混合模式(更推荐)
<AsyncRoot level="INFO">
    <AppenderRef ref="FILE" />
</AsyncRoot>

你想想看,同步日志下,每个业务线程都在等磁盘 I/O。
Async Logger 把耗时从毫秒级降到微秒级,高并发场景收益巨大。

避坑: 我曾经一上来就乱配 includeLocation,结果性能反而比同步还差。
记住:Async Logger 下 includeLocation="false" 能砍掉 50% 的延迟。
如果需要定位,就用线程名 + MDC 来代替。

4.1.1 关键参数一览

参数 默认值 说明
RingBufferSize 256 * 1024 环形缓冲区大小,高并发可加大到 1024*1024
WaitStrategy busy-spin sleep / yield / block(低延迟选 busy-spin,省 CPU 选 sleep)
includeLocation true 生产环境务必设为 false,否则有巨大的对象创建开销

4.2 Routing Appender —— 动态路由,日志按条件分流

一个系统里有订单日志、支付日志、风控日志,全写到同一个文件里够他们翻半天。
Routing Appender 可以根据 ThreadContext 或 MDC 的值,把日志路由到不同的 Appender。

我曾在金融项目里用它做「多租户」日志隔离。
每个租户的请求通过拦截器设置 ThreadContext.put("tenantId", tenant),日志自动落到对应租户的文件。

<Routing name="ROUTER">
    <Routes pattern="$${ctx:tenantId}">
        <Route key="tenant_a">
            <File name="A-FILE" fileName="/logs/a/app.log" />
        </Route>
        <Route key="tenant_b">
            <File name="B-FILE" fileName="/logs/b/app.log" />
        </Route>
        <Route ref="DEFAULT" />
    </Routes>
</Routing>
小心路由键为空: 如果没有设置 tenantId 会走默认路由,但默认路由会复用第一个创建的 Appender。
我曾经没配默认路由,结果某些请求的日志神秘消失了……
给每个路由配置单独指定一个 Route ref="default-file" 最稳妥。

4.2.1 路由与性能的博弈

Routing Appender 每次匹配都会执行字符串解析,频度太高时会增加 CPU 开销。
我的经验是:不要对每一条日志都做路由判断。
可以做两级优化:先用 Async Logger 缓冲,再让后台线程统一路由。

4.3 Failover Appender —— 主备切换,日志一条不丢

日志不允许出现「写挂就丢」的情况。
Failover Appender 备选的方案是:主 Appender 写入失败时,自动切换到备 Appender。

<Failover name="FAILOVER" primary="PRIMARY-FILE">
    <Failovers>
        <AppenderRef ref="BACKUP-FILE" />
        <AppenderRef ref="BACKUP-DB" />
    </Failovers>
    <!— 主 Appender 不可用时,等待 10 秒再尝试 —>
    <TimeoutInterval>10000</TimeoutInterval>
</Failover>

<File name="PRIMARY-FILE" fileName="/data/logs/app.log" />
<File name="BACKUP-FILE" fileName="/backup/logs/app.log" />

为什么要配多个备?

如果你只有一个备,主挂备也挂,日志就彻底没了。
我一般配两级:第一备是同磁盘不同目录,第二备是远端共享存储或日志平台。

4.3.1 故障场景实战

  • 磁盘满了: 主写入失败 → Failover 自动切到备盘,日志继续写。等磁盘清理完成后,主恢复(需要额外脚本重启 Appender)。
  • 网络分区: 如果主是远程 Socket Appender,网络闪断时 Failover 会切到本地文件。记得设 retryInterval 不要太大(默认 30 秒),否则恢复太慢。
  • 备也失败: 所有备都挂掉时,Failover Appender 会抛出异常。建议在最外层的 Async Logger 兜底,避免业务线程阻塞。
黄金组合: Async Logger + Failover Appender + 不同磁盘的备。
这样既让日志写入不阻塞业务,又能在故障时不丢失数据。我在网关项目里用这套配置,即使磁盘挂了一次也没丢一条日志。

4.4 配置协同与性能影响

这三个进阶组件不是孤立的。它们可以组合使用,也可以独立部署。
比如:Async Logger 作为全局队列 → 日志进入 Routing Appender 分流 → 每个目标 Appender 再套 Failover。

Log4j 2 进阶配置核心流程 Async Logger 环形缓冲区 + 异步消费者 Routing Appender 根据 ThreadContext 分流 Failover Appender 主备切换,故障恢复 本地文件 / 控制台 按租户/业务分类 备盘 / 远端日志 建议顺序:Web 层 → Async Logger → Routing → Failover → 各 Appender 错误处理:避免在 Routing 表达式内执行重逻辑;Failover 的 retryInterval 不要小于 5 秒

4.5 避坑清单(实战总结)

  1. Async Logger 丢日志? 检查 RingBufferSize 是否过小。高并发写入时如果缓冲区满,新日志会被丢弃(默认行为)。我调大后还没丢过。
  2. Routing 的匹配性能 避免用 $${ctx:complexObject} 做路由键,字符串化开销大。尽量用简单字符串或枚举。
  3. Failover 脑裂 如果主备切换后主恢复,但 Failover 不会自动切回。你需要外部监控定时执行 http://localhost:8081/stopAppender?name=PRIMARY-FILE 再重启,否则会一直写备盘。
  4. 同步阻塞兜底 即使全部用异步,也要给最底层的 Appender 留一个同步回退,防止 JVM 崩溃前 flush 不完。
致命陷阱: 不要把 Failover Appender 的备设置成主所在的相同物理磁盘。
「磁盘坏道」这种故障会导致主备同时失效。我曾经就踩过这个坑,日志全丢后才加了一个 NFS 远端备。

嗯,这三个组件用好,基本能应对 99% 的生产日志需求。
从异步缓冲到智能路由,再到故障逃生,层层递进。配置本身不难,难的是每个参数在实际场景下的取舍。多压测、多验证,比任何文档都靠谱。