1. 程式人生 > >Kubernetes容器日誌收集

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主容器產生的日誌。

三種方式都有利有弊,沒有哪種方式能夠完美的解決100%問題的,所以要根據場景來貼合。

一、原生方式


簡單的說,原生方式就是直接使用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日誌收集最佳實