日志采集原理:日志格式、采集方式及异步处理
日志采集这件事,说起来好像就是「把日志记下来发走」。但我做了几年监控系统,踩过的坑比写过的代码还多。日志格式不对、采集器把业务进程拖死、队列满了丢日志——这些事故我全经历过。今天就把底层原理掰开揉碎了聊一聊。
日志格式:结构化才是出路
记得刚接手一个老项目,日志全是 [INFO] user login success 这种纯文本。到了要排查问题时,靠 grep 和正则硬抠字段,效率低不说,还经常漏数据。后来我定了个规矩:所有日志必须走 JSON 或 key=value 的结构化格式。
为什么一定要结构化?说白了,监控系统后面接的是 ES、Loki、ClickHouse 这些列式存储,非结构化的文本进去,检索、聚合、告警都很难搞。
我推荐的日志格式模板(JSON):
{
"timestamp": "2025-04-07T10:15:30.123Z",
"level": "ERROR",
"logger": "com.example.payment.Service",
"thread": "http-nio-8080-exec-3",
"message": "订单支付超时",
"traceId": "a1b2c3d4e5f6",
"spanId": "abc123",
"userId": "u_98765",
"duration_ms": 1200,
"extra": {
"orderId": "ORD-20250407-001"
}
}
核心字段说明:timestamp 统一用 ISO8601 带时区;traceId 用于链路追踪;duration_ms 统计耗时;extra 放业务上下文。这些字段会让后续的告警规则写起来非常顺手。
采集方式:三种主流方案对比
采集日志的方式,我归纳下来就三类:
- Agent 模式 – 在每台机器上部署采集代理(如 Filebeat、Fluent Bit、Vector),监听日志文件变化,推送走。
- Sidecar 模式 – K8s 里每个 Pod 挂一个日志采集容器,跟业务容器共享 Volume。
- 直接推模式 – 业务代码通过 SDK 直接调用 API 把日志发到中央管道(比如 Kafka、HTTP Sink)。
我个人的习惯:老物理机用 Agent 模式,云原生环境用 Sidecar 模式,高吞吐实时监控场景用 直接推模式。你想想看,直接推模式延迟最低,但耦合度最高;Agent 模式最成熟,但文件轮转容易丢日志末尾。
| 对比维度 | Agent 模式 | Sidecar 模式 | 直接推模式 |
|---|---|---|---|
| 部署复杂度 | 中(需维护 agent) | 低(容器化) | 高(需改业务代码) |
| 性能影响 | 低(异步读文件) | 低(独立容器) | 中(同步/异步取决于实现) |
| 日志实时性 | 接近实时(秒级) | 接近实时(秒级) | 实时(毫秒级) |
| 常见工具 | Filebeat, Fluent Bit | Fluentd, Logstash | SDK 内部封装 |
我曾在生产环境碰到 Filebeat 因为日志文件轮转太快,导致老文件还没读完就被删除,丢了几百条关键日志。解决方案是给文件句柄加一个 close_inactive: 5m 的配置,并在轮转时保留至少一个副本。
异步处理:别让日志拖慢业务
日志采集最忌讳什么?阻塞业务线程。我曾经见过一个支付系统的日志 SDK 用了同步 BlockingQueue,结果队列满的时候线程挂起,RT 飙到 5 秒。那次线上事故持续了 20 分钟才定位到——教训太深刻。
异步处理的核心思路就两点:
- 生产者缓冲 – 业务线程只往内存队列写日志,不等待网络 IO。
- 消费者批量推送 – 后台线程攒一批日志(比如 100 条或 1 秒),打包发送到中央管道。
来个代码片段看看(伪代码风格,实际 SDK 实现类似):
// 日志采集异步引擎核心逻辑
class AsyncLogEngine {
private val buffer: RingBuffer<LogEvent> = RingBuffer(1024)
fun append(event: LogEvent) {
// 非阻塞写,满了就丢弃(可配置策略)
if (!buffer.offer(event)) {
stats.countDropped() // 采样告警
}
}
fun flushLoop() {
while (true) {
val batch = buffer.drain(100, 1000) // 最多 100 条,最长等 1s
if (batch.isNotEmpty()) {
sendBatch(batch) // 网络 IO 在其他线程
}
}
}
}
注意:千万别在 sendBatch 里加锁。缓冲区设计要考虑无锁化。我踩过的坑是用了 synchronized 保护批量发送,结果并发一高 CPU 全花在线程调度上。改用 Disruptor 的 RingBuffer 后性能提升 5 倍。
知识体系图:日志采集核心逻辑
下面这张图是整个日志采集管线的数据流转。我把四个关键阶段画出来了:
整条链路的精髓就是「松耦合 + 缓冲 + 批量」。业务侧只管往缓冲区写,采集侧只管批量刷出去。两边互不阻塞——这就是异步采集的核心思想。
实战避坑:我曾经遇到的两个典型问题
- 日志格式混用导致解析失败 – 团队里有人用了 log4j 的 PatternLayout,有人用了 Logback 的 JsonLayout,采集器解析时抛异常。后来我强制在 CI 阶段加了一个日志格式校验步骤,不合格直接构建失败。
- 异步队列背压导致 OOM – 生产环境突发流量,日志产生速度远大于发送速度,内存队列无限增长。解决方案:给队列设置固定大小,满了就用
dropHead策略丢弃最早日志,同时触发告警。
如果你用 Java,推荐 LMAX Disruptor 做 RingBuffer;用 Go 的话,Go 自带的 channel 加上有缓冲的 chan 基本够用。我一般会再加一层指标上报——监控队列填充率,一旦超过 80% 就要扩容发送线程或升级带宽。
好了,日志采集原理就聊到这。这几个点你记住:结构化格式、异步非阻塞、批量压缩发送。下回配置告警时会发现,数据干净了,写规则都舒服多了。