日志架构

应用日志可以让你了解应用内部的运行状况。日志对调试问题和监控集群活动非常有用。 大部分现代化应用都有某种日志记录机制。同样地,容器引擎也被设计成支持日志记录。 针对容器化应用,最简单且最广泛采用的日志记录方式就是写入标准输出和标准错误流。

但是,由容器引擎或运行时提供的原生功能通常不足以构成完整的日志记录方案。 例如,如果发生容器崩溃、Pod 被逐出或节点宕机等情况,你可能想访问应用日志。 在集群中,日志应该具有独立的存储和生命周期,与节点、Pod 或容器的生命周期相独立。 这个概念叫 集群级的日志

集群级日志架构需要一个独立的后端用来存储、分析和查询日志。 Kubernetes 并不为日志数据提供原生的存储解决方案。 相反,有很多现成的日志方案可以集成到 Kubernetes 中。 下面各节描述如何在节点上处理和存储日志。

Kubernetes 中的基本日志记录

这里的示例使用包含一个容器的 Pod 规约,每秒钟向标准输出写入数据。

debug/counter-pod.yaml 日志架构 - 图1

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: counter
  5. spec:
  6. containers:
  7. - name: count
  8. image: busybox
  9. args: [/bin/sh, -c,
  10. 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

用下面的命令运行 Pod:

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

输出结果为:

  1. pod/counter created

像下面这样,使用 kubectl logs 命令获取日志:

  1. kubectl logs counter

输出结果为:

  1. 0: Mon Jan 1 00:00:00 UTC 2001
  2. 1: Mon Jan 1 00:00:01 UTC 2001
  3. 2: Mon Jan 1 00:00:02 UTC 2001
  4. ...

你可以使用命令 kubectl logs --previous 检索之前容器实例的日志。 如果 Pod 中有多个容器,你应该为该命令附加容器名以访问对应容器的日志。 详见 kubectl logs 文档

节点级日志记录

节点级别的日志记录

容器化应用写入 stdoutstderr 的任何数据,都会被容器引擎捕获并被重定向到某个位置。 例如,Docker 容器引擎将这两个输出流重定向到某个 日志驱动(Logging Driver) , 该日志驱动在 Kubernetes 中配置为以 JSON 格式写入文件。

说明: Docker JSON 日志驱动将日志的每一行当作一条独立的消息。 该日志驱动不直接支持多行消息。你需要在日志代理级别或更高级别处理多行消息。

默认情况下,如果容器重启,kubelet 会保留被终止的容器日志。 如果 Pod 在工作节点被驱逐,该 Pod 中所有的容器也会被驱逐,包括容器日志。

节点级日志记录中,需要重点考虑实现日志的轮转,以此来保证日志不会消耗节点上全部可用空间。 Kubernetes 并不负责轮转日志,而是通过部署工具建立一个解决问题的方案。 例如,在用 kube-up.sh 部署的 Kubernetes 集群中,存在一个 logrotate,每小时运行一次。 你也可以设置容器运行时来自动地轮转应用日志。

例如,你可以找到关于 kube-up.sh 为 GCP 环境的 COS 镜像设置日志的详细信息, 脚本为 configure-helper 脚本

当使用某 CRI 容器运行时 时,kubelet 要负责对日志进行轮换,并 管理日志目录的结构。kubelet 将此信息发送给 CRI 容器运行时,后者 将容器日志写入到指定的位置。kubelet 标志 container-log-max-sizecontainer-log-max-files 可以用来配置每个日志文件的最大长度 和每个容器可以生成的日志文件个数上限。

当运行 kubectl logs 时, 节点上的 kubelet 处理该请求并直接读取日志文件,同时在响应中返回日志文件内容。

说明: 如果有外部系统执行日志轮转或者使用了 CRI 容器运行时,那么 kubectl logs 仅可查询到最新的日志内容。 比如,对于一个 10MB 大小的文件,通过 logrotate 执行轮转后生成两个文件, 一个 10MB 大小,一个为空,kubectl logs 返回最新的日志文件,而该日志文件 在这个例子中为空。

系统组件日志

系统组件有两种类型:在容器中运行的和不在容器中运行的。例如:

  • 在容器中运行的 kube-scheduler 和 kube-proxy。
  • 不在容器中运行的 kubelet 和容器运行时。

在使用 systemd 机制的服务器上,kubelet 和容器容器运行时将日志写入到 journald 中。 如果没有 systemd,它们将日志写入到 /var/log 目录下的 .log 文件中。 容器中的系统组件通常将日志写到 /var/log 目录,绕过了默认的日志机制。 他们使用 klog 日志库。 你可以在日志开发文档 找到这些组件的日志告警级别约定。

和容器日志类似,/var/log 目录中的系统组件日志也应该被轮转。 通过脚本 kube-up.sh 启动的 Kubernetes 集群中,日志被工具 logrotate 执行每日轮转,或者日志大小超过 100MB 时触发轮转。

集群级日志架构

虽然Kubernetes没有为集群级日志记录提供原生的解决方案,但你可以考虑几种常见的方法。 以下是一些选项:

  • 使用在每个节点上运行的节点级日志记录代理。
  • 在应用程序的 Pod 中,包含专门记录日志的边车(Sidecar)容器。
  • 将日志直接从应用程序中推送到日志记录后端。

使用节点级日志代理

使用节点日志记录代理

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

由于日志记录代理必须在每个节点上运行,通常可以用 DaemonSet 的形式运行该代理。 节点级日志在每个节点上仅创建一个代理,不需要对节点上的应用做修改。

容器向标准输出和标准错误输出写出数据,但在格式上并不统一。 节点级代理 收集这些日志并将其进行转发以完成汇总。

使用 sidecar 容器运行日志代理

你可以通过以下方式之一使用边车(Sidecar)容器:

  • 边车容器将应用程序日志传送到自己的标准输出。
  • 边车容器运行一个日志代理,配置该日志代理以便从应用容器收集日志。

传输数据流的 sidecar 容器

带数据流容器的边车容器

利用边车容器向自己的 stdoutstderr 传输流的方式, 你就可以利用每个节点上的 kubelet 和日志代理来处理日志。 边车容器从文件、套接字或 journald 读取日志。 每个边车容器向自己的 stdoutstderr 流中输出日志。

这种方法允许你将日志流从应用程序的不同部分分离开,其中一些可能缺乏对写入 stdoutstderr 的支持。重定向日志背后的逻辑是最小的,因此它的开销几乎可以忽略不计。 另外,因为 stdoutstderr 由 kubelet 处理,你可以使用内置的工具 kubectl logs

例如,某 Pod 中运行一个容器,该容器向两个文件写不同格式的日志。 下面是这个 pod 的配置文件:

admin/logging/two-files-counter-pod.yaml 日志架构 - 图5

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: counter
  5. spec:
  6. containers:
  7. - name: count
  8. image: busybox
  9. args:
  10. - /bin/sh
  11. - -c
  12. - >
  13. i=0;
  14. while true;
  15. do
  16. echo "$i: $(date)" >> /var/log/1.log;
  17. echo "$(date) INFO $i" >> /var/log/2.log;
  18. i=$((i+1));
  19. sleep 1;
  20. done
  21. volumeMounts:
  22. - name: varlog
  23. mountPath: /var/log
  24. volumes:
  25. - name: varlog
  26. emptyDir: {}

不建议在同一个日志流中写入不同格式的日志条目,即使你成功地将其重定向到容器的 stdout 流。相反,你可以创建两个边车容器。每个边车容器可以从共享卷 跟踪特定的日志文件,并将文件内容重定向到各自的 stdout 流。

下面是运行两个边车容器的 Pod 的配置文件:

admin/logging/two-files-counter-pod-streaming-sidecar.yaml 日志架构 - 图6

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: counter
  5. spec:
  6. containers:
  7. - name: count
  8. image: busybox
  9. args:
  10. - /bin/sh
  11. - -c
  12. - >
  13. i=0;
  14. while true;
  15. do
  16. echo "$i: $(date)" >> /var/log/1.log;
  17. echo "$(date) INFO $i" >> /var/log/2.log;
  18. i=$((i+1));
  19. sleep 1;
  20. done
  21. volumeMounts:
  22. - name: varlog
  23. mountPath: /var/log
  24. - name: count-log-1
  25. image: busybox
  26. args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
  27. volumeMounts:
  28. - name: varlog
  29. mountPath: /var/log
  30. - name: count-log-2
  31. image: busybox
  32. args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log']
  33. volumeMounts:
  34. - name: varlog
  35. mountPath: /var/log
  36. volumes:
  37. - name: varlog
  38. emptyDir: {}

现在当你运行这个 Pod 时,你可以运行如下命令分别访问每个日志流:

  1. kubectl logs counter count-log-1

输出为:

  1. 0: Mon Jan 1 00:00:00 UTC 2001
  2. 1: Mon Jan 1 00:00:01 UTC 2001
  3. 2: Mon Jan 1 00:00:02 UTC 2001
  4. ...
  1. kubectl logs counter count-log-2

输出为:

  1. Mon Jan 1 00:00:00 UTC 2001 INFO 0
  2. Mon Jan 1 00:00:01 UTC 2001 INFO 1
  3. Mon Jan 1 00:00:02 UTC 2001 INFO 2
  4. ...

集群中安装的节点级代理会自动获取这些日志流,而无需进一步配置。 如果你愿意,你也可以配置代理程序来解析源容器的日志行。

注意,尽管 CPU 和内存使用率都很低(以多个 CPU 毫核指标排序或者按内存的兆字节排序), 向文件写日志然后输出到 stdout 流仍然会成倍地增加磁盘使用率。 如果你的应用向单一文件写日志,通常最好设置 /dev/stdout 作为目标路径, 而不是使用流式的边车容器方式。

应用本身如果不具备轮转日志文件的功能,可以通过边车容器实现。 该方式的一个例子是运行一个小的、定期轮转日志的容器。 然而,还是推荐直接使用 stdoutstderr,将日志的轮转和保留策略 交给 kubelet。

具有日志代理功能的边车容器

含日志代理的边车容器

如果节点级日志记录代理程序对于你的场景来说不够灵活,你可以创建一个 带有单独日志记录代理的边车容器,将代理程序专门配置为与你的应用程序一起运行。

说明:

在边车容器中使用日志代理会带来严重的资源损耗。 此外,你不能使用 kubectl logs 命令访问日志,因为日志并没有被 kubelet 管理。

下面是两个配置文件,可以用来实现一个带日志代理的边车容器。 第一个文件包含用来配置 fluentd 的 ConfigMap

admin/logging/fluentd-sidecar-config.yaml 日志架构 - 图8

  1. apiVersion: v1
  2. kind: ConfigMap
  3. metadata:
  4. name: fluentd-config
  5. data:
  6. fluentd.conf: |
  7. <source>
  8. type tail
  9. format none
  10. path /var/log/1.log
  11. pos_file /var/log/1.log.pos
  12. tag count.format1
  13. </source>
  14. <source>
  15. type tail
  16. format none
  17. path /var/log/2.log
  18. pos_file /var/log/2.log.pos
  19. tag count.format2
  20. </source>
  21. <match **>
  22. type google_cloud
  23. </match>

说明:

要进一步了解如何配置 fluentd,请参考 fluentd 官方文档

第二个文件描述了运行 fluentd 边车容器的 Pod 。 flutend 通过 Pod 的挂载卷获取它的配置数据。

admin/logging/two-files-counter-pod-agent-sidecar.yaml 日志架构 - 图9

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: counter
  5. spec:
  6. containers:
  7. - name: count
  8. image: busybox
  9. args:
  10. - /bin/sh
  11. - -c
  12. - >
  13. i=0;
  14. while true;
  15. do
  16. echo "$i: $(date)" >> /var/log/1.log;
  17. echo "$(date) INFO $i" >> /var/log/2.log;
  18. i=$((i+1));
  19. sleep 1;
  20. done
  21. volumeMounts:
  22. - name: varlog
  23. mountPath: /var/log
  24. - name: count-agent
  25. image: k8s.gcr.io/fluentd-gcp:1.30
  26. env:
  27. - name: FLUENTD_ARGS
  28. value: -c /etc/fluentd-config/fluentd.conf
  29. volumeMounts:
  30. - name: varlog
  31. mountPath: /var/log
  32. - name: config-volume
  33. mountPath: /etc/fluentd-config
  34. volumes:
  35. - name: varlog
  36. emptyDir: {}
  37. - name: config-volume
  38. configMap:
  39. name: fluentd-config

在示例配置中,你可以将 fluentd 替换为任何日志代理,从应用容器内 的任何来源读取数据。

从应用中直接暴露日志目录

直接从应用程序暴露日志

从各个应用中直接暴露和推送日志数据的集群日志机制 已超出 Kubernetes 的范围。