kubernetes日志采集方案

1 总体介绍

关于kubernetes的日志分几种,针对kubernetes本身而言有三种:

  1. 资源运行时的event事件。比如在k8s集群中创建pod之后,可以通过kubectl describe pod 命令查看pod的详细信息。
  2. 容器中运行的应用程序自身产生的日志,比如tomcat、nginx、php的运行日志。
  3. k8s各组件的服务日志,比如systemctl status kubelet。

容器日志收集的方式通常有以下几种:

  1. 宿主机Node层面统一收集:将宿主机的目录挂载为容器的日志目录,然后在宿主机上收集。
  2. pod中以sidecar容器的方式收集:这个sidecar容器收集应用日志之后,可以输出到宿主机或者直接发送到远端的日志收集服务。
  3. 网络收集:容器内应用将日志直接发送到日志中心,比如java程序可以使用log4j2转换日志格式并发送到远端。或者通过修改docker的–log-driver。可以利用不同的driver把日志输出到不同地方,将log-driver设置为syslog、fluentd、splunk等日志收集服务,然后发送到远端。

Kubernetes日志基础

通过一个例子了解kubernetes中标准输出中的输出日志。容器中执行shell,定时输出统计数。

debug/counter-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: 192.168.200.197:80/test-private/busybox:latest
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']

To run this pod, use the following command:

kubectl apply -f https://k8s.io/examples/debug/counter-pod.yaml
pod/counter created

使用kubectl logs命令查看日志:

[root@kube-196 ~]# kubectl logs counter
1126: Tue Jun 18 09:27:07 UTC 2019
1127: Tue Jun 18 09:27:08 UTC 2019
1128: Tue Jun 18 09:27:09 UTC 2019
1129: Tue Jun 18 09:27:10 UTC 2019
1130: Tue Jun 18 09:27:11 UTC 2019
1131: Tue Jun 18 09:27:12 UTC 2019
...

查看对应的日志文件:

[root@kube-196 ~]# less /var/log/pods/default_counter_9ce0c974-91a8-11e9-9cd8-fa163e1e5fde/count/0.log
{"log":"1081: Tue Jun 18 09:26:22 UTC 2019\n","stream":"stdout","time":"2019-06-18T09:26:22.866910994Z"}
{"log":"1082: Tue Jun 18 09:26:23 UTC 2019\n","stream":"stdout","time":"2019-06-18T09:26:23.868754044Z"}
{"log":"1083: Tue Jun 18 09:26:24 UTC 2019\n","stream":"stdout","time":"2019-06-18T09:26:24.870266589Z"}
{"log":"1084: Tue Jun 18 09:26:25 UTC 2019\n","stream":"stdout","time":"2019-06-18T09:26:25.871929216Z"}

symlink /var/log/containers is pointing to /var/log/pods/pod_UID/*.logs and which is also a symlink to /var/lib/docker/containers/…

[root@kube-197 kube-proxy]# cd /var/log/containers/
[root@kube-197 containers]# ll
total 112
lrwxrwxrwx 1 root root 101 Jun 25 10:47 apm-els-elasticsearch-master-2_default_chown-2a0eea1ac1a4c4a8fe66fa5ab16bbdcf896fde9c5e752abdc5bbe6340fd00a17.log -> /var/log/pods/default_apm-els-elasticsearch-master-2_ac94ba63-96f2-11e9-9cd8-fa163e1e5fde/chown/0.log
lrwxrwxrwx 1 root root 109 Jun 25 10:47 apm-els-elasticsearch-master-2_default_elasticsearch-e6727337b8d1146868f480ce829a13100629aa78f61786c1aa8823850bfdfbc5.log -> /var/log/pods/default_apm-els-elasticsearch-master-2_ac94ba63-96f2-11e9-9cd8-fa163e1e5fde/elasticsearch/0.log

2 Node层面


每个容器通过stdout和stderr将日志输出到宿主机的日志文件中。

通过在每个节点上运行一个日志收集的agent来采集日志数据,日志采集agent是一种专用工具,用于将日志数据推送到统一的后端。一般来说,这种agent用一个容器来运行,可以访问该节点上所有应用程序容器的日志文件所在目录。

由于这种agent必须在每个节点上运行,所以直接使用 DaemonSet控制器运行该应用程序即可。在节点上运行一个日志收集的agent这种方式是最常见的一直方法,因为它只需要在每个节点上运行一个代理程序,并不需要对节点上运行的应用程序进行更改,对应用程序没有任何侵入性,但是这种方法也仅仅适用于收集输出到 stdout 和 stderr 的应用程序日志。

Note: Docker json日志驱动将每一行当做一条独立的
消息。如果想支持多行消息,需要在agent或者更高的层面去自行处理。

Note:如果使用了logrotate,那么只能获取最新的文件,比如有个15MB的文件,通过logrotate分割成一个10MB和一个5MB的文件,那么kubectl logs只能查看5MB的文件。

系统组件日志

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

Kubernetes的scheduler和kube-proxy在容器中运行。
kubelet和容器运行时(例如Docker)不在容器中运行。
对于系统级的日志文件,也是存放在/var/log目录下,也需要配置logrotate分割。

集群方案

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

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

上述机制,就是我们今天要讲解的容器日志收集的基础假设。而如果你的应用是把文件输出到其他地方,比如直接输出到了容器里的某个文件里,或者输出到了远程存储里,那就属于特殊情况了。当然,我在文章里也会对这些特殊情况的处理方法进行讲述。

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

  • 每个node部署一个node级别的代理
  • 每个应用的pod中集成一个sidecar容器
  • 从应用内部直接推送日志到后端

每个node部署一个代理

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

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

举个例子,我们可以通过 Fluentd 项目作为宿主机上的 logging-agent,然后把日志转发到远端的 ElasticSearch 里保存起来供将来进行检索。

因为k8s集群已经将pod日志在集群的/var/log/container/ 目录下做了软连接(xxxx.log)到pod所在 /var/lib/docker/containers/xxxxx/-json.log 文件中,只需要将fluentd监控相关/var/log/container/ 目录下的.log 文件,即可达到搜集日志的效果,同时由于在 docker 层设置了日志分割方式,将日志占用空间做了限制,基本上不会出现日志过期不能清理的问题。

  "log-opts": {
                "max-size": "100m"
        },

另外,在很多 Kubernetes 的部署里,会自动为你启用 logrotate,在日志文件超过10MB的时候自动对日志文件进行 rotate 操作。

可以看到,在Node上部署logging agent 最大的优点,在于一个节点只需要部署一个agent,并且不会对应用和 Pod有任何侵入性。所以,这个方案,在社区里是最常用的一种。

但是也不难看到,这种方案的不足之处就在于,它要求应用输出的日志,都必须是直接输出到容器的stdout和stderr 里。

具体参考官网的方案配置:https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-elasticsearch

每个应用的pod中集成一个sidecar容器

使用sidecar容器方式,有两种方式:

  • sidecar将应用日志输出到stdout
  • sidecar运行日志代理

Streaming sidecar container

所以,Kubernetes容器日志方案的第二种,就是对这种特殊情况的一个处理,即:当容器的日志只能输出到某些文件里的时候,我们可以通过一个sidecar容器把这些日志文件重新输出到 sidecar的stdout和stderr上,这样就能够继续使用第一种方案了。这个方案的具体工作原理,如下图所示:

比如,现在我的应用Pod只有一个容器,它会把日志输出到容器里的 /var/log/1.log 和 2.log 这两个文件里。这个 Pod 的 YAML 文件如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: 192.168.200.197:80/test-private/busybox:latest
    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 命令是看不到
应用的任何日志的;/var/log/pods也没有日志文件。而且我们前面讲解的、最常用的方案一,也是没办法使用的。

那么这个时候,我们就可以为这个 Pod 添加两个sidecar 容器,分别将上述两个日志文件里的内容重新以 stdout 和 stderr 的方式输出出来,这个 YAML 文件的写法如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: 192.168.200.197:80/test-private/busybox:latest
    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: 192.168.200.197:80/test-private/busybox:latest
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: 192.168.200.197:80/test-private/busybox:latest
    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 container with a logging agent

If the node-level logging agent is not flexible enough for your situation, you can create a sidecar container with a separate logging agent that you have configured specifically to run with your application.

这种方式有两个缺点:

  1. 消耗比较多的资源,应该每个pod都有一个agent;
  2. 不能通过kubectl logs命令查看日志;

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

admin/logging/fluentd-sidecar-config.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluentd.conf: |
    
      type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag count.format1
    

    
      type tail
      format none
      path /var/log/2.log
      pos_file /var/log/2.log.pos
      tag count.format2
    

    
      type google_cloud
    

上面的配置文件是配置收集原文件 /var/log/1.log 和 /var/log/2.log 的日志数据,然后通过 google_cloud 这个插件将数据推送到 Stackdriver 后端去。

下面是我们使用上面的配置文件在应用程序中运行一个 fluentd 的容器来读取日志数据:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: 192.168.200.197:80/test-private/busybox:latest
    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

上面的Pod创建完成后,count-agent容器就会将 count 容器中的日志进行收集然后上传。当然,这只是一个简单的示例,我们也完全可以使用其他的任何日志采集工具来替换 fluentd,比如 logstash、fluent-bit 等等。

从应用内部直接推送日志到后端

除了容器应用的日志之外,还有一个重要的部分就是,程序自身的日志。一般情况下,会将程序日志写到固定目录文件、或者写入到中间件保存和读取。

这部分日志的处理,可以直接通过日志文件收集、或者是让开发程序的同学修改程序把日志输出到中间件或者是直接输出到elk上

总结

在本篇文章中,我们详细讲解了Kubernetes项目对容器应用日志的收集方式。综合对比以上三种方案,个人倾向于使用第一种方式,将应用日志输出到stdout和stderr,然后通过在宿主机上部署logging-agent的方式来集中处理日志。这种方案不仅管理简单,kubectl logs 也可以用,而且可靠性高,并且宿主机本身,很可能就自带了rsyslogd等非常成熟的日志收集组件来供你使用。

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

参考

https://kubernetes.io/docs/concepts/cluster-administration/logging/

你可能感兴趣的:(云原生)