容器日志收集与管理:让日志无处可逃

本文将详细介绍 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的任督二脉,掌握容器技术体系的精髓

☟☟☟ 立刻拥有,学习kubernetes得心应手

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值