Kubernetes容器日志收集

  • 2019 年 12 月 4 日
  • 笔记

日志采集方式

日志从传统方式演进到容器方式的过程就不详细讲了,可以参考一下这篇文章Docker日志收集最佳实践,由于容器的漂移、自动伸缩等特性,日志收集也就必须使用新的方式来实现,Kubernetes官方给出的方式基本是这三种:原生方式、DaemonSet方式和Sidecar方式。

1.原生方式:使用 kubectl logs 直接在查看本地保留的日志,或者通过docker engine的 log driver 把日志重定向到文件、syslog、fluentd等系统中。

2.DaemonSet方式:在K8S的每个node上部署日志agent,由agent采集所有容器的日志到服务端。

3.Sidecar方式:一个POD中运行一个sidecar的日志agent容器,用于采集该POD主容器产生的日志。

三种方式都有利有弊,没有哪种方式能够完美的解决100%!问(MISSING)题的,所以要根据场景来贴合。

一、原生方式

简单的说,原生方式就是直接使用kubectl logs来查看日志,或者将docker的日志通过日志驱动来打到syslog、journal等去,然后再通过命令来排查,这种方式最好的优势就是简单、资源占用率低等,但是,在多容器、弹性伸缩情况下,日志的排查会十分困难,仅仅适用于刚开始研究Kubernetes的公司吧。不过,原生方式确实其他两种方式的基础,因为它的两种最基础的理念,daemonset和sidecar模式都是基于这两种方式而来的。

1.1 控制台stdout方式

这种方式是daemonset方式的基础。将日志全部输出到控制台,然后docker开启journal,然后就能在/var/log/journal下面看到二进制的journal日志,如果要查看二进制的日志的话,可以使用journalctl来查看日志:journalctl -u docker.service -n 1 –no-pager -o json -o json-pretty

{          "__CURSOR" : "s=113d7df2f5ff4d0985b08222b365c27a;i=1a5744e3;b=05e0fdf6d1814557939e52c0ac7ea76c;m=5cffae4cd4;t=58a452ca82da8;x=29bef852bcd70ae2",          "__REALTIME_TIMESTAMP" : "1559404590149032",          "__MONOTONIC_TIMESTAMP" : "399426604244",          "_BOOT_ID" : "05e0fdf6d1814557939e52c0ac7ea76c",          "PRIORITY" : "6",          "CONTAINER_ID_FULL" : "f2108df841b1f72684713998c976db72665f353a3b4ea17cd06b5fc5f0b8ae27",          "CONTAINER_NAME" : "k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1",          "CONTAINER_TAG" : "f2108df841b1",          "CONTAINER_ID" : "f2108df841b1",          "_TRANSPORT" : "journal",          "_PID" : "6418",          "_UID" : "0",          "_GID" : "0",          "_COMM" : "dockerd-current",          "_EXE" : "/usr/bin/dockerd-current",          "_CMDLINE" : "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry hub.gcloud.lab --insecure-registry 172.30.0.0/16 --log-level=warn --signature-verification=false --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",          "_CAP_EFFECTIVE" : "1fffffffff",          "_SYSTEMD_CGROUP" : "/system.slice/docker.service",          "_SYSTEMD_UNIT" : "docker.service",          "_SYSTEMD_SLICE" : "system.slice",          "_MACHINE_ID" : "225adcce13bd233a56ab481df7413e0b",          "_HOSTNAME" : "dev4.gcloud.set",          "MESSAGE" : "I0601 23:56:30.148153       1 event.go:221] Event(v1.ObjectReference{Kind:"DaemonSet", Namespace:"openshift-monitoring", Name:"node-exporter", UID:"f6d2bdc1-6658-11e9-aca2-fa163e938959", APIVersion:"apps/v1", ResourceVersion:"15378688", FieldPath:""}): type: ''Normal'' reason: ''SuccessfulCreate'' Created pod: node-exporter-hvrpf",          "_SOURCE_REALTIME_TIMESTAMP" : "1559404590148488"  }

在上面的json中,_CMDLINE以及其他字段占用量比较大,而且这些没有什么意义,会导致一条简短的日志却被封装成多了几十倍的量,所以的在日志量特别大的情况下,最好进行一下字段的定制,能够减少就减少。

我们一般需要的字段是CONTAINER_NAME以及MESSAGE,通过CONTAINER_NAME可以获取到Kubernetes的namespace和podName,比如CONTAINER_NAME为k8s_controllers_master-controllers-dev4.gcloud.set_kube-system_dcab37be702c9ab6c2b17122c867c74a_1的时候

container name in pod: controllers

pod name: master-controllers-dev4.gcloud.set

namespace: kube-system

pod uid: dcab37be702c9ab6c2b17122c867c74a_1

1.2 新版本的subPathExpr

journal方式算是比较标准的方式,如果采用hostPath方式,能够直接将日志输出这里。这种方式唯一的缺点就是在旧Kubernetes中无法获取到podName,但是最新版的Kubernetes1.14的一些特性subPathExpr,就是可以将目录挂载的时候同时将podName写进目录里,但是这个特性仍旧是alpha版本,谨慎使用。

简单说下实现原理:容器中填写的日志目录,挂载到宿主机的/data/logs/namespace/service_name/$(PodName)/xxx.log里面,如果是sidecar模式,则将改目录挂载到sidecar的收集目录里面进行推送。如果是宿主机安装fluentd模式,则需要匹配编写代码实现识别namespace、service_name、PodName等,然后发送到日志系统。

可参考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md

日志落盘参考细节:

    env:      - name: POD_NAME        valueFrom:          fieldRef:            apiVersion: v1            fieldPath: metadata.name     ...      volumeMounts:      - name: workdir1        mountPath: /logs        subPathExpr: $(POD_NAME)

我们主要使用了在Pod里的主容器挂载了一个fluent-agent的收集器,来将日志进行收集,其中我们修改了Kubernetes-Client的源码使之支持subPathExpr,然后发送到日志系统的kafka。这种方式能够处理多种日志的收集,比如业务方的日志打到控制台了,但是jvm的日志不能同时打到控制台,否则会发生错乱,所以,如果能够将业务日志挂载到宿主机上,同时将一些其他的日志比如jvm的日志挂载到容器上,就可以使用该种方式。

{      "_fileName":"/data/work/logs/epaas_2019-05-22-0.log",      "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",      "_collectTime":"2019-05-22 17:23:58",      "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",      "_domain":"rongqiyun-dev",      "_podName":"aofjweojo-5679849765-gncbf",      "_hostName":"dev4.gcloud.set"  }

二、Daemonset方式

daemonset方式也是基于journal,日志使用journal的log-driver,变成二进制的日志,然后在每个node节点上部署一个日志收集的agent,挂载/var/log/journal的日志进行解析,然后发送到kafka或者es,如果节点或者日志量比较大的话,对es的压力实在太大,所以,我们选择将日志推送到kafka。容器日志收集普遍使用fluentd,资源要求较少,性能高,是目前最成熟的日志收集方案,可惜是使用了ruby来写的,普通人根本没时间去话时间学习这个然后进行定制,好在openshift中提供了origin-aggregated-logging方案。

我们可以通过fluent.conf来看origin-aggregated-logging做了哪些工作,把注释,空白的一些东西去掉,然后我稍微根据自己的情况修改了下,结果如下:

@include configs.d/openshift/system.conf  设置fluent的日志级别  @include configs.d/openshift/input-pre-*.conf  最主要的地方,读取journal的日志  @include configs.d/dynamic/input-syslog-*.conf  读取syslog,即操作日志  <label @INGRESS>    @include configs.d/openshift/filter-retag-journal.conf    进行匹配    @include configs.d/openshift/filter-k8s-meta.conf    获取Kubernetes的相关信息    @include configs.d/openshift/filter-viaq-data-model.conf    进行模型的定义    @include configs.d/openshift/filter-post-*.conf    生成es的索引id    @include configs.d/openshift/filter-k8s-record-transform.conf    修改日志记录,我们在这里进行了字段的定制,移除了不需要的字段    @include configs.d/openshift/output-applications.conf    输出,默认是es,如果想使用其他的比如kafka,需要自己定制  </label>

当然,细节上并没有那么好理解,换成一步步理解如下:

1. 解析journal日志

origin-aggregated-logging会将二进制的journal日志中的CONTAINER_NAME进行解析,根据匹配规则将字段进行拆解

    "kubernetes": {        "container_name": "fas-dataservice-dev-new",        "namespace_name": "fas-cost-dev",        "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",        "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",        "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"      }

2. es封装

主要用的是elasticsearch_genid_ext插件,写在了filter-post-genid.conf上。

3. 日志分类

通过origin-aggregated-logging来收集journal的日志,然后推送至es,origin-aggregated-logging在推送过程中做了不少优化,即适应高ops的、带有等待队列的、推送重试等,详情可以具体查看一下。

还有就是对日志进行了分类,分为三种:

(1).操作日志(在es中以.operations*匹配的),记录了对Kubernetes的操作

(2).项目日志(在es中以project*匹配的),业务日志,日志收集中最重要的

(3).孤儿日志(在es中以.orphaned.*匹配的),没有namespace的日志都会打到这里

4. 日志字段定制

经过origin-aggregated-logging推送至后采集的一条日志如下:

{      "CONTAINER_TAG": "4ad125bb7558",      "docker": {        "container_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c"      },      "kubernetes": {        "container_name": "fas-dataservice-dev-new",        "namespace_name": "fas-cost-dev",        "pod_name": "fas-dataservice-dev-new-5c48d7c967-kb79l",        "pod_id": "4ad125bb7558f52e30dceb3c5e88dc7bc160980527356f791f78ffcaa6d1611c",        "namespace_id": "f95238a6-3a67-11e9-a211-20040fe7b690"      },      "systemd": {        "t": {          "BOOT_ID": "6246327d7ea441339d6d14b44498b177",          "CAP_EFFECTIVE": "1fffffffff",          "CMDLINE": "/usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled=false --log-driver=journald --insecure-registry hub.paas.kjtyun.com --insecure-registry 10.77.0.0/16 --log-level=warn --signature-verification=false --bridge=none --max-concurrent-downloads=20 --max-concurrent-uploads=20 --storage-driver devicemapper --storage-opt dm.fs=xfs --storage-opt dm.thinpooldev=/dev/mapper/docker--vg-docker--pool --storage-opt dm.use_deferred_removal=true --storage-opt dm.use_deferred_deletion=true --mtu=1450",          "COMM": "dockerd-current",          "EXE": "/usr/bin/dockerd-current",          "GID": "0",          "MACHINE_ID": "0096083eb4204215a24efd202176f3ec",          "PID": "17181",          "SYSTEMD_CGROUP": "/system.slice/docker.service",          "SYSTEMD_SLICE": "system.slice",          "SYSTEMD_UNIT": "docker.service",          "TRANSPORT": "journal",          "UID": "0"        }      },      "level": "info",      "message": "tat com.sun.proxy.$Proxy242.execute(Unknown Source)",      "hostname": "host11.rqy.kx",      "pipeline_metadata": {        "collector": {          "ipaddr4": "10.76.232.16",          "ipaddr6": "fe80::a813:abff:fe66:3b0c",          "inputname": "fluent-plugin-systemd",          "name": "fluentd",          "received_at": "2019-05-15T09:22:39.297151+00:00",          "version": "0.12.43 1.6.0"        }      },      "@timestamp": "2019-05-06T01:41:01.960000+00:00",      "viaq_msg_id": "NjllNmI1ZWQtZGUyMi00NDdkLWEyNzEtMTY3MDQ0ZjEyZjZh"    }

可以看出,跟原生的journal日志类似,增加了几个字段为了写进es中而已,总体而言,其他字段并没有那么重要,所以我们对其中的字段进行了定制,以减少日志的大小,定制化字段之后,一段日志的输出变为(不是同一段,只是举个例子):

{      "hostname":"dev18.gcloud.set",      "@timestamp":"2019-05-17T04:22:33.139608+00:00",      "pod_name":"istio-pilot-8588fcb99f-rqtkd",      "appName":"discovery",      "container_name":"epaas-discovery",      "domain":"istio-system",      "sortedId":"NjA3ODVhODMtZDMyYy00ZWMyLWE4NjktZjcwZDMwMjNkYjQ3",      "log":"spiffluster.local/ns/istio-system/sa/istio-galley-service-account"  }

5.部署

最后,在node节点上添加logging-infra-fluentd: "true"的标签,就可以在namespace为openshift-logging中看到节点的收集器了。

logging-fluentd-29p8z              1/1       Running   0          6d  logging-fluentd-bpkjt              1/1       Running   0          6d  logging-fluentd-br9z5              1/1       Running   0          6d  logging-fluentd-dkb24              1/1       Running   1          5d  logging-fluentd-lbvbw              1/1       Running   0          6d  logging-fluentd-nxmk9              1/1       Running   1          5d

6.关于ip

业务方不仅仅想要podName,同时还有对ip的需求,控制台方式正常上是没有记录ip的,所以这算是一个难点中的难点,我们在kubernetes_metadata_common.rb的kubernetes_metadata中添加了 ''pod_ip'' => pod_object''status'',最终是有些有ip,有些没有ip,这个问题我们继续排查。

三、Sidecar模式

这种方式的好处是能够获取日志的文件名、容器的ip地址等,并且配置性比较高,能够很好的进行一系列定制化的操作,比如使用log-pilot或者filebeat或者其他的收集器,还能定制一些特定的字段,比如文件名、ip地址等。

sidecar模式用来解决日志收集的问题的话,需要将日志目录挂载到宿主机的目录上,然后再mount到收集agent的目录里面,以达到文件共享的目的,默认情况下,使用emptydir来实现文件共享的目的,这里简单介绍下emptyDir的作用。

EmptyDir类型的volume创建于pod被调度到某个宿主机上的时候,而同一个pod内的容器都能读写EmptyDir中的同一个文件。一旦这个pod离开了这个宿主机,EmptyDir中的数据就会被永久删除。所以目前EmptyDir类型的volume主要用作临时空间,比如Web服务器写日志或者tmp文件需要的临时目录。

日志如果丢失的话,会对业务造成的影响不可估量,所以,我们使用了尚未成熟的subPathExpr来实现,即挂载到宿主的固定目录/data/logs下,然后是namespace,deploymentName,podName,再然后是日志文件,合成一块便是/data/logs/${namespace}/${deploymentName}/${podName}/xxx.log。

具体的做法就不在演示了,这里只贴一下yaml文件。

apiVersion: extensions/v1beta1  kind: Deployment  metadata:    name: xxxx    namespace: element-dev  spec:    template:      spec:        volumes:          - name: host-log-path-0            hostPath:              path: /data/logs/element-dev/xxxx              type: DirectoryOrCreate        containers:          - name: xxxx            image: ''xxxxxxx''            volumeMounts:              - name: host-log-path-0                mountPath: /data/work/logs/                subPathExpr: $(POD_NAME)          - name: xxxx-elog-agent            image: ''agent''            volumeMounts:              - name: host-log-path-0                mountPath: /data/work/logs/                subPathExpr: $(POD_NAME)

fluent.conf的配置文件由于保密关系就不贴了,收集后的一条数据如下:

{      "_fileName":"/data/work/logs/xxx_2019-05-22-0.log",      "_sortedId":"660c2ce8-aacc-42c4-80d1-d3f6d4c071ea",      "_collectTime":"2019-05-22 17:23:58",      "_log":"[33m2019-05-22 17:23:58[0;39m |[34mINFO [0;39m |[34mmain[0;39m |[34mSpringApplication.java:679[0;39m |[32mcom.hqyg.epaas.EpaasPortalApplication[0;39m | The following profiles are active: dev",      "_domain":"namespace",      "_ip":"10.128.93.31",      "_podName":"xxxx-5679849765-gncbf",      "_hostName":"dev4.gcloud.set"  }

四、总结

总的来说,daemonset方式比较简单,而且适合更加适合微服务化,当然,不是完美的,比如业务方想把业务日志打到控制台上,但是同时也想知道jvm的日志,这种情况下或许sidecar模式更好。但是sidecar也有不完美的地方,每个pod里都要存在一个日志收集的agent实在是太消耗资源了,而且很多问题也难以解决,比如:主容器挂了,agent还没收集完,就把它给kill掉,这个时候日志怎么处理,业务会不会受到要杀掉才能启动新的这一短暂过程的影响等。所以,我们实际使用中首选daemonset方式,但是提供了sidecar模式让用户选择。

参考:

1.Kubernetes日志官方文档

2.Kubernetes日志采集Sidecar模式介绍

3.Docker日志收集最佳实践