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 日志采集的可靠性设计
这部分是重点。我见过太多系统,采集链路一断,日志全丢,查问题的时候两眼一抹黑。
可靠性设计,核心就三个问题:
- 怎么保证不丢日志?
- 怎么保证不重复?
- 怎么保证顺序不乱?
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 架构图:完整的采集链路
下面这张图展示了从日志产生到存储的完整链路:
4.6 总结
日志采集架构的选择,没有银弹。我的经验是:
- 小规模、传统部署:用 Agent 模式,省心省力
- Kubernetes 环境:核心服务用 Sidecar,普通服务用 DaemonSet
- 可靠性设计:本地缓存 + 确认机制 + 去重 ID,这三板斧基本够用
最后说一句:日志采集是日志系统的第一道防线,这里出问题,后面再好的分析工具都是白搭。所以,花时间把采集架构设计好,值。