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 兜底,避免业务线程阻塞。
这样既让日志写入不阻塞业务,又能在故障时不丢失数据。我在网关项目里用这套配置,即使磁盘挂了一次也没丢一条日志。
4.4 配置协同与性能影响
这三个进阶组件不是孤立的。它们可以组合使用,也可以独立部署。
比如:Async Logger 作为全局队列 → 日志进入 Routing Appender 分流 → 每个目标 Appender 再套 Failover。
4.5 避坑清单(实战总结)
- Async Logger 丢日志? 检查
RingBufferSize是否过小。高并发写入时如果缓冲区满,新日志会被丢弃(默认行为)。我调大后还没丢过。 - Routing 的匹配性能 避免用
$${ctx:complexObject}做路由键,字符串化开销大。尽量用简单字符串或枚举。 - Failover 脑裂 如果主备切换后主恢复,但 Failover 不会自动切回。你需要外部监控定时执行
http://localhost:8081/stopAppender?name=PRIMARY-FILE再重启,否则会一直写备盘。 - 同步阻塞兜底 即使全部用异步,也要给最底层的 Appender 留一个同步回退,防止 JVM 崩溃前 flush 不完。
「磁盘坏道」这种故障会导致主备同时失效。我曾经就踩过这个坑,日志全丢后才加了一个 NFS 远端备。
嗯,这三个组件用好,基本能应对 99% 的生产日志需求。
从异步缓冲到智能路由,再到故障逃生,层层递进。配置本身不难,难的是每个参数在实际场景下的取舍。多压测、多验证,比任何文档都靠谱。