🚀 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