02-容器日志

kubernetes中对容器日志的处理方式叫作cluster-level-logging,这个日志处理系统与容器、Pod、以及Node的生命周期都是完全无关的。这种设计是为了保证无论是容器挂了、Pod被删除、甚至节点宕机的时候,应用的日志依然可以被正常获取到

对于一个容器来说,当应用把日志输出到stdout和stderr之后,容器项目在默认情况下就会把这些日志输出到宿主机上的一个JSON文件里。这样,通过kubectl logs命令就可以看到这些容器的日志了。

上述机制是容器日志收集的基础假设,如果应用把文件输出到其他地方:

  • 直接输出到容器的某个文件里
  • 输出到远程存储里

这些属于特殊情况。

kubernetes本身是不会做容器日志收集的工作,所以要实现cluster-level-logging,需要在部署集群的时候,提前对具体的日志方案进行规划。kubernetes项目推荐了四种日志方案:

  1. 方案一:在Node上部署logging agent,将日志文件转发到后端存储里保存起来
  2. 方案二:当容器的日志只能输出到某些文件里的时候,通过sidecar容器把这些日志文件重新输出到sidecar的stdout和stderr上,然后继续使用第一种方案
  3. 方案三:通过一个sidecar容器,直接把应用的日志文件发送到远程存储里
  4. 方案四:在编写应用的时候,直接制定好日志的存储后端

0.1. 方案一

在Node上部署logging agent,将日志文件转发到后端存储里保存起来,这个方案的架构如下图所示:

image

这里的核心就在于logging agent,它一般会以DaemonSet的方式运行在节点上,然后将宿主机上的容器日志目录挂载进去,最后由logging-agent把日志转发出去。

可以通过Fluentd项目作为宿主机上的logging-agent,然后把日志转发到远端的ElasticSearch里保存起来工将来进行检索。此外,在很多kubernetes的部署里,会自动弃用logrotate,在日志文件超过10MB的时候自动对日志文件进行rotate操作。

在Node上部署logging agent:

  • 优点:一个节点只需部署一个agent,不会对应用和Pod有任何侵入性
  • 不足:日志,都必须是直接输出到容器的stdout和stderr里

0.2. 方案二

当容器的日志只能输出到某些文件里的时候,通过sidecar容器把这些日志文件重新输出到sidecar的stdout和stderr上,然后继续使用第一种方案,具体工作原理如下图所示:

image

比如,应用的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文件 这对磁盘是很大的浪费。

除非万不得已,或者应用容器完全不能修改,否则建议直接使用方案一,或者直接使用方案三

0.3. 方案三

通过一个sidecar容器,直接把应用的日志文件发送到远程存储里,相当于把方案一里的logging agent放在应用Pod里,方案架构如下图:

image

这这种方案里,应用可以直接把日志输出到固定的文件里,而不是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命令看不到任何日志输出。

0.4. 方案四

在编写应用的时候,直接制定好日志的存储后端,如下图所示:

image

这种方案下,kubernetes就完全不必操心容器日志的收集,这对于本身已经有完善的日志处理系统来说是一个非常好的选择。

0.5. 总结

综合对比四种方案,将应用日志输出到stdout和stderr,然后通过在宿主机上部署logging-agent的方式来集中处理日志。这种方案不仅简单,kubectl logs也能用,而且可靠性高,并且宿主机本身很可能就自带了rsyslogd等非常成熟的日志收集组件来使用。

无论哪种方案,都必须要及时将这些日志文件从宿主机上清理掉,或者给日志目录专门挂载一些容量巨大的远程盘,否则一旦主磁盘被打满,整个系统就可能会陷入奔溃状态

上次修改: 14 April 2020