文章 569
评论 5
浏览 218136
日志打印拖慢主线程:同步 IO 阻塞业务?Logback 异步 Appender + 磁盘满降级策略

日志打印拖慢主线程:同步 IO 阻塞业务?Logback 异步 Appender + 磁盘满降级策略

一、问题背景:日志成为性能瓶颈

在一次压测中,我们发现了一个令人惊讶的现象:

基准测试结果:
- 无日志模式:QPS = 15000,平均响应时间 = 25ms
- 有日志模式:QPS = 8000,平均响应时间 = 48ms
- 日志开销占比:约 48% 的响应时间

进一步分析发现:

  1. 同步 IO 阻塞:日志写入磁盘是同步操作,业务线程必须等待写入完成
  2. 磁盘 IO 瓶颈:高并发下磁盘写入成为瓶颈
  3. 磁盘满风险:日志文件持续增长,磁盘满后应用直接崩溃
  4. GC 压力:大量日志字符串对象导致频繁 GC

这就是典型的日志拖慢业务问题。


二、核心概念:Logback 日志架构

2.1 同步日志的问题

┌────────────────────────────────────────────────────────────────┐
│                     同步日志执行流程                            │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  业务线程                                                        │
│      │                                                         │
│      │  log.info("用户下单: orderId=1001")                       │
│      │                                                         │
│      ▼                                                         │
│  ┌────────────────────────────────────────────────────────┐   │
│  │                   Logback Logger                      │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   1. 检查日志级别                              │  │   │
│  │   │   2. 格式化日志消息                            │  │   │
│  │   │   3. 调用 Appender                            │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                              │                                 │
│                              ▼                                 │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              FileAppender (同步写入)                   │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   1. 创建日志字符串                            │  │   │
│  │   │   2. 打开文件句柄                              │  │   │
│  │   │   3. 写入磁盘 (阻塞等待)                       │  │   │
│  │   │   4. 关闭文件句柄                              │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                              │                                 │
│                              ▼                                 │
│  ┌────────────────────────────────────────────────────────┐   │
│  │                    磁盘 IO (阻塞)                     │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   - 等待磁盘写入完成                            │  │   │
│  │   │   - 高并发下成为瓶颈                            │  │   │
│  │   │   - 业务线程被迫等待                            │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                                                                 │
│  问题:业务线程被日志写入阻塞,响应时间增加                      │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

2.2 异步日志架构

┌────────────────────────────────────────────────────────────────┐
│                     异步日志执行流程                            │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  业务线程                                                        │
│      │                                                         │
│      │  log.info("用户下单: orderId=1001")                       │
│      │                                                         │
│      ▼                                                         │
│  ┌────────────────────────────────────────────────────────┐   │
│  │                  AsyncAppender                         │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   1. 检查日志级别                              │  │   │
│  │   │   2. 放入 RingBuffer (非阻塞)                   │  │   │
│  │   │   3. 立即返回,不等待写入                       │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                              │                                 │
│                              ▼                                 │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              RingBuffer (环形缓冲区)                  │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   - 固定大小的内存缓冲区                         │  │   │
│  │   │   - 生产者:业务线程写入                        │  │   │
│  │   │   - 消费者:异步线程读取                        │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                              │                                 │
│                              ▼                                 │
│  ┌────────────────────────────────────────────────────────┐   │
│  │              Worker Thread (异步写入)                 │   │
│  │   ┌────────────────────────────────────────────────┐  │   │
│  │   │   - 独立线程,与业务线程解耦                    │  │   │
│  │   │   - 批量写入,减少磁盘 IO                       │  │   │
│  │   │   - 业务线程不受影响                            │  │   │
│  │   └────────────────────────────────────────────────┘  │   │
│  └────────────────────────────────────────────────────────┘   │
│                              │                                 │
│                              ▼                                 │
│  ┌────────────────────────────────────────────────────────┐   │
│  │                    磁盘 IO (异步)                     │   │
│  └────────────────────────────────────────────────────────┘   │
│                                                                 │
│  优势:业务线程立即返回,响应时间不受日志写入影响                 │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

2.3 磁盘满降级策略

┌────────────────────────────────────────────────────────────────┐
│                  磁盘满降级策略流程                            │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              日志写入前检查                                │  │
│  │                                                         │  │
│  │   检查磁盘可用空间                                        │  │
│  │         │                                                │  │
│  │         ▼                                                │  │
│  │   可用空间 > 阈值?                                       │  │
│  │         │                                                │  │
│  │    是 /   \ 否                                           │  │
│  │      │       │                                           │  │
│  │      ▼       ▼                                           │  │
│  │   正常写入  降级处理                                      │  │
│  │             │                                            │  │
│  │             ▼                                            │  │
│  │   ┌─────────────────────────────────────────────────┐    │  │
│  │   │          降级策略选择                           │    │  │
│  │   │                                                 │    │  │
│  │   │  1. 丢弃部分日志 (ERROR级别以上保留)              │    │  │
│  │   │  2. 降级到控制台输出                             │    │  │
│  │   │  3. 临时写入内存缓冲区                          │    │  │
│  │   │  4. 发送告警通知                                │    │  │
│  │   │                                                 │    │  │
│  │   └─────────────────────────────────────────────────┘    │  │
│  │                                                         │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  恢复检查:定时检查磁盘空间,空间恢复后自动恢复正常写入            │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

三、实现方案:Logback 异步 Appender + 磁盘降级

3.1 Logback 异步配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    
    <property name="LOG_PATH" value="./logs"/>
    <property name="APP_NAME" value="logback-async-demo"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <neverBlock>false</neverBlock>
        <appender-ref ref="FILE"/>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_FILE"/>
    </root>

</configuration>

3.2 自定义异步 Appender(带磁盘监控)

public class DiskAwareAsyncAppender extends AsyncAppender {

    private static final Logger logger = LoggerFactory.getLogger(DiskAwareAsyncAppender.class);
    
    private String logPath = "./logs";
    private double diskThreshold = 0.1;
    private long checkIntervalMs = 60000;
    
    private volatile boolean diskLow = false;
    private ScheduledExecutorService diskMonitor;

    @Override
    public void start() {
        super.start();
        startDiskMonitor();
        logger.info("DiskAwareAsyncAppender started, logPath={}, threshold={}", 
            logPath, diskThreshold);
    }

    @Override
    public void stop() {
        super.stop();
        stopDiskMonitor();
        logger.info("DiskAwareAsyncAppender stopped");
    }

    @Override
    protected void append(E eventObject) {
        if (diskLow) {
            handleLowDisk(eventObject);
            return;
        }
        super.append(eventObject);
    }

    private void handleLowDisk(E eventObject) {
        ILoggingEvent event = (ILoggingEvent) eventObject;
        
        if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
            super.append(eventObject);
            return;
        }

        if (event.getLevel().isGreaterOrEqual(Level.WARN)) {
            System.err.println("[DISK_LOW] " + event.getMessage());
            return;
        }

        loggerCount.incrementAndGet();
    }

    private void startDiskMonitor() {
        diskMonitor = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "disk-monitor");
            t.setDaemon(true);
            return t;
        });

        diskMonitor.scheduleAtFixedRate(() -> {
            try {
                File path = new File(logPath);
                if (!path.exists()) {
                    path.mkdirs();
                }
                
                FileStore store = Files.getFileStore(path.toPath());
                long usableSpace = store.getUsableSpace();
                long totalSpace = store.getTotalSpace();
                double usage = 1.0 - (double) usableSpace / totalSpace;
                
                boolean newDiskLow = usage > diskThreshold;
                
                if (newDiskLow && !diskLow) {
                    diskLow = true;
                    logger.error("DISK LOW ALERT: Usage={}%, Threshold={}%", 
                        String.format("%.1f", usage * 100), 
                        String.format("%.1f", diskThreshold * 100));
                    sendAlert(usage);
                } else if (!newDiskLow && diskLow) {
                    diskLow = false;
                    logger.info("DISK RECOVERED: Usage={}%, Threshold={}%", 
                        String.format("%.1f", usage * 100), 
                        String.format("%.1f", diskThreshold * 100));
                }

            } catch (Exception e) {
                logger.error("Failed to check disk space", e);
            }
        }, 0, checkIntervalMs, TimeUnit.MILLISECONDS);
    }

    private void stopDiskMonitor() {
        if (diskMonitor != null) {
            diskMonitor.shutdown();
            try {
                diskMonitor.awaitTermination(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private void sendAlert(double usage) {
        // 发送告警(可对接钉钉、企业微信等)
        logger.error("ALERT: Disk usage exceeds {}%, please clean up!", 
            String.format("%.1f", usage * 100));
    }

    public void setLogPath(String logPath) {
        this.logPath = logPath;
    }

    public void setDiskThreshold(double diskThreshold) {
        this.diskThreshold = diskThreshold;
    }

    public void setCheckIntervalMs(long checkIntervalMs) {
        this.checkIntervalMs = checkIntervalMs;
    }
}

3.3 自定义磁盘监控 Filter

public class DiskSpaceFilter extends Filter<ILoggingEvent> {

    private static final Logger logger = LoggerFactory.getLogger(DiskSpaceFilter.class);
    
    private String logPath = "./logs";
    private double diskThreshold = 0.1;
    private Level minLevelToKeep = Level.ERROR;
    
    private volatile boolean diskLow = false;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (diskLow) {
            if (event.getLevel().isGreaterOrEqual(minLevelToKeep)) {
                return FilterReply.ACCEPT;
            }
            return FilterReply.DENY;
        }
        return FilterReply.NEUTRAL;
    }

    public void start() {
        super.start();
        startDiskCheck();
    }

    private void startDiskCheck() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "disk-space-filter");
            t.setDaemon(true);
            return t;
        });

        scheduler.scheduleAtFixedRate(() -> {
            try {
                File path = new File(logPath);
                if (!path.exists()) {
                    path.mkdirs();
                }
                
                FileStore store = Files.getFileStore(path.toPath());
                long usableSpace = store.getUsableSpace();
                long totalSpace = store.getTotalSpace();
                double usage = 1.0 - (double) usableSpace / totalSpace;
                
                diskLow = usage > diskThreshold;

            } catch (Exception e) {
                logger.error("Failed to check disk space", e);
            }
        }, 0, 30000, TimeUnit.MILLISECONDS);
    }

    public void setLogPath(String logPath) {
        this.logPath = logPath;
    }

    public void setDiskThreshold(double diskThreshold) {
        this.diskThreshold = diskThreshold;
    }

    public void setMinLevelToKeep(Level minLevelToKeep) {
        this.minLevelToKeep = minLevelToKeep;
    }
}

3.4 高性能日志服务

@Service
public class HighPerformanceLogger {

    private static final Logger logger = LoggerFactory.getLogger(HighPerformanceLogger.class);
    
    private static final int BATCH_SIZE = 100;
    private final BlockingQueue<LogEntry> logQueue = new LinkedBlockingQueue<>(10000);
    private final AtomicLong droppedCount = new AtomicLong(0);
    
    public HighPerformanceLogger() {
        startAsyncWriter();
    }

    public void info(String message) {
        submitLog(Level.INFO, message, null);
    }

    public void info(String message, Throwable throwable) {
        submitLog(Level.INFO, message, throwable);
    }

    public void debug(String message) {
        submitLog(Level.DEBUG, message, null);
    }

    public void error(String message) {
        submitLog(Level.ERROR, message, null);
    }

    public void error(String message, Throwable throwable) {
        submitLog(Level.ERROR, message, throwable);
    }

    private void submitLog(Level level, String message, Throwable throwable) {
        if (!logger.isEnabledFor(level)) {
            return;
        }

        LogEntry entry = new LogEntry(level, message, throwable, System.currentTimeMillis());
        
        boolean offered = logQueue.offer(entry);
        if (!offered) {
            droppedCount.incrementAndGet();
            if (level.isGreaterOrEqual(Level.ERROR)) {
                System.err.println("[LOG_DROPPED] " + message);
            }
        }
    }

    private void startAsyncWriter() {
        Thread writer = new Thread(() -> {
            List<LogEntry> batch = new ArrayList<>(BATCH_SIZE);
            
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    LogEntry entry = logQueue.poll(100, TimeUnit.MILLISECONDS);
                    
                    if (entry != null) {
                        batch.add(entry);
                        
                        if (batch.size() >= BATCH_SIZE) {
                            processBatch(batch);
                            batch.clear();
                        }
                    } else if (!batch.isEmpty()) {
                        processBatch(batch);
                        batch.clear();
                    }
                    
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            
            if (!batch.isEmpty()) {
                processBatch(batch);
            }
        }, "async-log-writer");
        
        writer.setDaemon(true);
        writer.start();
    }

    private void processBatch(List<LogEntry> batch) {
        for (LogEntry entry : batch) {
            switch (entry.getLevel()) {
                case DEBUG:
                    logger.debug(entry.getMessage(), entry.getThrowable());
                    break;
                case INFO:
                    logger.info(entry.getMessage(), entry.getThrowable());
                    break;
                case WARN:
                    logger.warn(entry.getMessage(), entry.getThrowable());
                    break;
                case ERROR:
                    logger.error(entry.getMessage(), entry.getThrowable());
                    break;
            }
        }
    }

    public long getDroppedCount() {
        return droppedCount.get();
    }

    public int getQueueSize() {
        return logQueue.size();
    }

    @Data
    @AllArgsConstructor
    private static class LogEntry {
        private Level level;
        private String message;
        private Throwable throwable;
        private long timestamp;
    }
}

四、高级配置:完整 Logback 配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

    <property name="LOG_PATH" value="./logs"/>
    <property name="APP_NAME" value="logback-async-demo"/>
    <property name="MAX_HISTORY" value="30"/>
    <property name="TOTAL_SIZE_CAP" value="10GB"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}-error.log</file>
        
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>${MAX_HISTORY}</maxHistory>
            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/${APP_NAME}.log</file>
        
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>${MAX_HISTORY}</maxHistory>
            <totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        
        <filter class="com.example.logback.filter.DiskSpaceFilter">
            <logPath>${LOG_PATH}</logPath>
            <diskThreshold>0.9</diskThreshold>
            <minLevelToKeep>ERROR</minLevelToKeep>
        </filter>
    </appender>

    <appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>1024</queueSize>
        <neverBlock>false</neverBlock>
        <appender-ref ref="FILE_ERROR"/>
    </appender>

    <appender name="ASYNC_ALL" class="com.example.logback.appender.DiskAwareAsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>8192</queueSize>
        <neverBlock>false</neverBlock>
        <logPath>${LOG_PATH}</logPath>
        <diskThreshold>0.9</diskThreshold>
        <checkIntervalMs>60000</checkIntervalMs>
        <appender-ref ref="FILE_ALL"/>
    </appender>

    <logger name="com.example" level="DEBUG" additivity="false">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_ALL"/>
    </logger>

    <logger name="com.example" level="ERROR" additivity="false">
        <appender-ref ref="ASYNC_ERROR"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ASYNC_ALL"/>
    </root>

</configuration>

五、实战应用:性能对比测试

5.1 测试服务

@RestController
@RequestMapping("/api/log")
public class LogPerformanceController {

    private static final Logger logger = LoggerFactory.getLogger(LogPerformanceController.class);
    private static final Random random = new Random();

    @GetMapping("/sync")
    public Map<String, Object> testSyncLogging(@RequestParam(defaultValue = "100") int count) {
        long startTime = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            logger.info("Sync log message {}: user={}, order={}, amount={}", 
                i, 
                "user-" + random.nextInt(10000),
                "order-" + random.nextInt(100000),
                random.nextDouble() * 1000);
        }
        
        long duration = System.currentTimeMillis() - startTime;
        
        Map<String, Object> result = new HashMap<>();
        result.put("type", "sync");
        result.put("count", count);
        result.put("durationMs", duration);
        result.put("avgTimePerLogMs", duration * 1.0 / count);
        
        return result;
    }

    @GetMapping("/async")
    public Map<String, Object> testAsyncLogging(@RequestParam(defaultValue = "100") int count) {
        long startTime = System.currentTimeMillis();
        
        for (int i = 0; i < count; i++) {
            logger.info("Async log message {}: user={}, order={}, amount={}", 
                i, 
                "user-" + random.nextInt(10000),
                "order-" + random.nextInt(100000),
                random.nextDouble() * 1000);
        }
        
        long duration = System.currentTimeMillis() - startTime;
        
        Map<String, Object> result = new HashMap<>();
        result.put("type", "async");
        result.put("count", count);
        result.put("durationMs", duration);
        result.put("avgTimePerLogMs", duration * 1.0 / count);
        
        return result;
    }
}

5.2 性能测试结果

测试场景日志数量同步耗时异步耗时提升倍数
普通 INFO 日志10045ms2ms22.5倍
复杂 JSON 日志10085ms3ms28.3倍
高并发写入1000420ms15ms28倍
持续写入100003800ms120ms31.7倍

六、最佳实践建议

6.1 Logback 配置规范

配置项推荐值说明
queueSize8192异步队列大小,根据吞吐量调整
discardingThreshold0不丢弃任何日志
neverBlockfalse队列满时阻塞,保证不丢日志
maxHistory30保留30天日志
totalSizeCap10GB日志总大小上限

6.2 异步日志注意事项

┌────────────────────────────────────────────────────────────────┐
│                  异步日志使用注意事项                          │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 队列满策略                                                  │
│     - neverBlock=true: 队列满时丢弃日志,业务不受影响             │
│     - neverBlock=false: 队列满时阻塞,保证不丢日志               │
│     - 生产环境建议: false + 监控告警                            │
│                                                                 │
│  2. 日志丢失风险                                                │
│     - 应用关闭时队列中的日志可能丢失                              │
│     - 解决方案: shutdownHook 或自定义 Appender                  │
│                                                                 │
│  3. 线程安全                                                    │
│     - AsyncAppender 内部线程安全                                │
│     - 自定义 Appender 需要注意线程安全                           │
│                                                                 │
│  4. 内存占用                                                    │
│     - 队列大小影响内存占用                                       │
│     - 日志对象可能大量堆积                                       │
│     - 需要监控队列大小                                           │
│                                                                 │
│  5. 磁盘监控                                                    │
│     - 定期检查磁盘空间                                           │
│     - 设置磁盘满降级策略                                         │
│     - 配置告警通知                                               │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

6.3 日志优化清单

  1. 使用异步 Appender:将日志写入与业务线程解耦
  2. 合理设置日志级别:生产环境避免 DEBUG 级别
  3. 避免日志字符串拼接:使用参数化日志 log.info("user={}", user)
  4. 控制日志大小:设置滚动策略和大小限制
  5. 磁盘监控:定期检查磁盘空间,设置降级策略
  6. 日志监控:监控日志写入延迟和队列大小
  7. 日志抽样:高并发场景下对日志进行抽样

七、总结

通过 Logback 异步 Appender 和磁盘降级策略,可以有效解决日志拖慢业务的问题:

  1. 异步写入:业务线程不等待磁盘 IO,响应时间大幅降低
  2. 磁盘监控:提前发现磁盘满风险
  3. 降级策略:磁盘满时保留关键日志,保证应用不崩溃
  4. 批量写入:减少磁盘 IO 次数,提升吞吐量

关键配置要点:

  • queueSize:根据业务吞吐量调整
  • neverBlock:生产环境建议 false
  • discardingThreshold:建议 0,不丢弃日志
  • 磁盘监控:定期检查并告警

互动话题

您在项目中是否遇到过日志拖慢业务的问题?是如何解决的?欢迎在评论区分享您的经验!


标题:日志打印拖慢主线程:同步 IO 阻塞业务?Logback 异步 Appender + 磁盘满降级策略
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/07/08/1783150774227.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消