Kubernetes容器日誌收集
日誌採集方式
日誌從傳統方式演進到容器方式的過程就不詳細講了,可以參考一下這篇文章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主容器產生的日誌。
一、原生方式
簡單的說,原生方式就是直接使用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']['podIP'],最終是有些有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日誌收集最佳實