Kubernetes 基础体系 · 第 67/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 日志架构:stdout、Node Agent、Sidecar、采集和留存

Kubernetes 的日志系统不是一个单独的内置产品,而是一条由多个环节组成的数据路径:

flowchart LR
    A[应用进程] -->|stdout/stderr| B[容器运行时]
    A2[应用写入文件] --> C[共享卷或主机文件]
    B --> D[节点本地容器日志文件]
    D --> E[kubelet logs API]
    D --> F[Node Agent DaemonSet]
    C --> G[Sidecar]
    G --> B
    F --> H[日志后端]
    H --> I[查询、告警、留存]

其中:

  • stdoutstderr 是应用向容器运行时输出日志的默认路径;
  • Node Agent 是运行在节点上的日志采集代理,通常以 DaemonSet 部署;
  • Sidecar 是与业务容器共享 Pod 生命周期和资源边界的辅助容器,可用于转发文件日志;
  • 采集 是将节点上的日志读取、解析、补充 Kubernetes 元数据并发送到后端;
  • 留存 是日志在节点缓冲区、采集代理缓冲区和集中式后端中保存多长时间。

Kubernetes 负责容器日志的节点级承载、部分轮转以及 kubectl logs 访问接口,但不规定一个完整的集群级日志后端。生产环境中的 Elasticsearch、OpenSearch、Loki、云日志服务、Kafka 或其他系统,都属于 Kubernetes 之外的日志基础设施。


一、先区分三个对象:应用日志、容器日志和集中式日志

“日志”在 Kubernetes 中经常被混用,实际至少包含三个层次。

1. 应用日志

应用日志是程序产生的事件,例如:

2025-03-08T12:00:01Z INFO request finished method=GET path=/users/42 status=200 latency_ms=18

应用可以把它写到:

  • 标准输出 stdout
  • 标准错误 stderr
  • 容器文件系统中的文件;
  • 挂载卷中的文件;
  • 网络接口或专用日志库。

应用决定日志的格式和内容。Kubernetes 不会自动把应用写入任意文件的内容变成容器日志。

2. 容器日志

容器日志通常指容器运行时接收到的 stdoutstderr 数据,以及运行时或 kubelet 将这些数据落盘后的节点文件。

当执行:

kubectl logs pod-name -c app

请求的大致路径是:

kubectl
  -> Kubernetes API Server
  -> 运行 Pod 的 kubelet
  -> 节点上的容器日志文件或运行时接口
  -> 返回日志内容

它不是从 Elasticsearch、Loki 或其他集中式后端查询的。因此,kubectl logs 能否看到某条日志,与日志后端是否已经采集成功,是两件独立的事情。

3. 集中式日志

集中式日志是由 Node Agent 或 Sidecar 发送到集群外部或集群内部后端的数据。例如:

{
  "time": "2025-03-08T12:00:01.123Z",
  "level": "INFO",
  "service": "order-api",
  "trace_id": "abc123",
  "message": "order created"
}

集中式后端通常负责:

  • 索引或标签化;
  • 查询和聚合;
  • 访问控制;
  • 告警;
  • 压缩;
  • 分层存储;
  • 按时间或容量删除。

Kubernetes API 本身不提供上述完整能力。


二、stdout 为什么是 Kubernetes 中的默认日志路径

2.1 容器运行时负责接收 stdout 和 stderr

容器进程的标准输出和标准错误会被容器运行时接收。Kubernetes 通过 CRI(Container Runtime Interface)与运行时交互,常见运行时包括 containerd 和 CRI-O。

典型数据路径是:

应用进程
  -> stdout/stderr
  -> 容器运行时
  -> 节点上的容器日志文件
  -> kubelet
  -> kubectl logs 或 Node Agent

Kubernetes 的核心假设不是“应用必须使用某个日志库”,而是:

应用把面向运维的事件写入 stdout/stderr,运行时和节点组件负责承载这些输出。

因此,下面的应用代码能够直接被 kubectl logs 看到:

print('server started', flush=True)

而下面的内容不会自动出现在 kubectl logs 中:

with open('/var/log/app.log', 'a') as f:
    f.write('server started\n')

第二种写法只有在额外配置文件采集、Sidecar 或其他转发机制后,才可能进入集中式日志系统。

2.2 CRI 日志格式

Kubernetes 生态中常见的 CRI 日志文件每行包含时间戳、输出流和完整性标记,形式类似:

2025-03-08T12:00:01.123456789Z stdout F server started
2025-03-08T12:00:02.123456789Z stderr F connection failed

字段含义如下:

  • 第一个字段是时间戳;
  • stdoutstderr 表示输出流;
  • F 表示这一行是完整记录;
  • P 通常表示这一行只是一个被拆分日志记录的部分;
  • 最后是应用输出的内容。

具体文件名、目录结构和运行时内部实现属于节点实现细节,不应把某个运行时的路径硬编码为 Kubernetes API 保证。

常见节点路径包括:

/var/log/pods/
/var/log/containers/

/var/log/containers/ 中常见的是指向 Pod 日志文件的符号链接,/var/log/pods/ 通常包含按 Pod 组织的目录。具体布局可能随发行版、运行时和云厂商变化。

生产采集器通常优先支持 Kubernetes/CRI 语义,而不是只解析 Docker 的旧 json-file 格式。直接读取:

/var/lib/docker/containers/

属于运行时相关实现,不具备跨集群可移植性。

2.3 一行不一定等于一个应用事件

如果应用将一个 JSON 事件写成一行,采集最简单:

{"level":"INFO","message":"request finished","status":200}

但如果应用输出堆栈:

Traceback (most recent call last):
  File "server.py", line 10, in handle
    return call_backend()
RuntimeError: backend unavailable

采集器可能把它识别为多个日志事件。CRI 的 P/F 标记只能描述运行时对过长输出或记录片段的拆分,不能自动理解 Python、Java 或 Go 的多行堆栈语义。

因此,多行日志需要在某一层合并:

  1. 应用直接输出结构化单行 JSON;
  2. Node Agent 依据时间戳、正则或起始行规则合并;
  3. Sidecar 或日志库在写出前完成合并。

错误的多行配置会造成两类结果:

  • 一条堆栈被拆成许多条,查询困难;
  • 不相关的日志被错误合并,导致事件边界丢失。

三、kubelet、日志轮转和 kubectl logs

3.1 kubelet 在日志路径中的职责

kubelet 运行在每个节点上,负责与容器运行时配合管理 Pod。对于容器日志,它至少承担以下职责中的一部分:

  • 为 Kubernetes API 提供容器日志访问;
  • 根据 kubelet 配置执行容器日志轮转;
  • 将容器日志与 Pod、容器和重启状态关联。

kubectl logs 的访问对象不是一个永久日志仓库,而是 kubelet 当前能够提供的节点日志内容。

一个常见误解是:

kubectl logs 能显示的内容,就是这个容器产生过的全部日志。

这并不成立。尤其在发生日志轮转后,kubectl logs 的可见范围受 kubelet、运行时和当前日志文件状态影响;不能把它当作审计留存系统。历史日志应由集中式采集后端提供。

3.2 日志轮转解决的是节点空间问题,不是长期留存问题

日志轮转的目标是避免单个容器日志无限增长。例如,假设某容器平均输出速率为:

r = 2 MiB/s

如果节点只保留一个 100 MiB 日志文件,那么理论上:

T = 100 / 2 = 50 秒

日志可能很快覆盖或轮转。这里的 T 是节点上某一份本地日志的近似可见时间,不是集中式后端的留存时间。

kubelet 配置中常见的相关参数包括:

  • containerLogMaxSize:单个容器日志文件的最大大小;
  • containerLogMaxFiles:保留的日志文件数量。

这些参数属于 kubelet 配置,不是 Pod spec 中的通用字段。默认值和发行版配置可能不同,应检查实际 kubelet 配置,而不能假设所有集群一致。

可从节点上查看 kubelet 的启动参数或配置文件。例如,具体位置依发行版而异,排查时可以先查看进程参数:

ps -ef | grep '[k]ubelet'

如果集群使用 systemd,也可以查看:

systemctl cat kubelet

修改 kubelet 日志轮转参数会影响节点上所有容器,通常需要修改 kubelet 配置并谨慎滚动重启节点。不能只修改一个 Deployment 就改变其节点日志轮转策略。

3.3 不要让多个组件同时轮转同一文件

一个常见故障是:

kubelet 轮转容器日志
Node Agent 也直接轮转同一容器日志文件

两个组件同时重命名、截断或删除文件,可能造成:

  • 采集器丢失文件句柄;
  • 重复读取;
  • 轮转后的文件没有被发现;
  • offset 与 inode 对不上;
  • 日志出现缺口。

一般应让 kubelet 或容器运行时负责容器日志轮转,Node Agent 负责读取和发送。Node Agent 可以对自己的缓冲文件实施轮转,但不应擅自接管 Kubernetes 容器日志文件的生命周期,除非该采集器和节点方案明确要求这样做。


四、Node Agent:节点级采集的工作方式

4.1 Node Agent 的定义

Node Agent 是部署在每个节点上的日志采集进程。常见部署方式是 DaemonSet,即 Kubernetes 为每个符合条件的节点运行一个 Pod 副本。

典型 Node Agent 需要完成:

读取节点日志文件
  -> 识别 CRI 格式
  -> 解析 Pod/容器身份
  -> 调用 Kubernetes API 或读取元数据
  -> 添加 namespace、pod、container、node、labels
  -> 多行合并和过滤
  -> 批量发送到日志后端
  -> 保存读取位置和失败重试状态

Node Agent 与业务 Pod 的生命周期不同:

  • 业务 Pod 可以频繁创建和销毁;
  • Node Agent 通常伴随节点运行;
  • Agent 需要能够发现新 Pod;
  • Agent 需要处理容器重启、Pod 重建和日志文件轮转。

4.2 DaemonSet 的调度与容忍

一个简化的 Node Agent DaemonSet 结构如下:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: log-agent
  namespace: observability
spec:
  selector:
    matchLabels:
      app: log-agent
  template:
    metadata:
      labels:
        app: log-agent
    spec:
      tolerations:
        - operator: Exists
      containers:
        - name: agent
          image: example/log-agent:1.0
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 512Mi
          volumeMounts:
            - name: varlog
              mountPath: /var/log
              readOnly: true
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
            type: Directory

这个示例只展示 Kubernetes 部署结构,镜像中的采集器配置和后端地址仍需由具体产品提供。

关键点如下。

DaemonSet 不是“无条件每个节点都有”

DaemonSet 只会为符合调度条件的节点创建 Pod。以下因素都会改变覆盖范围:

  • 节点污点和 Pod 容忍;
  • nodeSelector
  • 节点亲和性;
  • 架构限制;
  • 资源不足;
  • 节点尚未 Ready;
  • Agent 镜像无法拉取。

如果日志 Agent 需要覆盖带有控制平面污点的节点,通常需要配置相应 tolerations。但 operator: Exists 会容忍所有污点,覆盖面大、隔离性弱,生产中应根据节点角色谨慎设置。

更新是 DaemonSet 的节点级滚动更新

更新 Agent 镜像时,DaemonSet 控制器会按更新策略替换各节点上的旧 Pod。要注意:

  • Agent 重启期间可能存在采集间隙;
  • Pod 删除前是否能 flush 缓冲,取决于 Agent 和终止时间配置;
  • 节点资源紧张时,Agent 更新可能被调度阻塞;
  • 采集器若使用本地 offset 文件,必须把状态保存到合适的持久位置,否则重启后可能从头读取或跳过日志。

DaemonSet 的更新成功,只表示 Pod 达到了期望状态,不表示日志已经成功到达后端。

资源治理必须包括日志流量

日志 Agent 的 CPU 和内存消耗与以下因素相关:

  • 日志输入速率;
  • JSON 解析和正则处理;
  • 多行合并缓存;
  • Kubernetes 元数据缓存;
  • 压缩和序列化;
  • 后端故障时的本地缓冲;
  • 标签基数。

如果只给 Agent 设置很小的内存限制,后端短暂不可用时,Agent 可能因为缓冲增长被 OOMKilled。反过来,如果没有限制,日志突发也可能挤占业务 Pod 的节点资源。

4.3 Node Agent 如何识别一条日志属于哪个 Pod

采集器通常从文件路径、符号链接或 CRI 元数据中得到:

namespace
pod name
pod UID
container name
container restart count
node name

之后再补充 Pod labels、annotations 等 Kubernetes 元数据。

这里存在一个重要的身份区别:

  • Pod 名称可能被删除后再次复用;
  • Pod UID 能区分不同生命周期的 Pod;
  • 容器重启可能改变容器日志文件;
  • 容器名称相同不代表是同一个容器实例。

因此,offset 和文件身份不能只用:

namespace/pod/container

作为唯一键。更稳妥的实现会结合 inode、文件路径、Pod UID、容器 ID 或其他运行时身份。

4.4 读取 offset 和“至少一次”语义

假设 Agent 读取日志文件到位置 1000,然后发生以下顺序:

1. Agent 读到字节 1000
2. Agent 将数据发送到后端
3. 后端已写入,但 Agent 尚未保存 offset
4. Agent 崩溃并重启
5. Agent 从旧 offset 重新读取

结果是后端可能出现重复日志。这是典型的至少一次采集语义。

如果顺序改为:

1. Agent 读到字节 1000
2. Agent 先保存 offset
3. Agent 尚未成功发送到后端
4. Agent 崩溃
5. 重启后从 1000 之后开始读

则可能出现日志丢失。

因此,在普通文件采集、远端 HTTP 或消息队列发送的组合下,通常只能在“重复”和“丢失”之间进行设计取舍。严格的端到端恰好一次语义需要后端幂等键、事务性 offset 提交和一致性协议,不能仅靠 DaemonSet 或 Kubernetes API 自动获得。


五、Node Agent 的故障路径

日志系统的可靠性必须按故障路径分析,而不能只看“Agent Pod 是 Running”。

5.1 Agent 未运行

如果节点没有 Agent:

应用 -> 节点本地日志文件

应用日志仍可能暂时存在,但不会被集中采集。若节点本地日志继续轮转,Agent 恢复后可能已经无法读取早期内容。

需要监控:

  • DaemonSet Desired、Current、Ready 数量;
  • 每个节点是否有 Agent;
  • Agent 重启次数;
  • Agent 采集输入速率;
  • Agent 发送成功率;
  • Agent backlog 或 buffer 使用量。

5.2 后端不可用

后端不可用时,Agent 通常有三种行为:

  1. 阻塞并在内存中缓存;
  2. 写入节点本地缓冲文件;
  3. 丢弃或采样。

三者都会产生代价:

  • 内存缓存可能导致 OOM;
  • 本地缓冲会消耗节点磁盘;
  • 丢弃会造成日志缺口。

假设日志输入速率为 r,允许后端不可用的时间为 T,仅从容量角度看,本地缓冲至少需要:

C >= r × T

其中:

  • C 是缓冲容量;
  • r 是单位时间日志量;
  • T 是希望覆盖的后端故障时长。

如果有压缩比、突发倍率和安全余量,可以写成:

C >= r_peak × T × safety_factor

例如:

r_peak = 20 MiB/s
T = 10 分钟 = 600 秒
safety_factor = 1.5
C >= 20 × 600 × 1.5 = 18,000 MiB

这还没有计算文件系统、其他 Pod 的 ephemeral storage 和 Agent 自身开销。这个算例说明,日志缓冲不是“随便开一点磁盘”就能完成的功能。

5.3 节点磁盘压力

容器日志文件、容器可写层、EmptyDir、Agent 缓冲都可能使用节点本地存储。节点进入 DiskPressure 后,kubelet 可能驱逐 Pod。

此时会形成反馈回路:

日志量增加
  -> 节点日志文件增大
  -> 节点磁盘压力
  -> Pod 被驱逐或重启
  -> 应用产生更多启动/错误日志
  -> 磁盘压力进一步增加

因此,日志系统本身也属于节点资源治理对象。应结合:

  • kubelet eviction 配置;
  • 容器和 Pod 的 ephemeral-storage requests/limits;
  • Agent 的磁盘缓冲上限;
  • 日志采样和限流;
  • 节点磁盘监控。

5.4 文件轮转和采集间隙

轮转时,Agent 必须正确处理:

  • 旧文件被重命名;
  • 新文件被创建;
  • 文件 inode 变化;
  • 旧文件仍被写入或等待发送;
  • Pod 删除后文件被清理。

如果 Agent 只按文件名记录位置,可能把新文件误认为旧文件,或者跳过新文件。生产采集器通常会用 inode、文件指纹或容器 ID 辅助判断,但具体算法属于采集器实现。


六、Sidecar:什么时候需要它,为什么它不是默认方案

6.1 Sidecar 的定义

Sidecar 是与主业务容器处于同一个 Pod 的辅助容器。它们共享:

  • Pod 的网络命名空间;
  • Pod 的生命周期;
  • Pod 的调度位置;
  • 可配置的卷;
  • Pod 资源边界。

Sidecar 不是 Kubernetes 专门为日志定义的 API 类型。普通多容器 Pod 就可以实现日志 Sidecar。

常见日志 Sidecar 模式是:

业务容器写共享卷文件
  -> Sidecar 读取文件
  -> Sidecar 输出 stdout
  -> 容器运行时和 Node Agent 继续处理

这样可以把传统文件日志转换为容器标准输出。

6.2 一个可运行的 Sidecar 示例

下面的 Pod 会让主容器写入共享卷,Sidecar 使用 tail -F 读取该文件并写到自己的 stdout:

apiVersion: v1
kind: Pod
metadata:
  name: file-log-sidecar
spec:
  containers:
    - name: app
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          i=0
          while true; do
            i=$((i + 1))
            printf '{"level":"INFO","event":"tick","count":%s}\n' "$i" >> /var/log/app/app.log
            sleep 2
          done
      volumeMounts:
        - name: app-log
          mountPath: /var/log/app

    - name: log-forwarder
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          touch /var/log/app/app.log
          exec tail -F /var/log/app/app.log
      volumeMounts:
        - name: app-log
          mountPath: /var/log/app

  volumes:
    - name: app-log
      emptyDir: {}

创建并查看:

kubectl apply -f file-log-sidecar.yaml
kubectl logs file-log-sidecar -c log-forwarder -f

预期能够看到:

{"level":"INFO","event":"tick","count":1}
{"level":"INFO","event":"tick","count":2}

每一步成立的原因是:

  1. emptyDir 在 Pod 内提供共享目录;
  2. applog-forwarder 都挂载同一个卷;
  3. 主容器向 /var/log/app/app.log 追加内容;
  4. Sidecar 读取该文件并写 stdout;
  5. 容器运行时将 Sidecar 的 stdout 作为容器日志;
  6. kubectl logs -c log-forwarder 读取 Sidecar 的容器日志。

这个例子适合说明数据路径,但不应直接作为生产日志转发器。tail 进程缺少完整的 offset 管理、后端重试、结构化解析、权限控制和可靠的轮转协调。

6.3 Sidecar 的资源和故障含义

Pod 中任意一个关键容器异常,都可能影响 Pod 的整体可用性。Sidecar 会带来额外资源:

Pod 总资源需求
  = 业务容器资源
  + Sidecar 资源
  + 共享卷和缓冲开销

如果 Sidecar 被 OOMKilled、配置错误或卡在文件读取上,可能出现:

  • 日志停止转发,但业务仍然正常;
  • Pod 被判定为不 Ready;
  • Sidecar 重启,造成重复或丢失;
  • 共享卷占满 Pod 的临时存储;
  • 每个业务 Pod 都运行一个日志代理,导致代理数量过多。

Sidecar 的最大优点是隔离性强:某个应用可以使用专有解析规则、专有传输协议或敏感数据脱敏逻辑。最大代价是资源和运维复杂度随 Pod 数量线性增加。

6.4 Sidecar 与 Node Agent 的选择

可以按数据来源区分:

场景 更常见的方案
应用能够改为 stdout 输出 直接 stdout + Node Agent
多个应用统一采集 CRI 日志 Node Agent
遗留程序只能写文件 共享卷 + Sidecar,或节点文件采集
某个应用需要特殊解析/脱敏 Sidecar 或应用内日志库
日志文件只应被该 Pod 看见 Sidecar + emptyDir
需要减少每个 Pod 的额外开销 Node Agent
需要避免节点级 Agent 读取敏感文件 Sidecar

如果应用已经把日志写入 Pod 内的挂载卷,Node Agent 不能自动读取所有此类文件,因为这些文件未必出现在节点标准容器日志目录中。此时必须明确设计文件采集路径。


七、stdout 设计的边界和常见误解

7.1 stdout 不是无限可靠的消息队列

stdout 只是进程输出流。它经过容器运行时、节点文件系统、kubelet 和采集器后,才可能成为集中式日志。

以下情况都可能造成日志不可见:

  • 应用进程在缓冲区尚未 flush 时崩溃;
  • 容器运行时异常;
  • 节点磁盘已满;
  • kubelet 或 Node Agent 停止;
  • 采集器解析失败;
  • 网络或日志后端不可用;
  • 日志轮转后采集器没有正确跟踪文件;
  • 日志后端主动采样或限流。

因此,不能因为“应用写 stdout”就推导出“日志一定永久存在”。

7.2 stderr 不等于错误级别

stderr 是一个输出流,不是日志级别。应用可以把 INFO 写到 stderr,也可以把错误写到 stdout。

例如:

echo "business error" >&1
echo "normal diagnostic" >&2

采集器通常会保留原始 stream 字段,但不应仅凭 stdoutstderr 判断日志级别。真正的级别应来自结构化字段、日志格式或应用语义。

7.3 kubectl logs 的常用排查命令

查看单容器 Pod:

kubectl logs pod-name

查看多容器 Pod 中指定容器:

kubectl logs pod-name -c app

跟随当前输出:

kubectl logs -f pod-name -c app

查看上一次已终止容器实例的日志:

kubectl logs -p pod-name -c app

查看带时间戳的输出:

kubectl logs pod-name -c app --timestamps

查看最近十分钟:

kubectl logs pod-name -c app --since=10m

这些命令的含义分别依赖 Pod 当前状态、容器重启记录和 kubelet 可访问的日志文件。-p 只能用于仍由 kubelet/运行时保留的上一个容器实例,不能替代集中式留存。

排查时还应同时执行:

kubectl describe pod pod-name
kubectl get events --sort-by=.lastTimestamp

因为“没有日志”可能是:

  • 容器根本没有启动;
  • 镜像拉取失败;
  • 容器被 OOMKilled;
  • 应用写的是文件而不是 stdout;
  • 容器已重启,当前日志不是上一次日志;
  • Agent 没有采集,而不是应用没有输出。

八、结构化日志、元数据和索引成本

8.1 推荐的单行结构化事件

一个适合集中采集的事件至少应明确时间、级别、消息和请求关联信息:

{
  "timestamp": "2025-03-08T12:00:01.123Z",
  "level": "INFO",
  "service": "order-api",
  "message": "order created",
  "request_id": "req-123",
  "trace_id": "trace-456",
  "order_id": "order-789"
}

时间字段应统一时区,通常使用 UTC。采集器收到日志后可能同时存在:

  • CRI 外层时间戳;
  • JSON 内层业务时间戳;
  • 后端接收时间;
  • 索引时间。

需要明确查询和排序使用哪一个时间。若应用时钟漂移,内层时间可能早于或晚于接收时间;因此不能假设所有时间戳天然一致。

8.2 Kubernetes 元数据不是日志原文

Node Agent 往往为事件附加:

namespace
pod
pod_uid
container
node
labels
annotations

这些字段通常来自文件路径、Kubernetes API 缓存或 Agent 的元数据解析,而不是应用自身输出。

Pod 被删除后,后端仍可能保存带有原 Pod 名称和 UID 的日志。查询系统中的历史日志时,应使用 pod_uid 作为生命周期区分依据,不能只按 Pod 名称判断“这是同一个实例”。

8.3 标签和字段会影响后端成本

将高基数字段作为索引标签会增加后端开销。例如:

trace_id
request_id
user_id
order_id

这些字段适合作为结构化字段或可检索内容,但未必适合作为 Loki label、时序标签或高频索引字段。

日志内容设计与后端模型必须一起考虑:

  • 标签适合低基数过滤;
  • 字段适合详细查询;
  • 大对象、请求体和堆栈会增加存储和传输成本;
  • 敏感字段应在应用或采集器阶段脱敏。

九、采集与留存不是同一个问题

9.1 三层存储模型

一个实际日志系统通常包含三层:

节点本地日志
  -> Agent 本地队列或缓冲
  -> 集中式后端

每层的职责不同:

主要目的 典型失效方式
节点本地日志 支持 kubectl logs、短期缓冲 轮转、节点损坏、磁盘压力
Agent 缓冲 应对短暂网络或后端故障 磁盘满、Agent 重启、队列溢出
集中式后端 查询、告警、长期保存 索引故障、容量不足、策略删除

“采集成功”通常表示日志已经被 Agent 读取并发送,未必表示已经完成持久化。后端返回 HTTP 成功、消息队列确认或索引写入确认的语义也可能不同,必须查看具体产品定义。

9.2 留存时间的容量推导

设:

  • R:平均日志速率,单位为 GiB/天;
  • D:目标留存天数;
  • K:索引、元数据、副本和压缩后的综合系数。

基础存储需求近似为:

S >= R × D × K

例如,某系统每天产生 200 GiB 原始日志,目标留存 30 天,后端副本和索引综合系数按 1.8 估算,则:

S >= 200 × 30 × 1.8
S >= 10,800 GiB

这只是容量估算,不是容量承诺。实际还需要加入:

  • 峰值而非平均速率;
  • 重建和迁移空间;
  • 热、温、冷层的不同副本策略;
  • 压缩效果;
  • 突发故障期间的积压;
  • 业务增长;
  • 合规保留和删除延迟。

9.3 留存策略应按日志类型区分

不同日志的价值和风险不同:

  • 审计日志通常需要更长留存、更严格防篡改和访问控制;
  • 应用访问日志适合用于排障和统计;
  • Debug 日志可能只在短期启用;
  • 含个人信息的日志可能必须脱敏或缩短保存时间;
  • 高体积请求体不应默认永久保存。

留存策略通常由后端 ILM、对象存储生命周期、索引策略或云服务日志策略实现,而不是由 Kubernetes Deployment 的 spec 实现。


十、Sidecar 文件采集与 Node Agent 文件采集的故障差异

假设应用写入文件,有两种主要设计。

方案一:Node Agent 直接读取节点上的文件

应用
  -> Pod 挂载卷
  -> 节点映射出的文件
  -> Node Agent
  -> 后端

优点:

  • 不需要每个 Pod 运行一个 Sidecar;
  • 采集器可以统一限流、解析和发送;
  • 资源开销集中,易于统一升级。

风险:

  • Agent 必须准确知道卷在节点上的实际路径;
  • HostPath 权限和安全边界复杂;
  • 文件路径可能随运行时、CSI 驱动和发行版变化;
  • Pod 删除后文件生命周期可能不符合预期;
  • 采集器可能需要读取超出业务 Pod 预期的主机文件。

方案二:Sidecar 读取共享卷并输出 stdout

应用
  -> emptyDir/PVC
  -> Sidecar
  -> Sidecar stdout
  -> Node Agent
  -> 后端

优点:

  • Sidecar 只访问本 Pod 共享的文件;
  • 应用级解析和脱敏更容易隔离;
  • 继续使用 Kubernetes 标准容器日志路径。

风险:

  • 每个 Pod 增加进程和资源;
  • Sidecar 自身可能失败;
  • 文件读取和 stdout 转发之间可能重复或丢失;
  • 主容器与 Sidecar 的关闭顺序需要处理;
  • 共享卷可能占满 Pod 的本地或持久化空间。

Sidecar 不是 Node Agent 的替代品。Sidecar 更适合解决单个应用的特殊日志接口,Node Agent 更适合做节点范围的统一采集。


十一、权限、安全和数据泄露边界

11.1 HostPath 是高权限边界

Node Agent 常需要挂载:

- name: varlog
  hostPath:
    path: /var/log

这意味着 Agent Pod 可以读取节点上的大量日志。若日志目录中存在其他系统服务、认证组件或敏感信息,Agent 的容器突破风险会扩大影响范围。

应重点审查:

  • 是否只读挂载;
  • 是否需要挂载整个 /var/log
  • 是否需要 privileged: true
  • 是否设置不必要的 hostPIDhostNetwork
  • ServiceAccount 是否拥有过大的 API 权限;
  • 后端凭据是否通过 Secret 注入;
  • 日志传输是否使用 TLS;
  • 日志查询端是否进行租户隔离。

11.2 日志本身可能是敏感数据

密码、Token、Cookie、身份证件、支付信息和完整请求体都可能意外进入日志。集中式采集会扩大这些数据的复制范围。

脱敏的时机越晚,泄露范围越大:

应用生成敏感日志
  -> 节点文件已保存
  -> Agent 读取
  -> Agent 脱敏
  -> 后端保存

此时节点上和 Agent 缓冲中仍可能存在原始数据。更安全的顺序是应用在输出前避免记录敏感内容;如果必须处理,则在最接近数据源的位置脱敏。


十二、生产诊断:从“应用没日志”反推故障层

遇到日志问题时,不要直接从后端查询失败推断应用没有输出。可以按数据路径逐层验证。

第一步:确认容器是否实际运行

kubectl get pod pod-name -o wide
kubectl describe pod pod-name

重点看:

  • Waiting 原因;
  • Terminated 原因;
  • Exit Code
  • 是否 OOMKilled
  • 重启次数;
  • 事件中的挂载、探针和镜像错误。

第二步:确认 stdout 是否有输出

kubectl logs pod-name -c app --timestamps
kubectl logs pod-name -c app -p --timestamps

如果当前日志为空,再确认:

  • 应用是否把内容写到文件;
  • 容器是否刚刚重启;
  • 是否查看了正确的容器;
  • 应用是否有输出缓冲;
  • 日志是否被写到了错误的 stream。

第三步:确认 Node Agent 覆盖节点

kubectl get pod pod-name -o wide
kubectl -n observability get daemonset log-agent
kubectl -n observability get pods -o wide

检查运行该业务 Pod 的节点上是否存在 Ready 的 Agent,而不是只看 Agent 总数。

第四步:确认 Agent 是否读取和发送

具体指标取决于 Agent,但通常应检查:

  • input records;
  • parse errors;
  • bytes read;
  • bytes sent;
  • retry count;
  • queue size;
  • dropped records;
  • last successful flush;
  • metadata lookup errors。

如果节点本地能看到日志,而后端看不到,问题通常位于:

Agent 发现
  -> 解析
  -> 元数据
  -> 队列
  -> 网络
  -> 后端接收

第五步:确认后端查询条件

常见查询错误包括:

  • 使用了错误 namespace;
  • 只按 Pod 名称查询,但 Pod 已重建;
  • 时间范围使用后端接收时间而不是应用时间;
  • 标签字段被解析成普通文本;
  • 查询被采样、限流或权限过滤;
  • 多行日志在采集时被合并。

十三、版本和实现差异

本文示例使用稳定的 Kubernetes API,例如:

apiVersion: apps/v1
kind: DaemonSet

但日志路径和运行时细节存在实现差异:

  1. Kubernetes 不保证所有发行版使用完全相同的节点目录;
  2. containerd、CRI-O 和历史 Docker 集成的日志文件格式与路径可能不同;
  3. 云厂商通常会预装日志 Agent,可能同时配置节点日志、控制平面日志和云端身份认证;
  4. kubelet 日志轮转参数、默认值和配置管理方式可能由发行版覆盖;
  5. 某些采集器对 CRI partial 日志、多行合并、容器重命名和轮转的支持程度不同;
  6. Sidecar 生命周期相关能力和行为可能受 Kubernetes 版本影响,不能把某个版本的实验性特性当作通用基础能力;
  7. Pod 日志 API 的可见范围受 kubelet 和运行时实现影响,不应被当成长期存储接口。

部署前应检查:

kubectl version
kubectl get nodes -o wide

并在实际节点上确认:

  • 容器运行时;
  • /var/log/pods/var/log/containers 是否存在;
  • kubelet 日志轮转配置;
  • 云厂商是否已经运行其他采集器;
  • 是否存在重复采集和重复发送。

如果节点上已经有云厂商 Agent,再部署一个自定义 DaemonSet 直接读取相同文件,可能产生重复日志、重复计费、后端重复索引和节点资源争抢。


十四、一个完整的架构判断

可以用下面的因果链判断一条日志是否最终可查询:

应用产生事件
  -> 事件被写入 stdout/stderr 或可采集文件
  -> 容器运行时接收并落盘或暴露
  -> kubelet/Node Agent 能发现该数据
  -> Agent 能正确解析边界和元数据
  -> Agent 有足够资源和缓冲
  -> 网络与认证正常
  -> 后端确认写入
  -> 后端留存策略尚未删除
  -> 查询条件命中

其中任一环节失败,最终都可能表现为“日志不存在”,但修复方法完全不同:

  • 应用没有输出:修应用日志配置;
  • kubectl logs 有、后端没有:修采集或发送;
  • 后端有、查询不到:修字段、时间范围或权限;
  • 过一段时间消失:检查节点轮转或后端留存;
  • 节点磁盘被打满:检查日志速率、轮转和缓冲上限;
  • 每条日志重复:检查 Agent offset 提交和重复部署;
  • 堆栈被拆散:修多行合并或改为单行结构化输出。

最终,Kubernetes 日志架构的核心不是“把日志放到 stdout”这一条建议,而是明确每个阶段的责任边界:

应用负责产生可解析、不过度泄露的事件;
容器运行时负责承载 stdout/stderr;
kubelet 负责节点级管理和日志访问;
Node Agent 负责发现、解析、补充元数据和发送;
Sidecar 负责特殊文件日志或应用级隔离;
日志后端负责查询、告警、持久化和留存策略。

只有把这些组件、状态和故障路径分别验证,才能区分“日志没有产生”“日志没有被采集”和“日志已经被删除”这三类完全不同的问题。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。