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等,然後發送到日誌系統。
日誌落盤參考細節:
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模式讓用戶選擇。
參考: