3、日志采集架构:Agent采集模式、无侵入式采集、日志传输管道设计

日志采集,是整个可观测性体系的「水源」。水源不干净,后面分析再牛也是白搭。我见过太多团队,花大价钱搭了ELK,结果采集端没搞好,日志丢了一半,或者把业务进程拖垮了。今天咱们就把采集架构这块掰开揉碎,讲清楚。

3.1 Agent采集模式:最主流,也最考验功底

Agent模式,说白了就是在每台机器上装一个采集进程。它负责读日志文件,然后发到后端。这是目前最成熟、最灵活的方式。

核心组件拆解

  • 文件监听器:监控日志文件变化,支持轮转(rotation)
  • 读取器:按行读取,支持断点续传(记录偏移量)
  • 缓冲区:内存或磁盘缓冲,防止突发流量打崩后端
  • 发送器:将日志批量压缩后发送

我个人习惯用 Filebeat 或 Fluent Bit。它们轻量,内存占用控制在 10-30MB 左右。你想想看,如果每个 Agent 吃掉 200MB 内存,1000 台机器就是 200GB,这成本谁扛得住?

关键指标:Agent 自身 CPU 占用不应超过单核的 5%,内存不超过 50MB。超过这个阈值,就得优化了。

断点续传怎么实现?

嗯,这里要注意。Agent 必须记录每个文件的 inode 和读取偏移量。我遇到过一个问题:日志轮转时,旧文件被重命名,新文件创建。如果 Agent 只认文件名,就会漏掉轮转间隙的日志。解决办法是:同时监控 inode 和文件名,轮转后立即打开新文件,并记录旧文件的最后位置。

# 伪代码示例:断点续传逻辑
def tail_file(path):
    inode = get_inode(path)
    offset = load_offset(inode)  # 从本地状态文件读取
    with open(path, 'r') as f:
        f.seek(offset)
        for line in f:
            send(line)
        save_offset(inode, f.tell())  # 实时保存

避坑指南:我曾经在生产环境遇到过 Agent 把磁盘写满的惨案。原因是缓冲区设置过大,又没有限制磁盘使用量。建议给 Agent 的磁盘缓冲设一个硬上限,比如 1GB,超过就丢弃旧数据或阻塞读取。

3.2 无侵入式采集:不动业务代码,也能拿到日志

有些场景,你没法装 Agent。比如容器环境、Serverless、或者第三方闭源软件。这时候就需要无侵入式采集。

常见手段:

  • 标准输出(stdout)劫持:容器环境下,应用把日志打到 stdout,由容器运行时(如 Docker)收集。这是 K8s 的默认做法。
  • 系统调用劫持:通过 LD_PRELOAD 或 eBPF 拦截 write() 系统调用,直接捕获日志内容。适合老旧系统。
  • 日志文件挂载:把宿主机的日志目录挂载到采集容器里,由采集容器读取。注意权限问题。

我个人比较推荐 eBPF 方案。它不需要改应用代码,也不需要重启进程。但 eBPF 也有坑——内核版本要求高,而且调试起来很痛苦。我记得有一次,eBPF 程序在内核 4.19 上跑得好好的,升级到 5.10 就挂了,查了两天才发现是 helper 函数接口变了。

警告:无侵入式采集虽然方便,但无法获取应用内部的上下文信息(比如用户 ID、请求追踪 ID)。如果你需要做全链路追踪,还是得靠 Agent 或 SDK 打点。

3.3 日志传输管道设计:别让数据堵在路上

采集完了,怎么把日志从几千台机器送到中心存储?这就是传输管道要解决的问题。管道设计不好,轻则丢日志,重则把网络打满。

管道架构分层:

层级 组件 职责
采集层 Agent(Filebeat/Fluent Bit) 本地采集、简单过滤、压缩
缓冲层 Kafka / Redis 削峰填谷,解耦生产与消费
消费层 Logstash / Fluentd 解析、转换、路由
存储层 Elasticsearch / ClickHouse 索引、存储、查询

为什么要加 Kafka?

你想想看,如果 Agent 直接往 Elasticsearch 写数据,ES 一旦慢(比如合并段、GC停顿),Agent 就会积压,然后 OOM。加一层 Kafka 之后,Agent 只管往 Kafka 写,Kafka 的吞吐量极高(单机几十万条/秒),ES 慢慢消费就行。说白了,Kafka 就是给管道加了一个「蓄水池」。

压缩与协议选择:

  • 传输前必须压缩。gzip 压缩比高,但耗 CPU;snappy/lz4 速度快,压缩比稍低。我建议用 lz4,平衡性好。
  • 协议上,优先用 gRPC 或 HTTP/2。它们支持流式传输,比 HTTP/1.1 的短连接效率高很多。
# Fluent Bit 配置示例:输出到 Kafka
[OUTPUT]
    Name          kafka
    Brokers       192.168.1.10:9092,192.168.1.11:9092
    Topics        app-logs
    Format        json
    rdkafka.compression.codec   lz4
    rdkafka.queue.buffering.max.messages  100000

避坑指南:我曾经遇到过一个诡异问题:Kafka 消费端偶尔会重复消费同一条日志。查了半天,发现是 Agent 发送后没等 Kafka 确认就认为发送成功,然后 Kafka 分区 leader 切换导致数据重平衡。解决办法:设置 acks=all,确保数据写入所有副本后才算成功。

背压与降级策略:

管道必须能处理下游故障。我常用的策略是:

  1. 本地磁盘缓冲:当 Kafka 不可用时,Agent 把日志暂存到本地磁盘。
  2. 指数退避重试:第一次重试等 1 秒,第二次 2 秒,第三次 4 秒……最大 60 秒。
  3. 最终丢弃:如果缓冲超过 1 小时,丢弃最旧的日志,保证 Agent 不崩溃。

嗯,这里要注意:降级策略一定要有监控告警。如果 Agent 开始丢日志了,你得第一时间知道,而不是等到排查问题时才发现数据不全。

总结一下:日志采集架构没有银弹。Agent 模式适合你能控制的机器,无侵入式适合容器和遗留系统,传输管道则要重点考虑缓冲和背压。把这三点搞扎实了,你的日志系统就成功了一半。