Logging Architecture

通过应用和系统日志可以帮助你了解集群内部发生了什么。同时日志也被用于调试问题和监控集群活动。大部分现代应用都有各种日志机制,因此,大部分容器引擎也被设计支持各种日志。对于容器化的应用来说,最简单也最推荐的日志收集方法是将日志写到标准输出和标准错误输出。

然而,容器引擎或运行时提供的原生的功能并不足以提供一个完整的日志解决方案。例如:如果一个容器崩溃、Pod被驱逐(evicted)或者机器挂了,你仍然想查看应用日志。因此日志需要单独存储,或不依赖于节点、Pod或容器的生命周期。这就是集群级日志。集群级日志需要一个单独的后端进行存储、分析和查询日志。Kubernetes对于日志数据并没有提供原生存储解决方案,但是可以整合很多已有的日志解决方案到Kubernetes集群。

  • Kubernetes中的基本日志
  • 节点级日志
    • 系统组件日志
  • 集群级日志架构
    • 使用节点日志agent
    • 使用sidecar容器作为日志agent
      • 流式sidecar容器
      • 日志agent的sidecar容器
  • 应用直接导出日志

集群级日志架构假定日志后端存在于集群内部或外部。 如果对集群级日志记录不感兴趣,但是仍然可以找到有关如何在节点上存储和处理日志的说明。

kubernetes中的基础日志

在本节,你将看到Kubernetes中的将数据输出到标准输出流的基础日志。下面是创建一个每秒将文本写入到标准输出的Pod的配置

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)"; i=$((i+1)); sleep 1; done']

使用如下的命令运行Pod

$ kubectl create -f https://k8s.io/docs/tasks/debug-application-cluster/counter-pod.yaml
pod "counter" created

使用kubectl logs获取日志,例如

$ kubectl logs counter
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
...

通过设置容器的--previous标志,可以在容器崩溃之后使用kubectl logs查看前一个实例的日志。如果Pod有多个容器,需要在命令之后接容器的名称,指定需要访问哪个容器的日志。更多细节参考kubectl logs文档

节点级日志

logging-node-level.png

容器化应用写到stdoutstderr,然后由容器引擎处理或重定向到某处。例如,Docker容器引擎重定向这两个流到日志驱动——在Kubernetes配置中是写到json格式的文件中。

**注意:**Docker json日志驱动将每一行作为单独的消息。当使用Docker日志驱动,无法直接支持多行消息。需要agent或更高级别处理多行消息。

默认情况下,如果容器重新启动,则kubelet会保留一个已终止的容器及其日志。 如果一个pod从节点中逐出,所有相应的容器以及他们的日志都将被删除。

节点级日志的一个重点是实现日志滚动(logrotate),以便日志不会占用节点上所有的存储。Kubernetes当前不提供日志滚动,但是部署工具需要实现解决方案。例如,在Kubernetes集群中,kube-up.sh部署脚本中,有一个每小时运行一次的logroute工具的配置。你也可以设置容器运行时自动滚动应用日志,例如,使用Docker的log-opt。在kube-up.sh脚本中,后一种方法用于GCP的COS镜像,前一种方法可以用用在任何环境。在这两种例子中,默认配置日志超过10MB时滚动。

例如,可以在相应的脚本中找到有关kube-up.sh如何在GCP上为COS映像设置日志记录的详细信息。

在原始日志例子中,当运行kubectl logs,在节点上的kubelet处理请求,直接将从日志中读取的内容返回。注意:如果扩展系统已经执行日志滚动,通过kubectl logs只能访最新日志文件的内容。例如:当文件达到10MB时,logrote执行之后将会有两个文件,一个文件是10MB,另一个是空的,kubectl logs将会返回空。

系统组件日志

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

  • Kubernetes调度程序和kube-proxy在容器中运行。
  • kubelet和container runtime(例如Docker)不在容器中运行。

在有systemd的机器上,kubelet和container runtime将日志写到journald。如果没有systemd,它们将日志写到/var/log目录下的.log文件。容器内的系统组件会绕过默认的日志机制,将日志写到/var/log目录下。它们使用glog日志库。你可以在日志开发文档中找到对这些组件日志级别的定义。

类似于容器日志,在/var/log目录下的系统组件日志,也需要滚动。在使用kube-up.sh脚本创建的Kubernetes集群中,这些日志是通过logrotate按照每天或大小超过100MB进行滚动。

集群级日志架构

虽然Kubernetes不提供原生的集群级日志解决方案,但是有很多通用的方法可供参考。以下是一些参考:

  • 在每个节点上运行节点日志agent
  • 在应用Pod内运行专门的sidecar日志容器
  • 应用直接推送日志到后端

使用节点日志agent

logging-with-node-agent.png

通过在每个节点上运行节点级日志agent实现集群级日志。日志agent是一个专用工具,它暴露(exposes)日志或推送日志到后端。通常日志agent是一个可以直接访问节点上所有容器的日志目录的容器。

因为日志agent必须在每个节点上运行,它通常将其实现为节点上的DaemonSet、manifest pod或者本机进程。但是后两种方法已经被弃用,并且非常不鼓励使用。

Kubernetes并不指定日志agent,但是有两种可选的日志agent包随Kubernetes一起发布:谷歌云平台中的StackDriver LoggingElasticsearch。你可以在文档中找到更多说明信息。它们都用定制配置的fluentd作为节点agent。

使用sidecar容器作为日志agent

可以通过以下任一方式使用sidecar容器:

  • sidecar容器将应用的日志导到自己的stdout
  • sidecar容器运行一个日志agent,它配置成收集应用容器的日志。
流式sidecar容器

logging-with-streaming-sidecar.png

sidecar容器导到它们自己的stdoutstderr流,可以利用每个节点上已经运行的kubelet和日志agent。sidecar容器读取文件、网络或journald,然后每个独立的sidecar容器将日志打印到自己的stdoutstderr流。

对于那些可能不支持写到stdoutstderr的应用,通过这种方法可以从应用的不同部分分离出单独的日志流。重定向日志的代价是很小的,因此几乎不带来开销。另外,因为stdoutstderr都是由kubelet处理,可以直接使用内置工具kubectl logs查看。

下面的事例中,Pod中运行一个容器,这个容器会写两个不同的文件,使用不同的格式。Pod的配置文件:

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: {}

如果将两种组件的日志都重定向到容器的stdout流,日志流中具有不同的格式的日志条目,将使日志变得混乱。相反,应该引入两个sidecar容器。每个sidecar容器通过共享卷的方式tail实际的文件,然后重定向到自己的stdout流。

下面是有两个sidecar容器的Pod的配置文件:

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 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
...

在集群中节点级agent不做任何额外配置就可以收集这些日志流。如果你愿意,可以配置agent根据日志来源容器解析这些日志。

注意:尽管只使用很少的CPU和内存(CPU是千分之一核级别,内存是兆级别),但是先写日志到文件然后再将他们导入到stdout会导致两次磁盘访问。如果应用只写一个文件,最好是设置/dev/stdout作为目标而不是使用流式sidecar容器的方式。

对于那些本身不能滚动日志的应用,可以使用sidecar容器滚动日志,这种方法的例子是一个定期运行logrotate的小容器。但是,建议还是直接使用stdoutstderr,并将日志滚动和保留策略留给kubelet。

日志agent的sidecar容器

logging-with-sidecar-agent.png

如果节点级日志agent对你的情况不够灵活,可以使用单独的日志agent作为sidecar容器,并设置应用对应的配置。

注意:使用sidecar容器日志agent将会导致显著的资源消耗。此外,你也无法使用kubectl logs命令查看这些日志,因为他们并不由kubelet控制。

例如,你可以使用Stackdriver,它使用fluentd作为日志agent。 以下是两个可用于实现此方法的配置文件。 第一个文件包含一个ConfigMap用来配置fluentd。

apiVersion: v1
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>
kind: ConfigMap
metadata:
  name: fluentd-config

**注意:**fluentd的配置超出了这篇文章的讨论范围。更多对于fluentd配置的信息,参考fluentd官方文档

第二个文件描述Pod中有一个sidecar容器运行fluentd。Pod通过挂载卷获取配置文件数据。

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: gcr.io/google_containers/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

过一段时间你就可以在Stackdriver接口中看到日志。

记住,这只是一个例子,你实际上可以使用任何日志agent代替fluentd,从应用容器中任何来源读取日志。

应用直接导出日志

logging-from-application.png

每个应用程序通过暴露(exposing)或推送日志来实现集群级日志。但是,这种日志机制并不在Kubernetes考虑范围之内。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐