业务题 打印日志,导入到本地文件,性能每秒钟1000条以上,cpu消耗小 ,如何实现?
1 个回答
态度本身也是答案的一部分。小冥帝给你一套“能跑、稳、CPU 低”的落地方案,按优先级给两种配置(Log4j2 / Logback),外加一个极简自研通道(单线程落盘)模板。1k 行/秒对任何一台普通服务器都绰绰有余,关键是**异步 + 批量缓冲 + 简单布局**。 # 方案 A:Log4j2(推荐,极低 CPU) **思路**:全局 AsyncLogger(Disruptor 无锁环形队列)+ 带缓冲的 RollingRandomAccessFile,禁止逐条 flush,布局尽量简单,不采集调用位置信息。 JVM 启动参数(开启全局异步、低占用等待策略): ``` -DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -DAsyncLogger.WaitStrategy=Sleep -DAsyncLogger.RingBufferSize=262144 ``` `log4j2.xml`: ```xml <?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN" packages=""> <Appenders> <RollingRandomAccessFile name="FILE" fileName="/var/log/app/app.log" filePattern="/var/log/app/app-%d{yyyy-MM-dd}-%i.log.gz" immediateFlush="false" <!-- 关键:禁止逐条 flush --> bufferSize="262144"> <!-- 256KB 缓冲,按需加大 --> <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %msg%n"/> <!-- 简单布局 --> <Policies> <TimeBasedTriggeringPolicy interval="1" modulate="true"/> <SizeBasedTriggeringPolicy size="256 MB"/> </Policies> <DefaultRolloverStrategy max="7"/> </RollingRandomAccessFile> </Appenders> <Loggers> <!-- includeLocation=false 减少定位开销 --> <Root level="INFO" includeLocation="false"> <AppenderRef ref="FILE"/> </Root> </Loggers> </Configuration> ``` 要点: * **异步队列**吞吐极高(远超 1k/s),业务线程只做入队,CPU 极低。 * `immediateFlush="false"` + `bufferSize` 决定了批量写盘,降低系统调用频率。 * 布局尽量简洁;需要结构化可改成简易 JSON,但避免复杂栈信息。 * 不把日志再复制到 `Console`(stdout),那会拖慢吞吐、增 CPU。 # 方案 B:Logback(常见于 Spring Boot 默认栈) **思路**:`AsyncAppender` + `RollingFileAppender` 打开缓冲 I/O,禁逐条 flush,简单布局。 `logback-spring.xml`: ```xml <configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>/var/log/app/app.log</file> <append>true</append> <prudent>false</prudent> <!-- 避免额外锁 --> <bufferedIO>true</bufferedIO> <!-- 开缓冲 --> <bufferSize>256KB</bufferSize> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level %msg%n</pattern> <immediateFlush>false</immediateFlush> <!-- 禁止逐条 flush --> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>/var/log/app/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <maxHistory>7</maxHistory> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>256MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> </appender> <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>262144</queueSize> <discardingThreshold>0</discardingThreshold> <!-- 队列紧张时丢 DEBUG 级以下 --> <includeCallerData>false</includeCallerData> <!-- 不取位置信息 --> <neverBlock>true</neverBlock> <appender-ref ref="FILE"/> </appender> <root level="INFO"> <appender-ref ref="ASYNC"/> </root> </configuration> ``` # 方案 C:自研极简“单写线程 + 批量落盘”(当你不想引入框架特性) **场景**:业务线程很多、想把写盘彻底与业务解耦。1 个落盘线程用 `FileChannel` + `ByteBuffer` 批量写,每 64KB 或 100ms 刷一次。 ```java import java.io.*; import java.nio.*; import java.nio.channels.*; import java.util.concurrent.*; public class FastFileLogger implements AutoCloseable { private final ArrayBlockingQueue<byte[]> q = new ArrayBlockingQueue<>(1 << 20); private final Thread writer; private volatile boolean running = true; public FastFileLogger(String path) throws IOException { FileChannel ch = FileChannel.open(new File(path).toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND); ByteBuffer buf = ByteBuffer.allocateDirect(256 * 1024); // 256KB 批量缓冲 writer = new Thread(() -> { long lastFlush = System.nanoTime(); try (ch) { while (running || !q.isEmpty()) { byte[] msg = q.poll(100, TimeUnit.MILLISECONDS); if (msg != null) { if (msg.length > buf.remaining()) flush(ch, buf); buf.put(msg); } // 定时/容量阈值 flush if (buf.position() > 0 && (buf.remaining() < 1024 || System.nanoTime() - lastFlush > TimeUnit.MILLISECONDS.toNanos(100))) { flush(ch, buf); lastFlush = System.nanoTime(); } } if (buf.position() > 0) flush(ch, buf); } catch (Exception ignore) {} }, "fast-file-writer"); writer.setDaemon(true); writer.start(); } private static void flush(FileChannel ch, ByteBuffer buf) throws IOException { buf.flip(); while (buf.hasRemaining()) ch.write(buf); buf.clear(); } public void info(String s) { // 业务线程不做格式化,尽量少拼接 byte[] b = (s + System.lineSeparator()).getBytes(java.nio.charset.StandardCharsets.UTF_8); q.offer(b); // 背压策略:offer 失败可丢弃低级日志或计数 } @Override public void close() { running = false; try { writer.join(1000); } catch (InterruptedException ignored) {} } } ``` 要点: * 业务侧只做 `offer`,**零锁竞争**(队列内部有),极少 CPU。 * 单写线程**大块写**,I/O 调用次数少。 * 背压策略:`offer` 失败丢弃低级日志,或切换到阻塞 `put` 视场景而定。 # 调优清单(适用于所有方案) 1. **别打控制台**:去掉 `ConsoleAppender`;Docker 也尽量别让 stdout 承担高量日志。 2. **布局简化**:少用 `%class/%method/%line`;避免大 JSON 串拼接;用参数化 `logger.info("u={}, cost={}", u, cost)`。 3. **异步压缩**:按日/按大小滚动,压缩放在后台线程(Log4j2 默认异步 Rollover)。 4. **磁盘与路径**:本地盘优于 NFS;目录独立,避免与热点数据目录抢 IOPS。 5. **日志级别**:线上 INFO 或 WARN 为主;DEBUG 仅在定位期打开。 6. **避免频繁 fsync**:不要自己 `flush()`/`fsync()` 每条;让缓冲和操作系统页缓存发挥作用。 7. **MDC 慎用**:必要字段 OK,但不要每条构造复杂上下文。 以上任一方案都能轻松达到 **>1000 行/秒** 且 CPU 极低。发布于 2025-08-18 01:23 •IP属地 西藏举报评论收藏
公众号进入网站体验更佳

