kubernetes中对容器日志的处理方式叫作cluster-level-logging,这个日志处理系统与容器、Pod、以及Node的生命周期都是完全无关的。这种设计是为了保证无论是容器挂了、Pod被删除、甚至节点宕机的时候,应用的日志依然可以被正常获取到。
对于一个容器来说,当应用把日志输出到stdout和stderr之后,容器项目在默认情况下就会把这些日志输出到宿主机上的一个JSON文件里。这样,通过
kubectl logs
命令就可以看到这些容器的日志了。
上述机制是容器日志收集的基础假设,如果应用把文件输出到其他地方:
这些属于特殊情况。
kubernetes本身是不会做容器日志收集的工作,所以要实现cluster-level-logging,需要在部署集群的时候,提前对具体的日志方案进行规划。kubernetes项目推荐了四种日志方案:
在Node上部署logging agent,将日志文件转发到后端存储里保存起来,这个方案的架构如下图所示:
这里的核心就在于logging agent,它一般会以DaemonSet的方式运行在节点上,然后将宿主机上的容器日志目录挂载进去,最后由logging-agent把日志转发出去。
可以通过Fluentd项目作为宿主机上的logging-agent,然后把日志转发到远端的ElasticSearch里保存起来工将来进行检索。此外,在很多kubernetes的部署里,会自动弃用logrotate,在日志文件超过10MB的时候自动对日志文件进行rotate操作。
在Node上部署logging agent:
当容器的日志只能输出到某些文件里的时候,通过sidecar容器把这些日志文件重新输出到sidecar的stdout和stderr上,然后继续使用第一种方案,具体工作原理如下图所示:
比如,应用的Pod中只有一个容器,它会把日志输出到容器里的/var/log/1.log
和/var/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里,方案架构如下图:
这这种方案里,应用可以直接把日志输出到固定的文件里,而不是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就完全不必操心容器日志的收集,这对于本身已经有完善的日志处理系统来说是一个非常好的选择。
综合对比四种方案,将应用日志输出到stdout和stderr,然后通过在宿主机上部署logging-agent的方式来集中处理日志。这种方案不仅简单,kubectl logs也能用,而且可靠性高,并且宿主机本身很可能就自带了rsyslogd等非常成熟的日志收集组件来使用。
无论哪种方案,都必须要及时将这些日志文件从宿主机上清理掉,或者给日志目录专门挂载一些容量巨大的远程盘,否则一旦主磁盘被打满,整个系统就可能会陷入奔溃状态。