4、日志采集架构:Agent 模式、无 Agent 模式(Sidecar)、日志采集的可靠性设计

日志采集,说白了就是解决一个问题:日志怎么从应用里出来,安全可靠地送到存储系统。

我做了这么多年运维,见过太多因为采集环节出问题导致故障排查无门的案例。有一次线上服务挂了,查了半天才发现是日志采集器把磁盘打满了,应用自己写不了日志,直接 OOM。嗯,这种坑踩过一次就记住了。

今天咱们聊聊三种主流的采集架构,以及怎么保证日志不丢、不重、不乱。

4.1 Agent 模式:最经典的做法

Agent 模式,就是每台机器上部署一个采集进程,由它来负责这台机器上所有应用的日志采集。

举个例子,你有一台物理机,上面跑了 10 个 Java 应用。你在这台机器上装一个 Filebeat 或 Logstash,它去读这 10 个应用的日志文件,然后统一发到 Kafka 或 ES。

这种模式的好处很明显:

  • 资源开销小:一台机器只跑一个采集进程,CPU 和内存占用可控
  • 管理简单:你只需要维护一个 Agent 的配置,不用关心每个应用
  • 成熟稳定:Filebeat、Fluentd 这些工具已经经过大量验证

但缺点也很突出:

  • 隔离性差:一个应用把日志打得太快,可能会拖慢其他应用的采集速度
  • 权限问题:Agent 需要有读取所有应用日志的权限,安全上是个隐患
  • 配置耦合:所有应用的采集规则都写在一个配置文件里,改起来容易误伤

适用场景:传统虚拟机部署、物理机部署,日志量不大(单机 < 100MB/s)的场景。

4.2 无 Agent 模式(Sidecar):容器时代的标配

到了 Kubernetes 时代,Agent 模式就不太够用了。为什么?因为 Pod 是动态的,你没法保证每台 Node 上的 Agent 配置能跟上 Pod 的变化。

Sidecar 模式的做法是:每个 Pod 里除了业务容器,再跑一个日志采集容器。这个 Sidecar 只负责采集本 Pod 的日志。

我个人习惯用这种方式。你想想看,每个 Pod 的日志格式、采集频率、目标存储可能都不一样。用 Sidecar 的话,配置跟着 Pod 走,完全解耦。

# 一个典型的 Sidecar 配置示例
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  - name: log-collector
    image: fluentd:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
    env:
    - name: FLUENTD_TARGET
      value: "kafka:9092"
  volumes:
  - name: logs
    emptyDir: {}

Sidecar 模式的优势:

  • 完全隔离:一个 Pod 的日志采集出问题,不影响其他 Pod
  • 配置灵活:每个 Pod 可以有自己的采集规则、过滤逻辑
  • 资源可控:你可以给 Sidecar 单独设置资源限制,避免它抢业务容器的资源

但代价也很明显:

  • 资源开销翻倍:每个 Pod 多一个容器,1000 个 Pod 就多 1000 个采集进程
  • 管理复杂度上升:你需要维护每个 Pod 的 Sidecar 配置,虽然可以用模板,但总归多了工作量

我的建议:如果你的集群规模在 500 个 Pod 以内,用 Sidecar 完全没问题。超过这个量,可以考虑混合模式——核心服务用 Sidecar,普通服务用 Node 级别的 DaemonSet Agent。

4.3 两种模式的对比

维度 Agent 模式 Sidecar 模式
资源开销 低(单机一个进程) 高(每个 Pod 一个进程)
隔离性 差(共享采集进程) 好(独立采集进程)
配置灵活性 低(全局配置) 高(按 Pod 配置)
管理复杂度 低 高
适用场景 传统部署、小规模 Kubernetes、大规模

4.4 日志采集的可靠性设计

这部分是重点。我见过太多系统,采集链路一断,日志全丢,查问题的时候两眼一抹黑。

可靠性设计,核心就三个问题:

  1. 怎么保证不丢日志?
  2. 怎么保证不重复?
  3. 怎么保证顺序不乱?

4.4.1 不丢日志:至少一次语义

最常用的策略是「至少一次」(At-Least-Once)。说白了就是:日志只要被采集了,就一定送到目的地,哪怕多送一次。

实现方式:

  • 本地缓存:采集器先把日志写到本地磁盘,确认发送成功后再删除。如果发送失败,下次重试。
  • 确认机制:采集器发送日志后,必须等存储端返回 ACK,才算成功。
  • 重试机制:发送失败时,指数退避重试,直到成功或达到最大重试次数。
# 伪代码:带本地缓存的采集逻辑
def collect_and_send():
    while True:
        log = read_from_file()
        # 先写本地缓存
        write_to_local_cache(log)
        # 尝试发送
        success = send_to_kafka(log)
        if success:
            # 收到 ACK 后删除缓存
            delete_from_local_cache(log)
        else:
            # 重试,指数退避
            retry_with_backoff(log)

注意:本地缓存一定要控制大小。我曾经遇到过缓存写满磁盘,导致整个系统崩溃的情况。建议设置磁盘使用率阈值,超过 80% 就告警。

4.4.2 不重复日志:去重机制

「至少一次」语义的代价就是可能产生重复日志。怎么去重?

常用的做法是给每条日志加一个唯一 ID。这个 ID 通常由「机器 ID + 进程 ID + 时间戳 + 自增序号」组成。

存储端收到日志后,根据这个 ID 做去重。比如 Kafka 的幂等生产者,或者 ES 的文档 ID 去重。

# 生成唯一日志 ID
import uuid
import time

def generate_log_id():
    # 使用 UUID 保证全局唯一
    return str(uuid.uuid4())

# 或者用组合方式
def generate_log_id_v2(hostname, pid):
    timestamp = int(time.time() * 1000000)
    seq = get_sequence_number()
    return f"{hostname}-{pid}-{timestamp}-{seq}"

4.4.3 顺序不乱:分区有序

日志的顺序问题,说白了就是:同一个业务操作的日志,必须按时间顺序排列。

解决方案:

  • 按 Key 分区:比如按「订单 ID」或「用户 ID」做 Hash,保证同一个 Key 的日志进入同一个分区
  • 单线程写入:同一个分区的日志,由同一个消费者线程处理,保证顺序
  • 时间戳排序:如果实在无法保证顺序,可以在消费端按时间戳重新排序

避坑指南:我曾经遇到过一个案例,日志采集器用了多线程发送,结果同一个请求的日志被打散到不同分区,查问题的时候顺序全乱了。后来改成按请求 ID 分区,问题解决。

4.5 架构图:完整的采集链路

下面这张图展示了从日志产生到存储的完整链路:

应用 日志文件 采集器 本地缓存 Kafka ES / 对象存储 去重模块 应用/存储 采集器 消息队列 缓存/去重

4.6 总结

日志采集架构的选择,没有银弹。我的经验是:

  • 小规模、传统部署:用 Agent 模式,省心省力
  • Kubernetes 环境:核心服务用 Sidecar,普通服务用 DaemonSet
  • 可靠性设计:本地缓存 + 确认机制 + 去重 ID,这三板斧基本够用

最后说一句:日志采集是日志系统的第一道防线,这里出问题,后面再好的分析工具都是白搭。所以,花时间把采集架构设计好,值。