Kafka + MongoDB 构建异步日志系统(含 DLQ、Prometheus 可视化)实战记录

🚀 Kafka + MongoDB 构建异步日志系统(含 DLQ、Prometheus 可视化)实战记录

🧱 背景:日志系统的可观测性 + 解耦挑战

在实际项目中,我们希望记录用户操作日志,但又不希望主流程被数据库写入操作阻塞。

因此,我们设计并实现了一个异步日志系统,具备以下能力:

  • ✨ 完全异步:用户请求不等待日志落库
  • 🔁 自动重试:失败自动重试 + 自定义重试次数
  • 💀 死信转移:失败消息写入 Dead Letter Queue(DLQ)
  • 📊 实时可观测:通过 Prometheus + Grafana 观察运行情况

💡 选择 Kafka + MongoDB:

  • Kafka 支持高吐量、体系解耦、免阻塞性的日志流通
  • MongoDB 结构灵活,大量多模块日志数据无需预编程模型
  • 后期日志系统可以抽取成 Audit 中心,用于分析日志模式,识别异常行为,检测敏感操作,发现系统瓶颈等等

✅ 1. Kafka 异步发送日志

🔸 定义结构化事件

typescript 复制代码
@Data
public class AuditLogEvent {
    private String action;
    private String entity;
    private String payload;
    private Instant timestamp;
}

🔸 Producer 发送逻辑

ini 复制代码
String randomKey = UUID.randomUUID().toString();
kafkaTemplate.send("audit-log-topic", randomKey, auditLogEvent);

✅ 使用 UUID 作为 Key,是为了让 Kafka 自动将日志消息"随机"分配到各个分区中,形成负载均衡,适合日志系统这种对顺序性不敏感的场景。


✅ 2. Kafka Consumer 多线程并发处理

🛠️ 如何增加 Topic 分区数

默认创建 topic 时 partition 数是 1,为了支持多线程并发消费(每线程绑定一个分区),我们手动扩展分区数量。

执行以下命令(假设你用的是 Docker 启动的 Kafka 容器):

bash 复制代码
docker exec -it kafka kafka-topics \
  --alter \
  --bootstrap-server localhost:9092 \
  --topic audit-log-topic \
  --partitions 3

执行成功后,可以在 Kafka UI 中看到 audit-log-topic 被划分为 partition-0/1/2 三个分区。

配置消费线程数:

ini 复制代码
factory.setConcurrency(3);

每个分区由独立线程处理,配合日志打印:

less 复制代码
log.info("📥 [Thread: {}] Received: {}", Thread.currentThread().getName(), event.getAction());

通过 partition 分布 + 彩色输出可视化处理线程。比如:

bash 复制代码
📥 [Thread: kafkaListener-0] Received: GENERATE  (绿色)
📥 [Thread: kafkaListener-1] Received: GENERATE  (蓝色)
📥 [Thread: kafkaListener-2] Received: GENERATE  (黄色)

✅ 自动重试机制 + Jitter 随机延迟

💡 为什么需要?

当 Kafka 消费者处理失败时,如果立即重试,容易出现"雪崩效应"(所有线程同时重试),加剧服务压力。加入 Jitter(抖动) 后,可错开重试时间。

☕ 代码片段:
java 复制代码
BackOff backOff = new BackOff() {
    private final Random random = new Random();
    private final int maxRetries = 3;

    @Override
    public BackOffExecution start() {
        return new BackOffExecution() {
            int attempt = 0;

            @Override
            public long nextBackOff() {
                if (attempt++ >= maxRetries) return STOP;
                return 2000 + random.nextInt(1000); // 2s ± 1s
            }
        };
    }
};
✨ 效果展示:
  • 每条失败消息最多重试 3 次
  • 每次重试延迟为 2~3s 的随机值
  • 超过最大次数后进入 DLQ(死信队列)

✅ 手动 ACK + 分区并发处理

💡 为什么选择手动 ACK?

可控性更强:你可以在保存数据库后手动提交 offset,确保处理成功再确认。

☕ 代码片段:
java 复制代码
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);

💡 为什么设置并发数?

Kafka 每个分区只能被一个线程消费,因此设置并发数可激活多线程并行处理。

☕ 代码片段:
java 复制代码
factory.setConcurrency(3);

✅ 日志彩色可视化:看见线程

为了便于观察多线程消费效果,我们根据线程名动态指定颜色输出:

☕ 代码片段:
java 复制代码
private static final String[] COLORS = { "\u001B[34m", "\u001B[32m", ... };

String threadName = Thread.currentThread().getName();
int colorIndex = Math.abs(threadName.hashCode()) % COLORS.length;
log.info(color + "📥 [{}] Received: {}" + RESET, threadName, event.getAction());

📈 效果示例:

bash 复制代码
📥 [Thread: kafkaListener-0] Received: GENERATE  (蓝色)
📥 [Thread: kafkaListener-1] Received: GENERATE  (绿色)
📥 [Thread: kafkaListener-2] Received: GENERATE  (黄色)

这样你能一眼看出:

  • 哪个分区的消息被哪个线程处理
  • 消费逻辑是否均匀分布到多个线程

✅ 3. 死信队列(DLQ)处理

✨ 自定义 Recoverer:失败转发至 DLQ

csharp 复制代码
kafkaTemplate.send("audit-log-dlt", record.key(), record.value());

✨ 创建 DLQ Consumer:入库 MongoDB, 并统计处理所耗时间:

scss 复制代码
@KafkaListener(topics = "audit-log-dlt", groupId = "dlq-consumer-group")
public void handleDLQ(AuditLogEvent event) {
    Instant start = Instant.now();
    log.warn("❌ [DLQ] Received: {}", event);

    AuditDeadLetterDocument doc = new AuditDeadLetterDocument();
    doc.setAction(event.getAction());
    doc.setEntity(event.getEntity());
    doc.setPayload(event.getPayload());
    doc.setTimestamp(event.getTimestamp());
    doc.setDeadLetteredAt(LocalDateTime.now());
    doc.setErrorMessage("Failed after max retry attempts");

    deadLetterRepository.save(doc);

    log.warn("✅ DLQ message saved to MongoDB (audit_dead_logs)");

    // ✅ 埋点:DLQ 消费成功次数 +1
    meterRegistry.counter("dlq_consumed_success_total").increment();

    // Record time cost
    Timer.builder("dlq_processing_duration_seconds")
            .description("Duration for processing DLQ messages")
            .register(meterRegistry)
            .record(Duration.between(start, Instant.now()));
}

✅ 4. Prometheus + Grafana 可视化

📍 添加 Micrometer 依赖

xml 复制代码
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

📍 application.yml

yaml 复制代码
management:
  endpoints.web.exposure.include: prometheus
  metrics.export.prometheus.enabled: true

📍 docker-compose.yml

bash 复制代码
prometheus:
  image: prom/prometheus
  ports:
    - "9090:9090"
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  • Prometheus 指定 scrape频率 + spring boot actuator,Prometheus Targets 可看到: http://localhost:9090/targets
  • Grafana 通过 http://localhost:3000 连接 Prometheus 源
  • 直接搜索: dlq_consumed_success_total / dlq_processing_duration_seconds

🧪 遇到的坑 & 排查记录

AckMode 无法识别

  • 问题描述 :无法使用 setAckMode(...) 方法
  • 原因 :没有通过 getContainerProperties() 获取 ContainerProperties 实例
  • 解决方案
java 复制代码
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);

BiConsumer 无法强转为 ConsumerRecordRecoverer

  • 问题描述 :使用 (ConsumerRecordRecoverer) biConsumer 强转时报错
  • 原因 :Lambda 表达式无法直接转为 ConsumerRecordRecoverer 类型
  • 解决方案:使用显式实现:
java 复制代码
ConsumerRecordRecoverer recoverer = (record, ex) -> {
    kafkaTemplate.send("audit-log-dlt", record.key().toString(), (AuditLogEvent) record.value());
};

❌ Prometheus /actuator/prometheus 返回 404

Issue : 访问 http://localhost:8080/actuator/prometheus 时出现 Whitelabel Error Page
原因 :项目依赖已配置,但未执行 mvn clean install,导致依赖未生效

解决方案 | Solution:

bash 复制代码
mvn clean install

❌ DLQ 消费失败:反序列化异常

  • 问题描述@KafkaListener(topics = "audit-log-dlt") 无消费消息
  • 真实原因 :错误同时使用了 props.put(...) 和显式实例化 JsonDeserializer,导致反序列化器被覆盖,DLQ 中消息无法解析。
  • 错误写法(混用)
java 复制代码
JsonDeserializer<AuditLogEvent> deserializer = new JsonDeserializer<>(AuditLogEvent.class);
deserializer.addTrustedPackages("*");

// ⚠️ 错误:混用了实例和配置项
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer);
  • 正确写法(使用实例,不配置类名)
java 复制代码
JsonDeserializer<AuditLogEvent> deserializer = new JsonDeserializer<>(AuditLogEvent.class);
deserializer.addTrustedPackages("*");

Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "audit-consumer-group");

return new DefaultKafkaConsumerFactory<>(
    props,
    new StringDeserializer(),
    deserializer
);

并移除 application.yml 中关于反序列化器的配置,避免冲突。


✅ 项目结构示意

erlang 复制代码
├── controller/
│   └── ...
├── dto/
│   └── ...
├── kafka/
│   ├── KafkaConsumerConfig.java
│   ├── KafkaConsumerService.java
│   └── DLQConsumerService.java
├── mongo/
│   ├── AuditLogDocument.java
│   ├── AuditDeadLetterDocument.java
│   ├── AuditLogRepository.java
│   └── DeadLetterRepository.java
├── service/
│   └── ...
├── utils/
│   └── ...
└── GeneratorApplication.java

resources/
├── static/
├── templates/
└── application.yml

docker-dev-env
├── docker-compose.yml
├── prometheus.yml

✅ 成果

目前我们已经完成:

  • Kafka 日志异步化 + 多线程消费
  • 自动重试 + DLQ 转发机制
  • MongoDB 日志与死信落地
  • Prometheus + Grafana 可视化健康指标

✅ 已发布:v1.1.2 -- Kafka DLQ + Observability

🧭 项目源码地址

  • 📌 本文所有代码已开源,欢迎 Star、Fork
  • 📬 如果你也在做类似项目,欢迎留言交流 !

👉 GitHub 项目地址:
🔗 https://github.com/xmyLydia/rapid-crud-generator

相关推荐
跟着珅聪学java3 小时前
kafka菜鸟教程
分布式·kafka
差不多程序员10 小时前
工作纪实_63-Mac电脑使用brew安装软件
kafka·mac·brew
慧一居士10 小时前
Spring Boot 集成 Kafka 及实战技巧总结
spring boot·kafka
斯普信专业组17 小时前
Kafka安全认证技术:SASL/SCRAM-ACL方案详解
分布式·安全·kafka
Goldchenn20 小时前
kafka服务端和springboot中使用
spring boot·分布式·kafka
芝法酱1 天前
芝法酱躺平攻略(21)——kafka安装和使用
分布式·kafka
小奏技术3 天前
Kafka要保证消息的发送和消费顺序性似乎没那么简单
后端·kafka
小吕学编程3 天前
基于Canal+Spring Boot+Kafka的MySQL数据变更实时监听实战指南
数据库·后端·mysql·spring·kafka
xmyLydia3 天前
Kafka 本地开发环境 + 可视化 UI 快速搭建与排坑记录
kafka