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[查询、告警、留存]
其中:
stdout和stderr是应用向容器运行时输出日志的默认路径;- 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. 容器日志
容器日志通常指容器运行时接收到的 stdout 和 stderr 数据,以及运行时或 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
字段含义如下:
- 第一个字段是时间戳;
stdout或stderr表示输出流;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 的多行堆栈语义。
因此,多行日志需要在某一层合并:
- 应用直接输出结构化单行 JSON;
- Node Agent 依据时间戳、正则或起始行规则合并;
- 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 通常有三种行为:
- 阻塞并在内存中缓存;
- 写入节点本地缓冲文件;
- 丢弃或采样。
三者都会产生代价:
- 内存缓存可能导致 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}
每一步成立的原因是:
emptyDir在 Pod 内提供共享目录;app和log-forwarder都挂载同一个卷;- 主容器向
/var/log/app/app.log追加内容; - Sidecar 读取该文件并写 stdout;
- 容器运行时将 Sidecar 的 stdout 作为容器日志;
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 字段,但不应仅凭 stdout 或 stderr 判断日志级别。真正的级别应来自结构化字段、日志格式或应用语义。
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; - 是否设置不必要的
hostPID、hostNetwork; - 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
但日志路径和运行时细节存在实现差异:
- Kubernetes 不保证所有发行版使用完全相同的节点目录;
- containerd、CRI-O 和历史 Docker 集成的日志文件格式与路径可能不同;
- 云厂商通常会预装日志 Agent,可能同时配置节点日志、控制平面日志和云端身份认证;
- kubelet 日志轮转参数、默认值和配置管理方式可能由发行版覆盖;
- 某些采集器对 CRI partial 日志、多行合并、容器重命名和轮转的支持程度不同;
- Sidecar 生命周期相关能力和行为可能受 Kubernetes 版本影响,不能把某个版本的实验性特性当作通用基础能力;
- 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 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes 指标与监控:Metrics Server、Prometheus、告警和 SLO
- 下一篇:Kubernetes 分布式追踪:OpenTelemetry、Context、采样和基础设施关联
- 延伸:DaemonSet:节点级 Agent、调度、更新、容忍和资源治理
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论