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日誌收集最佳實踐