日誌從傳統方式演進到容器方式的過程就不詳細講了,能夠參考一下這篇文章Docker日誌收集最佳實踐,因爲容器的漂移、自動伸縮等特性,日誌收集也就必須使用新的方式來實現,Kubernetes官方給出的方式基本是這三種:原生方式、DaemonSet方式和Sidecar方式。html
1.原生方式:使用 kubectl logs 直接在查看本地保留的日誌,或者經過docker engine的 log driver 把日誌重定向到文件、syslog、fluentd等系統中。
2.DaemonSet方式:在K8S的每一個node上部署日誌agent,由agent採集全部容器的日誌到服務端。
3.Sidecar方式:一個POD中運行一個sidecar的日誌agent容器,用於採集該POD主容器產生的日誌。
三種方式都有利有弊,沒有哪一種方式可以完美的解決100%問題的,因此要根據場景來貼合。java
簡單的說,原生方式就是直接使用kubectl logs來查看日誌,或者將docker的日誌經過日誌驅動來打到syslog、journal等去,而後再經過命令來排查,這種方式最好的優點就是簡單、資源佔用率低等,可是,在多容器、彈性伸縮狀況下,日誌的排查會十分困難,僅僅適用於剛開始研究Kubernetes的公司吧。不過,原生方式確實其餘兩種方式的基礎,由於它的兩種最基礎的理念,daemonset和sidecar模式都是基於這兩種方式而來的。node
這種方式是daemonset方式的基礎。將日誌所有輸出到控制檯,而後docker開啓journal,而後就能在/var/log/journal下面看到二進制的journal日誌,若是要查看二進制的日誌的話,可使用journalctl來查看日誌:journalctl -u docker.service -n 1 --no-pager -o json -o json-prettylinux
{ "__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_1git
journal方式算是比較標準的方式,若是採用hostPath方式,可以直接將日誌輸出這裏。這種方式惟一的缺點就是在舊Kubernetes中沒法獲取到podName,可是最新版的Kubernetes1.14的一些特性subPathExpr,就是能夠將目錄掛載的時候同時將podName寫進目錄裏,可是這個特性仍舊是alpha版本,謹慎使用。
簡單說下實現原理:容器中填寫的日誌目錄,掛載到宿主機的/data/logs/namespace/service_name/$(PodName)/xxx.log裏面,若是是sidecar模式,則將改目錄掛載到sidecar的收集目錄裏面進行推送。若是是宿主機安裝fluentd模式,則須要匹配編寫代碼實現識別namespace、service_name、PodName等,而後發送到日誌系統。github
可參考:https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20181029-volume-subpath-env-expansion.md
日誌落盤參考細節:docker
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的日誌掛載到容器上,就可使用該種方式。json
{ "_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方式也是基於journal,日誌使用journal的log-driver,變成二進制的日誌,而後在每一個node節點上部署一個日誌收集的agent,掛載/var/log/journal的日誌進行解析,而後發送到kafka或者es,若是節點或者日誌量比較大的話,對es的壓力實在太大,因此,咱們選擇將日誌推送到kafka。容器日誌收集廣泛使用fluentd,資源要求較少,性能高,是目前最成熟的日誌收集方案,惋惜是使用了ruby來寫的,普通人根本沒時間去話時間學習這個而後進行定製,好在openshift中提供了origin-aggregated-logging方案。
咱們能夠經過fluent.conf來看origin-aggregated-logging作了哪些工做,把註釋,空白的一些東西去掉,而後我稍微根據本身的狀況修改了下,結果以下:api
@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>
固然,細節上並無那麼好理解,換成一步步理解以下:ruby
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']['podIP'],最終是有些有ip,有些沒有ip,這個問題咱們繼續排查。
這種方式的好處是可以獲取日誌的文件名、容器的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日誌收集最佳實踐