本文将详细介绍 Kubernetes 里对容器日志的处理方式。
首先需要明确,Kubernetes 里对容器日志的处理方式都叫作 cluster-level-logging,即这个日志处理系统与容器、Pod 以及节点的生命周期都完全无关。这种设计当然是为了保证无论容器不 工作、Pod 被删除,甚至节点宕机,依然可以正常获取应用的日志。
对于一个容器来说,当应用把日志输出到 stdout 和 stderr 之后,容器项目本身默认会把这些日志输出到宿主机上的一个 JSON 文件里。这样,通过 kubectl logs 命令就可以看到这些容器的日志了。
上述机制就是本节要讲解的容器日志收集的基础假设。如果你的应用是把文件输出到别处, 比如直接输出到容器里的某个文件里,或者输出到远程存储里,就另当别论了。当然,本节也会 介绍这些特殊情况的处理方法。
Kubernetes 本身实际上不会为你做容器日志收集工作。所以,为了实现上述 cluster-level-logging, 你需要在部署集群的时候提前规划具体的日志方案。Kubernetes 项目本身主要推荐了 3 种日志 方案。
第一种方案,在节点上部署 logging agent,将日志文件转发到后端存储里保存起来。这种方 案的架构如图 1 所示。
(图1)
不难看出,这里的核心就在于 logging agent,它一般会以 DaemonSet 的方式在节点上运行, 然后把宿主机上的容器日志目录挂载进去,最后由 logging-agent 把日志转发出去。
例如,我们可以把 Fluentd 项目作为宿主机上的 logging-agent,然后把日志转发到远端的 Elasticsearch 里保存起来供将来检索。具体的操作过程参见官方文档。另外,Kubernetes 的很多部署会自动为你启用logrotate,在日志文件大小超过10 MB时自动对日志文件进行切割(logrotate)操作。
可以看到,在节点上部署 logging agent 最大的优点在于一个节点仅需部署一个agent,并且对应用和Pod 没有任何侵入性。所以,在社区中该方案最常用。但是,这种方案的明显不足之处在于,它要求应用输出的日志都必须直接输出到容器的 stdout 和 stderr 里。
第二种 Kubernetes 容器日志方案处理的就是这种特殊情况:当容器的日志只能输出到某些文件里时,我们可以通过一个 sidecar 容器把这些日志文件重新输出到 sidecar 的 stdout 和 stderr 上, 这样就能够继续使用第一种方案了。这种方案的具体工作原理如图 2 所示。
(图2)
比如,现在我的应用 Pod 只有一个容器,它会把日志输出到容器里的 /var/log/1.log 和 2.log 这两个文件里。这个 Pod 的 YAML 文件如下所示:
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
containers:
- name: count
image: busybox
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: {}
在这种情况下,使用 kubectl logs 命令看不到应用的任何日志。而且前面讲解的最常用 的第一种方案也无法使用。
此时,我们就可以为这个 Pod 添加两个 sidecar 容器,分别将上述两个日志文件里的内容重 新以 stdout 和 stderr 的方式输出。这个 YAML 文件的写法如下所示:
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
containers:
- name: count
image: busybox
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
args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
volumeMounts:
- name: varlog
mountPath: /var/log
- name: count-log-2
image: busybox
args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log']
volumeMounts:
- name: varlog
mountPath: /var/log
volumes:
- name: varlog
emptyDir: {}
此时,就可以通过 kubectl logs 命令查看这两个 sidecar 容器的日志,间接看到应用的日志内容了,如下所示:
$ kubectl logs counter count-log-1
0: Mon Jan 1 00:00:00 UTC 2001
1: Mon Jan 1 00:00:01 UTC 2001
2: Mon Jan 1 00:00:02 UTC 2001
...
$ kubectl logs counter count-log-2
Mon Jan 1 00:00:00 UTC 2001 INFO 0
Mon Jan 1 00:00:01 UTC 2001 INFO 1
Mon Jan 1 00:00:02 UTC 2001 INFO 2
...
由于 sidecar 跟主容器之间共享 Volume,因此这里的 sidecar 方案的额外性能损耗并不大,也就是多占用一点儿 CPU 和内存罢了。
需要注意的是,此时宿主机上实际上会存在两份相同的日志文件:一份是应用自己写入的, 另一份则是 sidecar 的 stdout 和 stderr 对应的 JSON 文件。这对磁盘是很大的浪费。所以,除非万不得已或者应用容器完全不可能被修改,否则还是建议你直接使用第一种方案,或者直接使用第三种方案。
第三种方案,就是通过一个 sidecar 容器直接把应用的日志文件发送到远程存储中去。这就相当于把第一种方案中的 logging agent 放在了应用 Pod 里。这种方案的架构如图 3 所示。
(图3)
在这种方案下,你的应用还可以直接把日志输出到固定的文件里,而不是 stdout,你的 logging-agent 还可以使用 fluentd,后端存储还可以是 Elasticsearch。只不过,fluentd 的输入源变 成了应用的日志文件。一般说来,我们会把 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 的定义里就可以声明一个 Fluentd 容器作为 sidecar,专门负责将应用生成的 1.log 和 2.log 转发到 Elasticsearch 当中。这个配置如下所示:
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
containers:
- name: count
image: busybox
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: k8s.gcr.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
可以看到,这个 Fluentd 容器使用的输入源就是通过引用前面编写的 ConfigMap 来指定的。这里用到了 Projected Volume 来把 ConfigMap 挂载到 Pod 里。需要注意的是,这种方案虽然部署简单,并且对宿主机非常友好,但是这个 sidecar 容器很可能会消耗较多资源,甚至拖垮应用容器。并且,由于日志还是没有输出到 stdout 上,因此通过 kubectl logs 看不到任何日志输出。以上就是 Kubernetes 项目管理容器应用日志最常用的 3 种手段。
小结
本文详细讲解了 Kubernetes 项目对容器应用日志的收集方式。上述 3 种方案中,最常用的一种方式就是将应用日志输出到 stdout 和 stderr,然后通过在宿主机上部署 logging-agent 来集中处 理日志。这种方案不仅管理简单,可靠性高,kubectl logs 也可以用,而且宿主机很可能自带了 rsyslogd 等成熟的日志收集组件供使用。除此之外,还有一种方式:在编写应用时直接指定好日志的存储后端,如图 4 所示。
(图4)
在这种方案下,Kubernetes 就完全不必操心容器日志的收集了,这对于已经有完善的日志处 理系统的公司来说是一个非常好的选择。
最后需要指出的是,无论采用哪种方案,你都必须配置好宿主机上的日志文件切割和清理工作,或者给日志目录专门挂载一些容量巨大的远程盘。否则,一旦主磁盘分区占满,整个系统就可能陷入崩溃状态,这是非常麻烦的。
小思考
1.当日志量很大时,直接将日志输出到容器 stdout 和 stderr 上有无隐患?有何解决办法?
2.你还有哪些容器日志收集方案?
- CNCF TOC成员张磊重磅作品,近4万读者一致好评
- 基于Kubernetes v1.18,深入剖析核心原理
- 后端技术人员与基础平台工程师必读
- 打通Kubernetes的任督二脉,掌握容器技术体系的精髓