日志架构

应用程序日志可以帮助你理解应用程序内部发生的事情。日志对于调试问题和监控集群活动尤其有用。大多数现代应用程序都具有某种日志机制。同样,容器引擎被设计为支持日志记录。对于容器化应用程序而言,最简单且最被广泛采用的日志记录方法是写入标准输出和标准错误流。

然而,容器引擎或运行时提供的原生功能通常不足以构建一个完整的日志记录解决方案。

例如,如果容器崩溃、Pod 被驱逐或节点发生故障,你可能需要访问应用程序的日志。

在集群中,日志应具有独立于节点、Pod 或容器的独立存储和生命周期。此概念称为集群级别日志记录

集群级别日志记录架构需要独立的后端来存储、分析和查询日志。Kubernetes 本身不提供日志数据的原生存储方案。相反,有许多日志记录解决方案可以与 Kubernetes 集成。以下章节描述了如何在节点上处理和存储日志。

Pod 和容器日志

Kubernetes 从运行中的 Pod 的每个容器中捕获日志。

此示例使用一个 Pod 的清单,该 Pod 的容器每秒向标准输出流写入一次文本。

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

要运行此 Pod,请使用以下命令

kubectl apply -f https://k8s.io/examples/debug/counter-pod.yaml

输出为

pod/counter created

要获取日志,请使用 kubectl logs 命令,如下所示

kubectl logs counter

输出类似于

0: Fri Apr  1 11:42:23 UTC 2022
1: Fri Apr  1 11:42:24 UTC 2022
2: Fri Apr  1 11:42:25 UTC 2022

你可以使用 kubectl logs --previous 获取容器先前实例的日志。如果你的 Pod 有多个容器,请通过在命令后附加容器名称并带上 -c 标志来指定要访问哪个容器的日志,例如

kubectl logs counter -c count

容器日志流

特性状态: Kubernetes v1.32 [alpha] (默认启用: false)

作为一项 Alpha 特性,kubelet 可以从容器产生的两个标准流中分离出日志:标准输出标准错误。要使用此行为,你必须启用 PodLogsQuerySplitStreams 特性门控。启用该特性门控后,Kubernetes 1.33 允许通过 Pod API 直接访问这些日志流。你可以使用 stream 查询字符串指定流名称(StdoutStderr)来获取特定的流。你必须有权读取该 Pod 的 log 子资源。

为了演示此特性,你可以创建一个 Pod,该 Pod 定期向标准输出和错误流写入文本。

apiVersion: v1
kind: Pod
metadata:
  name: counter-err
spec:
  containers:
  - name: count
    image: busybox:1.28
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; echo "$i: err" >&2 ; i=$((i+1)); sleep 1; done']

要运行此 Pod,请使用以下命令

kubectl apply -f https://k8s.io/examples/debug/counter-pod-err.yaml

要仅获取 stderr 日志流,你可以运行

kubectl get --raw "/api/v1/namespaces/default/pods/counter-err/log?stream=Stderr"

有关更多详细信息,请参阅 kubectl logs 文档

节点如何处理容器日志

Node level logging

容器运行时负责处理并将容器化应用程序生成的任何输出重定向到其 stdoutstderr 流。不同的容器运行时以不同的方式实现这一点;然而,与 kubelet 的集成已标准化为 CRI 日志格式

默认情况下,如果容器重启,kubelet 会保留一个已终止的容器及其日志。如果 Pod 从节点中被驱逐,所有相应的容器及其日志也将被驱逐。

kubelet 通过 Kubernetes API 的一个特殊特性向客户端提供日志。访问日志的常用方式是运行 kubectl logs

日志轮换

特性状态: Kubernetes v1.21 [stable]

kubelet 负责轮换容器日志并管理日志目录结构。kubelet 将此信息发送给容器运行时(使用 CRI),运行时将容器日志写入指定位置。

你可以使用 kubelet 配置文件 配置两个 kubelet 配置设置containerLogMaxSize (默认 10Mi) 和 containerLogMaxFiles (默认 5)。这些设置分别允许你配置每个日志文件的最大大小和每个容器允许的最大文件数量。

为了在工作负载生成的日志量很大的集群中执行高效的日志轮换,kubelet 还提供了一种机制来调整日志轮换的方式,包括可以执行的并发日志轮换数量以及根据需要监视和轮换日志的间隔。你可以使用 kubelet 配置文件 配置两个 kubelet 配置设置containerLogMaxWorkerscontainerLogMonitorInterval

当你像基本日志记录示例中那样运行 kubectl logs 时,节点上的 kubelet 处理该请求并直接从日志文件读取。kubelet 返回日志文件的内容。

系统组件日志

系统组件分为两类:通常运行在容器中的组件,以及直接参与运行容器的组件。例如

  • kubelet 和容器运行时不运行在容器中。kubelet 负责运行你的容器(将它们分组到 Pod 中)
  • Kubernetes 调度器、控制器管理器和 API 服务器运行在 Pod 中(通常是静态 Pod)。etcd 组件运行在控制平面中,并且通常也作为静态 Pod 运行。如果你的集群使用 kube-proxy,通常将其作为 DaemonSet 运行。

日志位置

kubelet 和容器运行时写入日志的方式取决于节点使用的操作系统

在使用 systemd 的 Linux 节点上,kubelet 和容器运行时默认写入 journald。你可以使用 journalctl 读取 systemd 日志;例如:journalctl -u kubelet

如果不存在 systemd,kubelet 和容器运行时会写入 /var/log 目录下的 .log 文件。如果你想将日志写入其他位置,可以通过辅助工具 kube-log-runner 间接运行 kubelet,并使用该工具将 kubelet 日志重定向到你选择的目录。

默认情况下,kubelet 指示你的容器运行时将日志写入 /var/log/pods 目录下的子目录。

有关 kube-log-runner 的更多信息,请阅读系统日志

默认情况下,kubelet 将日志写入 C:\var\logs 目录下的文件(请注意,这不是 C:\var\log)。

尽管 C:\var\log 是 Kubernetes 这些日志的默认位置,但一些集群部署工具会将 Windows 节点设置为将日志写入 C:\var\log\kubelet

如果你希望将日志写入其他位置,可以通过辅助工具 kube-log-runner 间接运行 kubelet,并使用该工具将 kubelet 日志重定向到你选择的目录。

然而,默认情况下,kubelet 指示你的容器运行时将日志写入 C:\var\log\pods 目录下的子目录。

有关 kube-log-runner 的更多信息,请阅读系统日志


对于运行在 Pod 中的 Kubernetes 集群组件,它们将日志写入 /var/log 目录内的文件,绕过了默认的日志记录机制(这些组件不写入 systemd 日志)。你可以使用 Kubernetes 的存储机制将持久存储映射到运行组件的容器中。

Kubelet 允许将 Pod 日志目录从默认的 /var/log/pods 更改为自定义路径。可以通过配置 kubelet 配置文件中的 podLogsDir 参数来进行此调整。

有关 etcd 及其日志的详细信息,请查阅etcd 文档。同样,你可以使用 Kubernetes 的存储机制将持久存储映射到运行该组件的容器中。

集群级别日志记录架构

尽管 Kubernetes 不提供原生集群级别日志记录解决方案,但有几种常见的方案可供你考虑。以下是一些选项

  • 使用在每个节点上运行的节点级别日志记录代理。
  • 在应用程序 Pod 中包含一个专用于日志记录的 sidecar 容器。
  • 直接从应用程序内部将日志推送到后端。

使用节点日志记录代理

Using a node level logging agent

你可以通过在每个节点上包含一个 节点级别日志记录代理 来实现集群级别日志记录。日志记录代理是一个专门的工具,它暴露日志或将日志推送到后端。通常,日志记录代理是一个容器,可以访问该节点上所有应用程序容器的日志文件所在的目录。

因为日志记录代理必须在每个节点上运行,所以建议将代理作为 DaemonSet 运行。

节点级别日志记录每个节点只创建一个代理,并且不需要对运行在节点上的应用程序进行任何更改。

容器将日志写入 stdout 和 stderr,但没有统一的格式。节点级别的代理收集这些日志并转发进行聚合。

使用带有日志记录代理的 sidecar 容器

你可以通过以下方式之一使用 sidecar 容器

  • sidecar 容器将其应用程序日志流式传输到自己的 stdout
  • sidecar 容器运行一个日志记录代理,该代理配置为从应用程序容器收集日志。

流式 sidecar 容器

Sidecar container with a streaming container

通过让 sidecar 容器写入其自己的 stdoutstderr 流,你可以利用每个节点上已有的 kubelet 和日志记录代理。sidecar 容器从文件、socket 或 journald 读取日志。每个 sidecar 容器将其日志输出到自己的 stdoutstderr 流。

这种方法允许你分离应用程序不同部分的多个日志流,其中一些部分可能不支持写入 stdoutstderr。重定向日志的逻辑非常简单,因此开销不大。此外,由于 stdoutstderr 由 kubelet 处理,你可以使用 kubectl logs 等内置工具。

例如,一个 Pod 运行一个容器,该容器使用两种不同的格式将日志写入两个不同的日志文件。这是一个 Pod 的清单

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

即使你设法将两个组件重定向到容器的 stdout 流,也不建议将不同格式的日志条目写入同一个日志流。相反,你可以创建两个 sidecar 容器。每个 sidecar 容器可以跟踪共享卷中的特定日志文件,然后将日志重定向到自己的 stdout 流。

这是一个具有两个 sidecar 容器的 Pod 的清单

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-1
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/2.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

现在运行此 Pod 时,可以通过运行以下命令分别访问每个日志流

kubectl logs counter count-log-1

输出类似于

0: Fri Apr  1 11:42:26 UTC 2022
1: Fri Apr  1 11:42:27 UTC 2022
2: Fri Apr  1 11:42:28 UTC 2022
...
kubectl logs counter count-log-2

输出类似于

Fri Apr  1 11:42:29 UTC 2022 INFO 0
Fri Apr  1 11:42:30 UTC 2022 INFO 0
Fri Apr  1 11:42:31 UTC 2022 INFO 0
...

如果你在集群中安装了节点级别的代理,该代理会自动收集这些日志流,无需额外配置。如果你愿意,可以配置代理根据源容器解析日志行。

即使对于 CPU 和内存使用率都很低的 Pod(CPU 占用几个毫核,内存占用几兆字节),将日志写入文件然后再流式传输到 stdout 会使节点上所需的存储空间翻倍。如果你的应用程序写入单个文件,建议将 /dev/stdout 设置为目标,而不是采用流式 sidecar 容器的方法。

Sidecar 容器也可以用于轮换应用程序本身无法轮换的日志文件。这种方法的一个示例是定期运行 logrotate 的小型容器。然而,更直接的方法是直接使用 stdoutstderr,并将轮换和保留策略留给 kubelet 处理。

带有日志记录代理的 sidecar 容器

Sidecar container with a logging agent

如果节点级别的日志记录代理对你的情况不够灵活,你可以创建一个带有独立日志记录代理的 sidecar 容器,该代理专门配置为与你的应用程序一起运行。

以下是你可以用来实现带有日志记录代理的 sidecar 容器的两个示例清单。第一个清单包含一个用于配置 fluentd 的 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluentd.conf: |
    <source>
      type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag count.format1
    </source>

    <source>
      type tail
      format none
      path /var/log/2.log
      pos_file /var/log/2.log.pos
      tag count.format2
    </source>

    <match **>
      type google_cloud
    </match>    

第二个清单描述了一个 Pod,该 Pod 包含一个运行 fluentd 的 sidecar 容器。该 Pod 挂载了一个卷,fluentd 可以从中获取其配置数据。

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-agent
    image: registry.k8s.io/fluentd-gcp:1.30
    env:
    - name: FLUENTD_ARGS
      value: -c /etc/fluentd-config/fluentd.conf
    volumeMounts:
    - name: varlog
      mountPath: /var/log
    - name: config-volume
      mountPath: /etc/fluentd-config
  volumes:
  - name: varlog
    emptyDir: {}
  - name: config-volume
    configMap:
      name: fluentd-config

直接从应用程序暴露日志

Exposing logs directly from the application

直接从每个应用程序暴露或推送日志的集群日志记录超出了 Kubernetes 的范围。

接下来

最后修改于 2024 年 10 月 17 日 11:21 PM PST: doc: 记录 PodLogsQuerySplitStreams 特性的使用 (387153787a)