记录:db服务器资源使用率过高

2025-12-19

从图中可以看出有一条insert sql每秒执行252.55次,并且是记录用户操作的,不是业务数据。

等待类型分析(Wait Events)

颜色

Wait 类型

含义

🟨 橙色

IO:XactSync

事务日志同步 I/O —— 写入 redo log 的等待

🟩 绿色

CPU

CPU 处理请求的时间

🟦 蓝色

IO:DataFileRead

读数据文件的 I/O

🟨 浅黄

IPC:BufferIO

缓冲区 I/O

可以看出,其中IO:XactSync 占主导,表明数据库正在频繁地将事务日志刷盘(write-ahead logging)。

存在风险

  • DB 延迟升高

  • 应用超时

  • 连接池耗尽

  • 最终服务不可用

问题分析

目前记录操作日志是通过event-listener异步逐条插入db,所以应用服务器目前没有太大压力,但是db有问题。并且没有设置开关/分级能动态启动关闭这个功能,所以导致活动期间,用户一多,操作飙升时造成db很大压力。

解决方案

  • 短期

在配置文件中添加userActionLogEnable:true,并且在listener中进行判断,如果为false则不落库。这种方式可以瞬间减轻db压力,缺点是操作记录丢失。

  • 中期

  1. 对操作进行分级(类似info,warn,error),这样可以在高并发时选择关闭info的操作记录又能记录重要的操作。

  2. 对listener进行改造,使用批量插入,而不是来一条插入一条,这样可以减少IO:XactSync的时间。

@Component
@Slf4j
public class BatchAuditLogListener {

    private final List<UserActionLog> buffer = new ArrayList<>();
    private final Object lock = new Object();

    @Autowired
    private UserActionLogRepository logRepository;

    // 使用独立线程池(避免占用 Spring 的事件线程)
    private final ExecutorService batchExecutor = 
        Executors.newSingleThreadExecutor(r -> new Thread(r, "audit-batch-writer"));

    // 监听事件:只入队,不写 DB
    @EventListener
    public void onUserAction(UserActionEvent event) {
        synchronized (lock) {
            buffer.add(convertToLog(event));
            // 可选:达到批次大小立即 flush
            if (buffer.size() >= 100) {
                flush();
            }
        }
    }

    // 每 2 秒强制刷一次(防止尾部数据积压)
    @Scheduled(fixedDelay = 2000)
    public void scheduledFlush() {
        synchronized (lock) {
            if (!buffer.isEmpty()) {
                flush();
            }
        }
    }

    private void flush() {
        if (buffer.isEmpty()) return;
        
        List<UserActionLog> toSave = new ArrayList<>(buffer);
        buffer.clear(); // 清空缓冲区

        batchExecutor.submit(() -> {
            try {
                logRepository.saveAll(toSave); // 批量插入(JPA/Hibernate)
                log.debug("Batch saved {} audit logs", toSave.size());
            } catch (Exception e) {
                log.error("Failed to batch save audit logs", e);
                // TODO: 考虑重试 or 写死信文件
            }
        });
    }

    @PreDestroy
    public void destroy() {
        scheduledFlush(); // 应用关闭前清空 buffer
        batchExecutor.shutdown();
    }
}

效果对比

指标

原方案(每事件 1 次 INSERT)

新方案(批量)

DB 事务数

250 / 秒

2~5 / 秒(每批 100 条)

IO:XactSync 等待

大幅降低

IOPS 消耗

~1000

~20~50

数据延迟

实时

最多 2 秒(可接受)

  • 长期

可以增加消息队列(Kafka),由于本应用在AWS上部署,可以优先考虑Kinesis Data Streams + Fargate 消费者。

最终效果

在问题发生当天采用了短期方案,然后活动过后对代码进行修改,使用了中期方案,通过后面半年的观察,再决定用不用使用长期方案。