第一章:云原生日志体系概述

大家好,我是你们的老朋友。今天咱们聊聊云原生日志体系。

说实话,我刚开始接触Kubernetes时,第一反应就是:日志怎么这么难搞?以前在虚拟机里tail -f跑得飞起,到了K8s里全乱套了。嗯,这背后其实有深层原因。

为什么传统日志收集在K8s中失效?

先说说传统方式。以前我们部署应用,日志通常写在本地磁盘上。比如Java应用写个/var/log/app.log,然后Logstash或者Filebeat去采集。这套模式在物理机或虚拟机时代跑得好好的。

但到了K8s里,问题来了——

  • Pod是临时性的:Pod随时可能被调度到其他节点。你刚把日志写到本地,Pod挂了,日志也跟着没了。我在项目中遇到过,一个关键Pod被OOM Kill,日志全丢,排查问题花了整整两天。
  • 容器文件系统不持久:容器重启后,文件系统重置。你想想看,如果日志写在容器内部,重启一次就清空一次,这谁受得了?
  • 多副本日志分散:一个Deployment有10个副本,日志分布在10个节点上。你手动去每个节点找?不现实。
  • 日志格式不统一:有的应用输出JSON,有的输出纯文本,有的带时间戳有的不带。采集端解析起来非常痛苦。
注意:千万别把日志写到容器内部文件系统。我曾经见过一个团队,所有日志都写在/tmp下,结果Pod一重启,日志全没了,线上问题根本没法追溯。

说白了,K8s的核心理念是「不可变基础设施」。容器应该被视为一次性消费品,而不是长期运行的服务器。日志收集方式必须跟着这个理念走。

云原生日志三大标准

那怎么办?社区总结出了三种主流方案。我按推荐程度排个序:

方案 适用场景 复杂度 资源消耗
标准输出 12-Factor App、无状态服务
Sidecar 需要日志预处理、格式转换
DaemonSet 节点级日志采集、遗留应用 低(共享)

标准输出(Stdout/Stderr)

这是最推荐的方式。应用直接把日志写到标准输出和标准错误流,K8s会自动捕获并存储到节点上。

为什么推荐?因为简单。你不需要关心日志文件放哪,不需要配置采集路径,K8s帮你搞定一切。

# 一个简单的Go应用示例
package main

import (
    "log"
    "os"
)

func main() {
    // 直接输出到标准输出
    log.SetOutput(os.Stdout)
    log.Println("应用启动成功")
    
    // 错误信息输出到标准错误
    log.SetOutput(os.Stderr)
    log.Println("数据库连接超时")
}

我个人习惯,新项目一律用标准输出。配合kubectl logs命令,排查问题非常方便。

小技巧:使用kubectl logs --tail=100 -f可以实时查看最近100行日志。加上--timestamps参数还能显示时间戳。

但标准输出也有局限。比如日志格式不统一,或者你需要对日志做预处理(比如加字段、过滤敏感信息)。这时候就需要Sidecar了。

Sidecar模式

Sidecar模式,说白了就是在同一个Pod里跑两个容器:一个是你自己的应用容器,另一个是日志处理容器。两个容器共享同一个Volume,日志通过Volume传递。

我举个例子。假设你的应用输出的是纯文本日志,但你的日志系统只接受JSON格式。怎么办?加个Sidecar容器,专门做格式转换。

apiVersion: v1
kind: Pod
metadata:
  name: app-with-sidecar
spec:
  containers:
  - name: app
    image: my-app:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
  - name: log-processor
    image: fluent-bit:latest
    volumeMounts:
    - name: logs
      mountPath: /var/log/app
    command: ["fluent-bit", "-c", "/etc/fluent-bit.conf"]
  volumes:
  - name: logs
    emptyDir: {}

你看,应用把日志写到/var/log/app,Sidecar容器读取并处理。两个容器共享同一个emptyDir卷。

注意:emptyDir卷的生命周期和Pod一致。Pod删除后日志也会消失。如果需要持久化,记得配置额外的存储。

Sidecar的好处是灵活。你可以做日志过滤、格式转换、压缩、加密,甚至发送到多个后端。但代价是资源消耗翻倍——每个Pod多跑一个容器。

DaemonSet模式

最后说说DaemonSet。这种模式在每个节点上部署一个日志采集代理,比如Filebeat、Fluentd、Logstash。代理采集节点上所有容器的日志,然后发送到中央存储。

DaemonSet适合什么场景?我遇到过一种情况:团队有大量遗留应用,代码改不动,没法输出到标准输出。这时候DaemonSet就派上用场了——它直接从节点文件系统采集日志。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: filebeat
spec:
  selector:
    matchLabels:
      app: filebeat
  template:
    metadata:
      labels:
        app: filebeat
    spec:
      containers:
      - name: filebeat
        image: docker.elastic.co/beats/filebeat:8.0.0
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers

DaemonSet的优点是资源利用率高——一个节点只跑一个采集代理,不管上面有多少Pod。但缺点也很明显:你需要管理采集代理的配置,而且采集路径可能因容器运行时不同而不同。

核心要点:三种模式不是互斥的。实际项目中,我经常混合使用——新应用用标准输出,遗留应用用DaemonSet,需要预处理时加Sidecar。

如何选择?

我个人的建议是这样的:

  1. 新项目:优先用标准输出。这是最云原生的方式,也是社区推荐的做法。
  2. 需要日志预处理:用Sidecar。比如日志格式转换、敏感信息脱敏、添加自定义字段。
  3. 遗留应用迁移:用DaemonSet。不改代码,直接采集文件日志。
  4. 混合场景:标准输出 + DaemonSet组合。标准输出用于新应用,DaemonSet兜底采集所有日志。

嗯,这里要注意一点:不管用哪种方式,日志格式一定要统一。我建议所有日志都输出JSON格式,包含时间戳、级别、模块、消息等字段。这样后续分析会轻松很多。

好了,第一章就聊到这。下一章我们深入讲讲标准输出日志的采集和配置,包括如何用kubectl logs高效排查问题。

公众号:蓝海资料掘金营,微信deep3321