1. 程式人生 > 實用技巧 >EFK收集Kubernetes叢集日誌資訊

EFK收集Kubernetes叢集日誌資訊

一、k8s日誌收集架構

官方連結:https://kubernetes.io/docs/concepts/cluster-administration/logging/

總體分為三種方式:

  • 使用在每個節點上執行的節點級日誌記錄代理。
  • 在應用程式的 pod 中,包含專門記錄日誌的 sidecar 容器。
  • 將日誌直接從應用程式中推送到日誌記錄後端。

1)使用節點級日誌代理

容器日誌驅動:

https://docs.docker.com/config/containers/logging/configure/

檢視當前的docker主機的驅動:

$ docker info --format '{{.LoggingDriver}}'

json-file格式,docker會預設將標準和錯誤輸出儲存為宿主機的檔案,路徑為:

/var/lib/docker/containers/<container-id>/<container-id>-json.log

並且可以設定日誌輪轉:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "labels": "production_status",
    "env": "os,customer"
  }
}

優勢:

  • 部署方便,使用DaemonSet型別控制器來部署agent即可
  • 對業務應用的影響最小,沒有侵入性

劣勢:

  • 只能收集標準和錯誤輸出,對於容器內的檔案日誌,暫時收集不到

2)使用 sidecar 容器和日誌代理

  • 方式一:sidecar 容器將應用程式日誌傳送到自己的標準輸出。

    思路:在pod中啟動一個sidecar容器,把容器內的日誌檔案吐到標準輸出,由宿主機中的日誌收集agent進行採集。

$ cat count-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-1
    image: busybox
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: busybox
    args: [/bin/sh, -c, 'tail -n+1 -f /var/log/2.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}
    
$ kubectl create -f counter-pod.yaml
$ kubectl logs -f counter -c count-log-1

優勢:

- 可以實現容器內部日誌收集
- 對業務應用的侵入性不大

劣勢:

- 每個業務pod都需要做一次改造
- 增加了一次日誌的寫入,對磁碟使用率有一定影響
  • 方式二:sidecar 容器執行一個日誌代理,配置該日誌代理以便從應用容器收集日誌。

思路:直接在業務Pod中使用sidecar的方式啟動一個日誌收集的元件(比如fluentd),這樣日誌收集可以將容器內的日誌當成本地檔案來進行收取。

優勢:不用往宿主機儲存日誌,本地日誌完全可以收集

劣勢:每個業務應用額外啟動一個日誌agent,帶來額外的資源損耗

3)從應用中直接暴露日誌目錄

4)企業日誌方案選型

目前來講,最建議的是採用節點級的日誌代理。

方案一:自研方案,實現一個自研的日誌收集agent,大致思路:

  • 針對容器的標準輸出及錯誤輸出,使用常規的方式,監聽宿主機中的容器輸出路徑即可
  • 針對容器內部的日誌檔案
    • 在容器內配置統一的環境變數,比如LOG_COLLECT_FILES,指定好容器內待收集的日誌目錄及檔案
    • agent啟動的時候掛載docker.sock檔案及磁碟的根路徑
    • 監聽docker的容器新建、刪除事件,通過docker的api,查出容器的儲存、環境變數、k8s屬性等資訊
    • 配置了LOG_COLLECT_FILES環境變數的容器,根據env中的日誌路徑找到主機中對應的檔案路徑,然後生成收集的配置檔案
    • agent與開源日誌收集工具(Fluentd或者filebeat等)配合,agent負責下發配置到收集工具中並對程序做reload

方案二:日誌使用開源的Agent進行收集(EFK方案),適用範圍廣,可以滿足絕大多數日誌收集、展示的需求。

二、實踐使用EFK實現業務日誌收集

1)EFK架構工作流程

  • Elasticsearch

    一個開源的分散式、Restful 風格的搜尋和資料分析引擎,它的底層是開源庫Apache Lucene。它可以被下面這樣準確地形容:

    • 一個分散式的實時文件儲存,每個欄位可以被索引與搜尋;
    • 一個分散式實時分析搜尋引擎;
    • 能勝任上百個服務節點的擴充套件,並支援 PB 級別的結構化或者非結構化資料。
  • Kibana

    Kibana是一個開源的分析和視覺化平臺,設計用於和Elasticsearch一起工作。可以通過Kibana來搜尋,檢視,並和儲存在Elasticsearch索引中的資料進行互動。也可以輕鬆地執行高階資料分析,並且以各種圖示、表格和地圖的形式視覺化資料。

  • Fluentd

    一個針對日誌的收集、處理、轉發系統。通過豐富的外掛系統,可以收集來自於各種系統或應用的日誌,轉化為使用者指定的格式後,轉發到使用者所指定的日誌儲存系統之中。

Fluentd 通過一組給定的資料來源抓取日誌資料,處理後(轉換成結構化的資料格式)將它們轉發給其他服務,比如 Elasticsearch、物件儲存、kafka等等。Fluentd 支援超過300個日誌儲存和分析服務,所以在這方面是非常靈活的。主要執行步驟如下

1. 首先 Fluentd 從多個日誌源獲取資料
2. 結構化並且標記這些資料
3. 然後根據匹配的標籤將資料傳送到多個目標服務

2)Fluentd精講

(1)Fluentd架構

為什麼推薦使用fluentd作為k8s體系的日誌收集工具?

  • 可插拔架構設計

  • 極小的資源佔用

    基於C和Ruby語言, 30-40MB,13,000 events/second/core

  • 極強的可靠性

    • 基於記憶體和本地檔案的快取
    • 強大的故障轉移
(2)fluentd事件流的生命週期及指令配置

https://docs.fluentd.org/v/0.12/quickstart/life-of-a-fluentd-event

Input -> filter 1 -> ... -> filter N -> Buffer -> Output

啟動命令

$ fluentd -c fluent.conf

指令介紹:

  • source ,資料來源,對應Input
    通過使用 source 指令,來選擇和配置所需的輸入外掛來啟用 Fluentd 輸入源, source 把事件提交到 fluentd 的路由引擎中。使用type來區分不同型別的資料來源。如下配置可以監聽指定檔案的追加輸入:

    <source>
      @type tail
      path /var/log/httpd-access.log
      pos_file /var/log/td-agent/httpd-access.log.pos
      tag myapp.access
      format apache2
    </source>
    
    
  • filter,Event processing pipeline(事件處理流)

    filter 可以串聯成 pipeline,對資料進行序列處理,最終再交給 match 輸出。 如下可以對事件內容進行處理:

    <source>
      @type http
      port 9880
    </source>
    
    <filter myapp.access>
      @type record_transformer
      <record>
        host_param “#{Socket.gethostname}”
      </record>
    </filter>
    

    filter 獲取資料後,呼叫內建的 @type record_transformer 外掛,在事件的 record 裡插入了新的欄位 host_param,然後再交給 match 輸出。

  • label指令

    可以在 source 裡指定 @label,這個 source 所觸發的事件就會被髮送給指定的 label 所包含的任務,而不會被後續的其他任務獲取到。

    <source>
      @type forward
    </source>
    
    <source>
    ### 這個任務指定了 label 為 @SYSTEM
    ### 會被髮送給 <label @SYSTEM>
    ### 而不會被髮送給下面緊跟的 filter 和 match
      @type tail
      @label @SYSTEM
      path /var/log/httpd-access.log
      pos_file /var/log/td-agent/httpd-access.log.pos
      tag myapp.access
      format apache2
    </source>
    
    <filter access.**>
      @type record_transformer
      <record>
      # …
      </record>
    </filter>
    
    <match **>
      @type elasticsearch
      # …
    </match>
    
    <label @SYSTEM>
      ### 將會接收到上面 @type tail 的 source event
      <filter var.log.middleware.**>
        @type grep
        # …
      </filter>
    
      <match **>
        @type s3
        # …
      </match>
    </label>
    
  • match,匹配輸出

    查詢匹配 “tags” 的事件,並處理它們。match 命令的最常見用法是將事件輸出到其他系統(因此,與 match 命令對應的外掛稱為 “輸出外掛”)

    <source>
      @type http
      port 9880
    </source>
    
    <filter myapp.access>
      @type record_transformer
      <record>
        host_param “#{Socket.gethostname}”
      </record>
    </filter>
    
    <match myapp.access>
      @type file
      path /var/log/fluent/access
    </match>
    

事件的結構:

time:事件的處理時間

tag:事件的來源,在fluentd.conf中配置

record:真實的日誌內容,json物件

比如,下面這條原始日誌:

192.168.0.1 - - [28/Feb/2013:12:00:00 +0900] "GET / HTTP/1.1" 200 777

經過fluentd 引擎處理完後的樣子可能是:

2020-07-16 08:40:35 +0000 apache.access: {"user":"-","method":"GET","code":200,"size":777,"host":"192.168.0.1","path":"/"}
(3)fluentd的buffer事件緩衝模型
Input -> filter 1 -> ... -> filter N -> Buffer -> Output

因為每個事件資料量通常很小,考慮資料傳輸效率、穩定性等方面的原因,所以基本不會每條事件處理完後都會立馬寫入到output端,因此fluentd建立了緩衝模型,模型中主要有兩個概念:

  • buffer_chunk:事件緩衝塊,用來儲存本地已經處理完待發送至目的端的事件,可以設定每個塊的大小。
  • buffer_queue:儲存chunk的佇列,可以設定長度

可以設定的引數,主要有:

  • buffer_type,緩衝型別,可以設定file或者memory
  • buffer_chunk_limit,每個chunk塊的大小,預設8MB
  • buffer_queue_limit ,chunk塊佇列的最大長度,預設256
  • flush_interval ,flush一個chunk的時間間隔
  • retry_limit ,chunk塊傳送失敗重試次數,預設17次,之後就丟棄該chunk資料
  • retry_wait ,重試傳送chunk資料的時間間隔,預設1s,第2次失敗再發送的話,間隔2s,下次4秒,以此類推

大致的過程為:

隨著fluentd事件的不斷生成並寫入chunk,快取塊持變大,當快取塊滿足buffer_chunk_limit大小或者新的快取塊誕生超過flush_interval時間間隔後,會推入快取queue佇列尾部,該佇列大小由buffer_queue_limit決定。

每次有新的chunk入列,位於佇列最前部的chunk塊會立即寫入配置的儲存後端,比如配置的是kafka,則立即把資料推入kafka中。

比較理想的情況是每次有新的快取塊進入快取佇列,則立馬會被寫入到後端,同時,新快取塊也持續入列,但是入列的速度不會快於出列的速度,這樣基本上快取佇列處於空的狀態,佇列中最多隻有一個快取塊。

但是實際情況考慮網路等因素,往往快取塊被寫入後端儲存的時候會出現延遲或者寫入失敗的情況,當快取塊寫入後端失敗時,該快取塊還會留在佇列中,等retry_wait時間後重試傳送,當retry的次數達到retry_limit後,該快取塊被銷燬(資料被丟棄)。

此時快取佇列持續有新的快取塊進來,如果佇列中存在很多未及時寫入到後端儲存的快取塊的話,當佇列長度達到buffer_queue_limit大小,則新的事件被拒絕,fluentd報錯,error_class=Fluent::Plugin::Buffer::BufferOverflowError error="buffer space has too many data"。

還有一種情況是網路傳輸緩慢的情況,若每3秒鐘會產生一個新塊,但是寫入到後端時間卻達到了30s鍾,佇列長度為100,那麼每個塊出列的時間內,又有新的10個塊進來,那麼佇列很快就會被佔滿,導致異常出現。

(4)實踐一:實現業務應用日誌的收集及欄位解析

目標:收集容器內的nginx應用的access.log日誌,並解析日誌欄位為JSON格式,原始日誌的格式為:

$ tail -f access.log
...
53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https

收集並處理成:

{
    "serverIp": "53.49.146.149",
    "timestamp": "1561620585.973",
    "respondTime": "0.005",
    "httpCode": "502",
    "eventTime": "27/Jun/2019:15:29:45 +0800",
    "clientIp": "178.73.215.171",
    "clientPort": "33337",
    "method": "GET",
    "protocol": "https"
}

思路:

  • 配置fluent.conf
    • 使用@tail外掛通過監聽access.log檔案
    • 用filter實現對nginx日誌格式解析
  • 啟動fluentd服務
  • 手動追加內容至access.log檔案
  • 觀察本地輸出內容是否符合預期

fluent.conf

<source>
	@type tail
	@label @nginx_access
	path /fluentd/access.log
	pos_file /fluentd/nginx_access.posg
	tag nginx_access
	format none
	@log_level trace
</source>
<label @nginx_access>
   <filter  nginx_access>
       @type parser
	   key_name message
	   format  /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/
   </filter>
   <match  nginx_access>
     @type stdout
   </match>
</label>

啟動服務,追加檔案內容:

$ docker run -u root --rm -ti 192.168.99.151:5000/fluentd_elasticsearch/fluentd:v2.5.2 sh
/ # cd /fluentd/
/ # touch access.log
/ # fluentd -c /fluentd/etc/fluent.conf
/ # echo '53.49.146.149 1561620585.973 0.005 502 [27/Jun/2019:15:29:45 +0800] 178.73.215.171 33337 GET https' >>/fluentd/access.log

使用該網站進行正則校驗: http://fluentular.herokuapp.com

(5)實踐二:使用ruby實現日誌欄位的轉換及自定義處理
<source>
	@type tail
	@label @nginx_access
	path /fluentd/access.log
	pos_file /fluentd/nginx_access.posg
	tag nginx_access
	format none
	@log_level trace
</source>
<label @nginx_access>
   <filter  nginx_access>
       @type parser
	   key_name message
	   format  /(?<serverIp>[^ ]*) (?<timestamp>[^ ]*) (?<respondTime>[^ ]*) (?<httpCode>[^ ]*) \[(?<eventTime>[^\]]*)\] (?<clientIp>[^ ]*) (?<clientPort>[^ ]*) (?<method>[^ ]*) (?<protocol>[^ ]*)/
   </filter>
   <filter  nginx_access>   
	   @type record_transformer
	   enable_ruby
       <record>
		host_name "#{Socket.gethostname}"
        my_key  "my_val"
        tls ${record["protocol"].index("https") ? "true" : "false"}
       </record>
   </filter>
   <match  nginx_access>
     @type stdout
   </match>
</label>

2)ConfigMap的配置檔案掛載使用場景

開始之前,我們先來回顧一下,configmap的常用的掛載場景。

(1)場景一:單檔案掛載到空目錄

假如業務應用有一個配置檔案,名為 application-1.conf,如果想將此配置掛載到pod的/etc/application/目錄中。

application-1.conf的內容為:

$ cat application-1.conf
name: "application"
platform: "linux"
purpose: "demo"
company: "lvzhenjiang"
version: "v2.1.0"

該配置檔案在k8s中可以通過configmap來管理,通常我們有如下兩種方式來管理配置檔案:

  • 通過kubectl命令列來生成configmap

    # 通過檔案直接建立
    $ kubectl -n default create configmap application-config --from-file=application-1.conf
    
    # 會生成配置檔案,檢視內容,configmap的key為檔名字
    $ kubectl -n default get cm application-config -oyaml
    
  • 通過yaml檔案直接建立

    $ cat application-config.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: application-config
      namespace: default
    data:
      application-1.conf: |
        name: "application"
        platform: "linux"
        purpose: "demo"
        company: "lvzhenjiang"
        version: "v2.1.0"
    
    # 建立configmap
    $ kubectl create -f application-config.yaml
    

準備一個demo-deployment.yaml檔案,掛載上述configmap到/etc/application/

$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      volumes:
      - configMap:
          name: application-config
        name: config
      containers:
      - name: nginx
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: "/etc/application"
          name: config

建立並檢視:

$ kubectl create -f demo-deployment.yaml

修改configmap檔案的內容,觀察pod中是否自動感知變化:

$ kubectl edit cm application-config

整個configmap檔案直接掛載到pod中,若configmap變化,pod會自動感知並拉取到pod內部。

但是pod內的程序不會自動重啟,所以很多服務會實現一個內部的reload介面,用來載入最新的配置檔案到程序中。

(2)場景二:多檔案掛載

假如有多個配置檔案,都需要掛載到pod內部,且都在一個目錄中

$ cat application-1.conf
name: "application-1"
platform: "linux"
purpose: "demo"
company: "lvzhenjiang"
version: "v2.1.0"
$ cat application-2.conf
name: "application-2"
platform: "linux"
purpose: "demo"
company: "lvzhenjiang"
version: "v2.1.0"

同樣可以使用兩種方式建立:

$ kubectl delete cm application-config

$ kubectl create cm application-config --from-file=application-1.conf --from-file=application-2.conf

$ kubectl get cm application-config -oyaml

觀察Pod已經自動獲取到最新的變化

$ kubectl exec demo-55c649865b-gpkgk ls /etc/application/
application-1.conf
application-2.conf

此時,是掛載到pod內的空目錄中/etc/application,假如想掛載到pod已存在的目錄中,比如:

$  kubectl exec   demo-55c649865b-gpkgk ls /etc/profile.d
color_prompt
locale

更改deployment的掛載目錄:

$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      volumes:
      - configMap:
          name: application-config
        name: config
      containers:
      - name: nginx
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: "/etc/profile.d"
          name: config

重建pod

$ kubectl apply -f demo-deployment.yaml

# 檢視pod內的/etc/profile.d目錄,發現已有檔案被覆蓋
$ kubectl exec demo-77d685b9f7-68qz7 ls /etc/profile.d
application-1.conf
application-2.conf
(3)場景三 掛載子路徑

實現多個配置檔案,可以掛載到pod內的不同的目錄中。比如:

  • application-1.conf掛載到/etc/application/
  • application-2.conf掛載到/etc/profile.d

configmap保持不變,修改deployment檔案:

$ cat demo-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: default
spec:
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      volumes:
      - name: config
        configMap:
          name: application-config
          items:
          - key: application-1.conf
            path: application1
          - key: application-2.conf
            path: application2
      containers:
      - name: nginx
        image: nginx:alpine
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - mountPath: "/etc/application/application-1.conf"
          name: config
          subPath: application1
        - mountPath: "/etc/profile.d/application-2.conf"
          name: config
          subPath: application2

測試掛載:

$ kubectl apply -f demo-deployment.yaml

$ kubectl exec demo-78489c754-shjhz ls /etc/application
application-1.conf

$ kubectl exec demo-78489c754-shjhz ls /etc/profile.d/
application-2.conf
color_prompt
locale

使用subPath掛載到Pod內部的檔案,不會自動感知原有ConfigMap的變更

3)部署es服務

(1)部署分析
  1. es生產環境是部署es叢集,通常會使用statefulset進行部署
  2. es預設使用elasticsearch使用者啟動程序,es的資料目錄是通過宿主機的路徑掛載,因此目錄許可權被主機的目錄許可權覆蓋,因此可以利用initContainer容器在es程序啟動之前把目錄的許可權修改掉,注意init container要用特權模式啟動。
  3. 若希望使用helm部署,參考 https://github.com/helm/charts/tree/master/stable/elasticsearch
使用StatefulSet管理有狀態服務

使用Deployment建立多副本的pod的情況:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: default
  labels:
    app: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx-deployment
  template:
    metadata:
      labels:
        app: nginx-deployment
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

使用StatefulSet建立多副本pod的情況:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx-statefulset
  namespace: default
  labels:
    app: nginx-sts
spec:
  replicas: 3
  serviceName: "nginx"
  selector:
    matchLabels:
      app: nginx-sts
  template:
    metadata:
      labels:
        app: nginx-sts
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

無頭服務Headless Service

kind: Service
apiVersion: v1
metadata:
  name: nginx
  namespace: default
spec:
  selector:
    app: nginx-sts
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  clusterIP: None
$ kubectl -n default exec  -ti nginx-statefulset-0 sh
/ # curl nginx-statefulset-2.nginx
(2)部署並驗證

es-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: es-config
  namespace: logging
data:
  elasticsearch.yml: |
    cluster.name: "lvzhenjiang-elasticsearch"
    node.name: "${POD_NAME}"
    network.host: 0.0.0.0
    discovery.seed_hosts: "es-svc-headless"
    cluster.initial_master_nodes: "elasticsearch-0,elasticsearch-1,elasticsearch-2"

es-svc-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: es-svc-headless
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  selector:
    k8s-app: elasticsearch
  clusterIP: None
  ports:
  - name: in
    port: 9300
    protocol: TCP

es-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  replicas: 3
  serviceName: es-svc-headless
  selector:
    matchLabels:
      k8s-app: elasticsearch
  template:
    metadata:
      labels:
        k8s-app: elasticsearch
    spec:
      initContainers:
      - command:
        - /sbin/sysctl
        - -w
        - vm.max_map_count=262144
        image: alpine:3.6
        imagePullPolicy: IfNotPresent
        name: elasticsearch-logging-init
        resources: {}
        securityContext:
          privileged: true
      - name: fix-permissions
        image: alpine:3.6
        command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
        securityContext:
          privileged: true
        volumeMounts:
        - name: es-data-volume
          mountPath: /usr/share/elasticsearch/data
      containers:
      - name: elasticsearch
        image: 192.168.99.151:5000/elasticsearch/elasticsearch:7.4.2
        env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
        resources:
          limits:
            cpu: '1'
            memory: 2Gi
          requests:
            cpu: '1'
            memory: 2Gi
        ports:
        - containerPort: 9200
          name: db
          protocol: TCP
        - containerPort: 9300
          name: transport
          protocol: TCP
        volumeMounts:
          - name: es-config-volume
            mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
            subPath: elasticsearch.yml
          - name: es-data-volume
            mountPath: /usr/share/elasticsearch/data
      volumes:
        - name: es-config-volume
          configMap:
            name: es-config
            items:
            - key: elasticsearch.yml
              path: elasticsearch.yml
  volumeClaimTemplates:
  - metadata:
      name: es-data-volume
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: "nfs"
      resources:
        requests:
          storage: 5Gi

es-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: es-svc
  namespace: logging
  labels:
    k8s-app: elasticsearch
spec:
  selector:
    k8s-app: elasticsearch
  ports:
  - name: out
    port: 9200
    protocol: TCP
$ kubectl create namespace logging

## 部署服務
$ kubectl create -f es-config.yaml
$ kubectl create -f es-svc-headless.yaml
$ kubectl create -f es-statefulset.yaml
$ kubectl create -f es-svc.yaml

## 等待片刻,檢視一下es的pod部署到了k8s-slave1節點,狀態變為running
$ kubectl -n logging get po -o wide  
NAME              READY   STATUS    RESTARTS   AGE   IP  
elasticsearch-0   1/1     Running   0          15m   10.244.0.126 
elasticsearch-1   1/1     Running   0          15m   10.244.0.127
elasticsearch-2   1/1     Running   0          15m   10.244.0.128
# 然後通過curl命令訪問一下服務,驗證es是否部署成功
$ kubectl -n logging get svc  
es-svc            ClusterIP   10.104.226.175   <none>        9200/TCP   2s
es-svc-headless   ClusterIP   None             <none>        9300/TCP   32m 
$ curl 10.104.226.175:9200
{
  "name" : "elasticsearch-2",
  "cluster_name" : "lvzhenjiang-elasticsearch",
  "cluster_uuid" : "7FDIACx9T-2ajYcB5qp4hQ",
  "version" : {
    "number" : "7.4.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
    "build_date" : "2019-10-28T20:40:44.881551Z",
    "build_snapshot" : false,
    "lucene_version" : "8.2.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"

4)部署kibana

(1)部署分析
  1. kibana需要暴露web頁面給前端使用,因此使用ingress配置域名來實現對kibana的訪問

  2. kibana為無狀態應用,直接使用Deployment來啟動

  3. kibana需要訪問es,直接利用k8s服務發現訪問此地址即可,http://es-svc:9200

(2)部署並驗證

efk/kibana.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: logging
  labels:
    app: kibana
spec:
  selector:
    matchLabels:
      app: "kibana"
  template:
    metadata:
      labels:
        app: kibana
    spec:
      containers:
      - name: kibana
        image: 192.168.99.151:5000/kibana/kibana:7.4.2
        resources:
          limits:
            cpu: 1000m
          requests:
            cpu: 100m
        env:
          - name: ELASTICSEARCH_HOSTS
            value: http://es-svc:9200
          - name: SERVER_NAME
            value: kibana-logging
          - name: SERVER_REWRITEBASEPATH
            value: "false"
        ports:
        - containerPort: 5601
---
apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: logging
  labels:
    app: kibana
spec:
  ports:
  - port: 5601
    protocol: TCP
    targetPort: 5601
  type: ClusterIP
  selector:
    app: kibana
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: kibana
  namespace: logging
spec:
  rules:
  - host: kibana.lvzhenjiang.com
    http:
      paths:
      - path: /
        backend:
          serviceName: kibana
          servicePort: 5601
$ kubectl create -f kibana.yaml  
deployment.apps/kibana created
service/kibana created  
ingress/kibana created

## 配置域名解析 kibana.lvzhenjiang.com,並訪問服務進行驗證,若可以訪問,說明連線es成功

5)Fluentd服務部署

(1)部署分析
  1. fluentd為日誌採集服務,kubernetes叢集的每個業務節點都有日誌產生,因此需要使用daemonset的模式進行部署
  2. 為進一步控制資源,會為daemonset指定一個選擇標籤,fluentd=true來做進一步過濾,只有帶有此標籤的節點才會部署fluentd
  3. 日誌採集,需要採集哪些目錄下的日誌,採集後傳送到es端,因此需要配置的內容比較多,我們選擇使用configmap的方式把配置檔案整個掛載出來
(2)部署服務

efk/fluentd-es-config-main.yaml

apiVersion: v1
data:
  fluent.conf: |-
    # This is the root config file, which only includes components of the actual configuration
    #
    #  Do not collect fluentd's own logs to avoid infinite loops.
    <match fluent.**>
    @type null
    </match>

    @include /fluentd/etc/config.d/*.conf
kind: ConfigMap
metadata:
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
  name: fluentd-es-config-main
  namespace: logging

配置檔案,fluentd-config.yaml,注意點:

  1. 資料來源source的配置,k8s會預設把容器的標準和錯誤輸出日誌重定向到宿主機中
  2. 預設集成了 kubernetes_metadata_filter 外掛,來解析日誌格式,得到k8s相關的元資料,raw.kubernetes
  3. match輸出到es端的flush配置

efk/fluentd-configmap.yaml

kind: ConfigMap
apiVersion: v1
metadata:
  name: fluentd-config
  namespace: logging
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  containers.input.conf: |-
    <source>
      @id fluentd-containers.log
      @type tail
      path /var/log/containers/*.log
      pos_file /var/log/es-containers.log.pos
      time_format %Y-%m-%dT%H:%M:%S.%NZ
      localtime
      tag raw.kubernetes.*
      format json
      read_from_head false
    </source>
    # Detect exceptions in the log output and forward them as one log entry.
    # https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions 
    <match raw.kubernetes.**>
      @id raw.kubernetes
      @type detect_exceptions
      remove_tag_prefix raw
      message log
      stream stream
      multiline_flush_interval 5
      max_bytes 500000
      max_lines 1000
    </match>
  output.conf: |-
    # Enriches records with Kubernetes metadata
    <filter kubernetes.**>
      @type kubernetes_metadata
    </filter>
    <match **>
      @id elasticsearch
      @type elasticsearch
      @log_level info
      include_tag_key true
      hosts elasticsearch-0.es-svc-headless:9200,elasticsearch-1.es-svc-headless:9200,elasticsearch-2.es-svc-headless:9200
      #port 9200
      logstash_format true
      #index_name kubernetes-%Y.%m.%d
      request_timeout    30s
      <buffer>
        @type file
        path /var/log/fluentd-buffers/kubernetes.system.buffer
        flush_mode interval
        retry_type exponential_backoff
        flush_thread_count 2
        flush_interval 5s
        retry_forever
        retry_max_interval 30
        chunk_limit_size 2M
        queue_limit_length 8
        overflow_action block
      </buffer>
    </match>

daemonset定義檔案,fluentd.yaml,注意點:

  1. 需要配置rbac規則,因為需要訪問k8s api去根據日誌查詢元資料
  2. 需要將/var/log/containers/目錄掛載到容器中
  3. 需要將fluentd的configmap中的配置檔案掛載到容器內
  4. 想要部署fluentd的節點,需要新增fluentd=true的標籤

efk/fluentd.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd-es
  namespace: logging
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd-es
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
  - ""
  resources:
  - "namespaces"
  - "pods"
  verbs:
  - "get"
  - "watch"
  - "list"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd-es
  labels:
    k8s-app: fluentd-es
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
  name: fluentd-es
  namespace: logging
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: fluentd-es
  apiGroup: ""
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
    k8s-app: fluentd-es
  name: fluentd-es
  namespace: logging
spec:
  selector:
    matchLabels:
      k8s-app: fluentd-es
  template:
    metadata:
      labels:
        k8s-app: fluentd-es
    spec:
      containers:
      - env:
        - name: FLUENTD_ARGS
          value: --no-supervisor -q
        image: 192.168.99.151:5000/fluentd_elasticsearch/fluentd:v2.5.2
        imagePullPolicy: IfNotPresent
        name: fluentd-es
        resources:
          limits:
            memory: 500Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - mountPath: /var/log
          name: varlog
        - mountPath: /var/lib/docker/containers
          name: varlibdockercontainers
          readOnly: true
        - mountPath: /fluentd/etc/config.d
          name: config-volume
        - mountPath: /fluentd/etc/fluent.conf
          name: config-volume-main
          subPath: fluent.conf
      nodeSelector:
        fluentd: "true"
      securityContext: {}
      serviceAccount: fluentd-es
      serviceAccountName: fluentd-es
      volumes:
      - hostPath:
          path: /var/log
          type: ""
        name: varlog
      - hostPath:
          path: /var/lib/docker/containers
          type: ""
        name: varlibdockercontainers
      - configMap:
          defaultMode: 420
          name: fluentd-config
        name: config-volume
      - configMap:
          defaultMode: 420
          items:
          - key: fluent.conf
            path: fluent.conf
          name: fluentd-es-config-main
        name: config-volume-main
## 給slave1打上標籤,進行部署fluentd日誌採集服務
$ kubectl label node k8s-slave1 fluentd=true  
$ kubectl label node k8s-slave2 fluentd=true

# 建立服務
$ kubectl create -f fluentd-es-config-main.yaml    
$ kubectl create -f fluentd-configmap.yaml  
$ kubectl create -f fluentd.yaml  

## 然後檢視一下pod是否已經在k8s-slave1
$ kubectl -n logging get po -o wide
NAME                      READY   STATUS    RESTARTS   AGE  
elasticsearch-logging-0   1/1     Running   0          123m  
fluentd-es-246pl   		  1/1     Running   0          2m2s  
kibana-944c57766-ftlcw    1/1     Running   0          50m

上述是簡化版的k8s日誌部署收集的配置,完全版的可以提供 https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-elasticsearch 來檢視。

6)EFK功能驗證

(1)驗證思路

在slave節點中啟動服務,同時往標準輸出中列印測試日誌,到kibana中檢視是否可以收集

(2)建立測試容器

efk/test-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  nodeSelector:
    fluentd: "true"
  containers:
  - name: count
    image: alpine:3.6
    args: [/bin/sh, -c,
            'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done']
$ kubectl get po  
NAME                          READY   STATUS    RESTARTS   AGE  
counter                       1/1     Running   0          6s

(3)配置kibana

登入kibana介面,按照截圖的順序操作:




也可以通過其他元資料來過濾日誌資料,比如可以單擊任何日誌條目以檢視其他元資料,如容器名稱,Kubernetes 節點,名稱空間等,比如kubernetes.pod_name : counter

到這裡,我們就在 Kubernetes 叢集上成功部署了 EFK ,要了解如何使用 Kibana 進行日誌資料分析,可以參考 Kibana 使用者指南文件:https://www.elastic.co/guide/en/kibana/current/index.html