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 通過一組給定的資料來源抓取日誌資料,處理後(轉換成結構化的資料格式)將它們轉發給其他服務,比如 Elasticsearch、物件儲存、kafka等等。Fluentd 支援超過300個日誌儲存和分析服務,所以在這方面是非常靈活的。主要執行步驟如下
1. 首先 Fluentd 從多個日誌源獲取資料
2. 結構化並且標記這些資料
3. 然後根據匹配的標籤將資料傳送到多個目標服務
2)Fluentd精講
(1)Fluentd架構
為什麼推薦使用fluentd作為k8s體系的日誌收集工具?
-
雲原生:https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/fluentd-elasticsearch
-
將日誌檔案JSON化
- 可插拔架構設計
-
極小的資源佔用
基於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)部署分析
- es生產環境是部署es叢集,通常會使用statefulset進行部署
- es預設使用elasticsearch使用者啟動程序,es的資料目錄是通過宿主機的路徑掛載,因此目錄許可權被主機的目錄許可權覆蓋,因此可以利用initContainer容器在es程序啟動之前把目錄的許可權修改掉,注意init container要用特權模式啟動。
- 若希望使用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)部署分析
-
kibana需要暴露web頁面給前端使用,因此使用ingress配置域名來實現對kibana的訪問
-
kibana為無狀態應用,直接使用Deployment來啟動
-
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)部署分析
- fluentd為日誌採集服務,kubernetes叢集的每個業務節點都有日誌產生,因此需要使用daemonset的模式進行部署
- 為進一步控制資源,會為daemonset指定一個選擇標籤,fluentd=true來做進一步過濾,只有帶有此標籤的節點才會部署fluentd
- 日誌採集,需要採集哪些目錄下的日誌,採集後傳送到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,注意點:
- 資料來源source的配置,k8s會預設把容器的標準和錯誤輸出日誌重定向到宿主機中
- 預設集成了 kubernetes_metadata_filter 外掛,來解析日誌格式,得到k8s相關的元資料,raw.kubernetes
- 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,注意點:
- 需要配置rbac規則,因為需要訪問k8s api去根據日誌查詢元資料
- 需要將/var/log/containers/目錄掛載到容器中
- 需要將fluentd的configmap中的配置檔案掛載到容器內
- 想要部署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