前言

如果你需要在Kubernetes上搭建EFK日志收集系統,這篇文章將會是你的最佳選擇。本篇文章將會帶你一步一步的搭建EFK日誌收集系統,並且會分享一些踩坑紀錄。整個文章的蓋架構如下:


從上圖來看,這就是我們要搭建的EFK日誌收集系統。主要有以下工作:

  1. 建立 NFS Provisioner 服務:
    • 因為我們希望每次加入一個節點於Kubernetes集群時,都能夠自動在新的節點中建立ElasticSearch,也就是說每個節點都會有一個ElasticSearch駐守,而這群ElasticSearch會形成一個叢集。
    • 然而,每個ElasticSearch都需要儲存資料,因此我們需要一個共享的儲存空間,這個共享的儲存空間就是NFS。
    • 而NFS Provisioner就是可以根據ElasticSearch建立起時,發送一個NFS PVC,那Provisioner就會根據PVC建立一個NFS PV,並且將PV掛載到ElasticSearch的Pod中。
  2. 建立 ElasticSearch:
    • ElasticSearch是一個分散式的搜尋引擎,我們將會在Kubernetes上建立一個ElasticSearch叢集。
    • 但是我們希望讓每個 Kubernetes 叢集都有一個 Elasticsearch 節點,可以採用 StatefulSet 來部署 Elasticsearch,這樣可以確保每個節點都有一個 Elasticsearch Pod,並且能夠保留數據狀態。
  3. 建立Kibana:
    • 這沒什麼好說的,Kibana是一個用於視覺化Elasticsearch數據的工具,我們將會在Kubernetes上建立一個Kibana服務。
  4. 建立fluentd DaemonSet:
    • fluentd是一個用於收集日誌的工具,我們將會在Kubernetes上建立一個fluentd DaemonSet,這樣可以收集所有節點上的日誌。
    • 使用 DaemonSet 是因為我們希望每個節點都有一個fluentd Pod,這樣可以確保每個節點上的日誌都能被收集。

1 建立ns以及svc

要先建立名為es-cluster-svcheadless service,這樣可以確保每個pod都有一個固定的名稱,這樣其他pod不需要知道要把log送到哪個pod,只要送到其中一個即可。

新建立kube-logging.yaml的namespace

1
2
3
4
apiVersion: v1
kind: Namespace
metadata:
name: logging
1
$ kubectl apply -f kube-logging.yaml

建立 headless service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kind: Service
apiVersion: v1
metadata:
name: es-cluster-svc
namespace: logging
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
clusterIP: None # 這裡是重點,要設定為None代表是headless service
ports:
- port: 9200
name: rest
- port: 9300
name: inter-node

2 安裝NFS provisioner


https://godleon.github.io/blog/Kubernetes/k8s-Config-StorageClass-with-NFS/

建立 NFS Provisioner 的目的是在 Kubernetes 集群中提供一個「動態的共享儲存解決方案」,讓Pod可以鬆地申請、使用和共享這些儲存空間。

  • NFS provisioner:
    • 負責建立 PV
    • 負責建立NFS volume: (其實就是一般的 directory),你在share directory會發現它會自動幫你根據pod切割環境
  • Service Account
    • 這是用來管控 NFS provisioner 在 k8s 中可以運行的權限
  • StorageClass:
    • 負責建立 PVC
    • 呼叫 NFS provisioner 進行設定工作,並讓 PVC 與 PV 繫結

流程:
這邊我使用了兩個方法,一個是使用nfs-subdir-external-provisioner,另一個是使用nfs-client-provisioner,但是我發現nfs-client-provisioner在我的環境中無法正常運行,因此我最後選擇使用nfs-subdir-external-provisioner

我們要先建立provisioner (可以透過helm或手動建立)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# way1: 使用nfs-subdir-external-provisioner加入repo
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner

# 安裝
helm install nfs-subdir-external-provisioner \
nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=34.80.7.234 \
--set nfs.path=/data/es \
--set storageClass.onDelete=true

# 如果想要解除安裝
helm uninstall nfs-subdir-external-provisioner

## way2 採坑: 我發現使用stable不知道為何無法建立...provisioner無法正常運行
helm repo add stable http://mirror.azure.cn/kubernetes/charts/

# 指定storiageClass.name
helm install nfs-client-provisioner \
stable/nfs-client-provisioner \
--set nfs.server=34.80.7.234 \
--set nfs.path=/data/es \
--set storageClass.name=nfs-client \
--set image.repository=quay.io/external_storage/nfs-client-provisioner-arm \
--set image.tag=latest \
--set storageClass.defaultClass=true
--set image.repository=quay.io/external_storage/nfs-client-provisioner-arm

helm uninstall nfs-client-provisioner

3 建立elasticSearch statefulSet

實作

ElasticSearch StatefulSet Yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: es-cluster
namespace: logging
spec:
serviceName: es-cluster-svc
replicas: 3
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
# 限制cpu數量
resources:
limits:
cpu: 1000m
requests:
cpu: 100m
ports:
- containerPort: 9200
name: rest
protocol: TCP
- containerPort: 9300
name: inter-node
protocol: TCP
# 設置掛載目錄
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
# 設置環境變量
env:
# 自訂義 clustenr name
- name: cluster.name
value: k8s-logs
# 定義傑點名稱 使用metadata.name名稱
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
# 發現節點的地址, discover.seed_hosts的值應該包含所有master的候選節點
- name: discovery.seed_hosts
value: "es-cluster-svc"
# 初始化時 ES從中選出master節點,對應metadata.name名稱加編號從0開始
- name: cluster.initial_master_nodes
value: "es-cluster-0,es-cluster-1,es-cluster-2"
# 至少多少個node來進行選舉master問題
- name: discovery.zen.minimum_master_nodes
value: "2"
# 安全設置
- name: xpack.security.enabled
value: "true"
- name: xpack.monitoring.collection.enabled
value: "true"
# 配置內存
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
# 初始化容器
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
securityContext:
privileged: true
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: increase-vm-max-map
image: busybox
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
- name: increase-fd-ulimit
image: busybox
command: ["sh", "-c", "ulimit -n 65536"]
securityContext:
privileged: true
volumeClaimTemplates:
- metadata:
name: data
labels:
app: elasticsearch
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nfs-client" # storageClass的名稱,你要先建立好喔!
resources:
requests:
storage: 5Gi # 通常可以寫大一點根據你的需求有些時候是500G

透過以下指令建立帳號密碼

1
k exec -it es-cluster-0 -n logging -- bin/elasticsearch-setup-passwords auto -b 

流程說明

讓我用更簡單的方式來解釋這個流程,並逐步說明每一個部分的目的和作用:

背景知識

首先,讓我們了解一些基本概念:

  • StorageClass(SC):它是一種定義 Kubernetes 如何動態配置儲存資源的規則。例如,當你需要儲存空間時,StorageClass 會告訴 Kubernetes 如何創建它。
  • PersistentVolumeClaim(PVC):應用程序用來請求儲存空間的方式,類似於提出「我要一個特定大小的儲存空間」的需求。
  • PersistentVolume(PV):PVC 請求儲存後,實際創建出來的儲存資源就是 PV。
  • NFS Provisioner:它是一個自動化工具,用來在 NFS 伺服器上為 PVC 動態配置儲存空間。

流程解釋

1. StorageClass 部分

  1. 建立 PVC(PersistentVolumeClaim)
    • 應用程序發出一個「我要儲存空間」的請求(建立 PVC)。
  2. 呼叫 NFS Provisioner
    • Kubernetes 根據 StorageClass 的配置,呼叫 NFS Provisioner,請求它去為 PVC 創建儲存空間。
    • NFS Provisioner 是你之前已經設定好的(在 step1 中已經部署),它知道如何在 NFS 伺服器上為 PVC 建立儲存空間。
  3. k get sc 確認 StorageClass
    • 使用 k get sc 命令,你可以查看到 Kubernetes 中的 StorageClass 列表,並確認有一個叫 nfs-client 的 StorageClass,這代表 Kubernetes 會使用這個 StorageClass 來配置儲存。
  4. k get pvc 確認建立的 PVC
    • 使用 k get pvc,你會看到 Kubernetes 已經依照應用程式需求,建立了三個 PVC,分別是 data-es-cluster-0data-es-cluster-1data-es-cluster-2。這些 PVC 代表 Elasticsearch 的三個節點需要各自的儲存空間。

2. NFS Provisioner 部分

  1. NFS Provisioner 收到通知並開始建立 PV(PersistentVolume)
    • 當 NFS Provisioner 收到 PVC 請求時,它開始在 NFS 伺服器上創建對應的目錄,並配置 PV,為這些 PVC 提供儲存空間。
  2. 建立 PV 與 NFS 之間的連結
    • 每個 PV 都指向 NFS 上的一個對應目錄,這樣 PVC 可以直接存取它需要的儲存空間。
  3. k get pv 確認建立的 PV
    • 使用 k get pv,你會看到三個 PV 已經被創建,分別用於 data-es-cluster-0data-es-cluster-1data-es-cluster-2 的儲存需求。
  4. k get pod 確認 NFS Provisioner 的運行情況
    • 使用 k get pod 可以確認 NFS Provisioner 正在正常運行(名稱類似於 nfs-subdir-external-provisioner-589f98599c-zxtps)。

總結

  • StorageClass 定義了儲存配置規則,並在 PVC 提出請求時觸發 NFS Provisioner 自動創建 PV。
  • NFS Provisioner 會在 NFS 伺服器上為每個 PVC 建立對應的儲存空間,並將它們與 Kubernetes PV 連結。
  • 最終,當 Elasticsearch 的 Pod 啟動時,它們會自動獲得對應的儲存空間,這確保每個 Pod(data-es-cluster-0data-es-cluster-1data-es-cluster-2)都有專屬的儲存資料夾。

這樣,你的 Elasticsearch 集群中的每個節點都可以擁有自己的持久化儲存空間,並且由 Kubernetes 進行自動化管理。

結果驗證

成功添加之後應該要可以再logging namespace底下看到所有資源對象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ kubectl get sts -n logging 
NAME READY AGE
es-cluster 3/3 17m

$ kubectl get pods -n logging
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 18m
es-cluster-1 1/1 Running 0 18m
es-cluster-2 1/1 Running 0 17m


$ kubectl get svc -n logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
es-cluster-svc ClusterIP None <none> 9200/TCP,9300/TCP 11m

使用port forward測試9200

  • 我們使用port-forward暫時測試pod裡面是否有正常運行
  • 可以參考官方 kubectl port-forward
  • kubectl port-forward 不会返回。你需要打開另一個terminal窗口來繼續操作。

termianl 1

1
2
3
4
5
6
7
8
# 可以先查看pod的port位置
$ kubectl get pod -n logging es-cluster-0 --template='{{(index (index .spec.containers 0).ports 0).containerPort}}{{"\n"}}'

$ kubectl port-forward es-cluster-0 9200:9200 --namespace=logging
Forwarding from 127.0.0.1:9200 -> 9200
Forwarding from [::1]:9200 -> 9200

<這時候請開另一個terminal>

terminal 2

1
$ curl http://localhost:9200/_cluster/state?pretty
  • 正常應該會看到類似如下訊息: 看到上面的信息就表明我们名为 k8s-logs 的 Elasticsearch 集群成功创建了3个节点:es-0,es-1,和es-2,当前主节点是 es-0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"cluster_name" : "k8s-logs",
"compressed_size_in_bytes" : 348,
"cluster_uuid" : "QD06dK7CQgids-GQZooNVw",
"version" : 3,
"state_uuid" : "mjNIWXAzQVuxNNOQ7xR-qg",
"master_node" : "IdM5B7cUQWqFgIHXBp0JDg",
"blocks" : { },
"nodes" : {
"u7DoTpMmSCixOoictzHItA" : {
"name" : "es-1",
"ephemeral_id" : "ZlBflnXKRMC4RvEACHIVdg",
"transport_address" : "10.244.4.191:9300",
"attributes" : { }
},
"IdM5B7cUQWqFgIHXBp0JDg" : {
"name" : "es-0",
"ephemeral_id" : "JTk1FDdFQuWbSFAtBxdxAQ",
"transport_address" : "10.244.2.215:9300",
"attributes" : { }
},
"R8E7xcSUSbGbgrhAdyAKmQ" : {
"name" : "es-2",
"ephemeral_id" : "9wv6ke71Qqy9vk2LgJTqaA",
"transport_address" : "10.244.40.4:9300",
"attributes" : { }
}
},
...

4 設定es的帳號密碼

找到user elastic 的 password 假設是 ArKsypD2Z2isKLz52wPe

1
$ kubectl create secret generic elasticsearch-pw-elastic -n logging --from-literal password=ArKsypD2Z2isKLz52wPe

5 建立Kibana

實作

新建一个 kibana.yaml 的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
apiVersion: v1
kind: Service
metadata:
name: kibana
namespace: logging
labels:
app: kibana
spec:
ports:
- port: 5601
# 偉了方便測試我們將service設置為NodePort類型
type: NodePort
selector:
app: kibana

---
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: docker.elastic.co/kibana/kibana:7.6.2
resources:
limits:
cpu: 1000m
requests:
cpu: 1000m
env:
# 使用以下變量設置es-cluster的端點直接使用k8s dns即可
# 此端點對應svc名稱es-cluster-svc是一個headless service所以該域將解析es pod的ip地址列表
- name: ELASTICSEARCH_HOSTS
value: http://es-cluster-svc:9200
ports:
- containerPort: 5601

結果驗證

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ kubectl get pods --namespace=logging
NAME READY STATUS RESTARTS AGE
es-cluster-0 1/1 Running 0 28m
es-cluster-1 1/1 Running 0 28m
es-cluster-2 1/1 Running 0 28m
kibana-d87c67f6d-bh8ct 0/1 Pending 0 66s

$ kubectl get svc -n logging
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
es-cluster-svc ClusterIP None <none> 9200/TCP,9300/TCP 31m
kibana NodePort 10.111.194.14 <none> 5601:32177/TCP 4m58s

# 看一下對外ip是多少
$ curl ifconfig.io
34.80.7.234

6 部屬fluentd DaemonSet

我們接下來要在每個Node上部屬一個fluentd,負責收集所有的Pod的log進行處理,並且將log傳送到ElasticSearch中。你可能會好奇…
為什麼要用 DaemonSet?
確保每個 Node 都有一個 Fluentd Pod

  • DaemonSet 是 Kubernetes 用來確保每個 Node(節點)上都會運行一個 Pod 的機制。
  • 在部署 Fluentd 時,我們希望每個 Kubernetes 節點上都運行一個 Fluentd 實例,這樣它就可以收集該節點上所有 Pod 的日誌。

設定檔介紹

  • Fluentd 的資料接收,資料處理資料導出的資料流處理流程都透過設定檔來進行設定
  • td-agent 的設定檔位於 /etc/td-agent/td-agent.conf

Fluentd config

1
2
3
4
5
6
7
8
9
10
11
12
13
# 資料輸入(Input)來源設定
<source>
...
</source>

# 將 tag 符合 pattern 的資料輸出(Output)到設定的目的地。
<match pattern>
# 資料處理與過濾方式。
<filter>
...
</filter>
...
</match>

source

設定 Fluentd 接收日誌的來源,並且將日誌數據轉換為 Fluentd 內部的事件格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<source>
@id fluentd-containers.log # 表示引用該日誌來源的唯一標識符,該標識可用於進一步過濾和路由結構化日誌數據
@type tail # Fluentd 內建的 tail 插件,用於監控文件變化並讀取文件內容
path /var/log/containers/*.log # 掛載容器日誌文件的路徑
pos_file /var/log/es-containers.log.pos
tag raw.kubernetes.* # 設置日誌的 tag,用於標識日誌來源
read_from_head true
<parse> # 多行日誌解析器
@type multi_format # 使用 multi-format-parser 解析器插件
<pattern>
format json # JSON 解析器
time_key time # 指定時間字段
time_format %Y-%m-%dT%H:%M:%S.%NZ # 時間格式
</pattern>
<pattern>
format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
time_format %Y-%m-%dT%H:%M:%S.%N%:z
</pattern>
</parse>
</source>

match

設定 Fluentd發送日誌的目的地,並且將日誌數據轉換為 Elasticsearch 的格式。

match:

  • 這個部分用來指定要處理哪些日誌,你可以把它想像成「我要處理什麼樣的日誌?」的設定。
  • 我們用 **(兩個星號)來代表「所有的日誌」,意思是「不管什麼日誌都抓過來處理」。
  • 這部分通常也稱為「output plugin」,因為它負責把資料送到特定的地方。

id:

  • 就是一個「名稱」或「代號」,用來標示這個設定。這樣如果你有多個設定,就可以分辨它們。

type:

  • 這部分是用來告訴 Fluentd「要把日誌送到哪裡」。
  • Fluentd 有很多內建的選項,例如把日誌存成檔案(file),或者送到另一台 Fluentd(forward)。
  • 我們這裡選擇 elasticsearch,因為我們想把日誌送到 Elasticsearch。

log_level:

  • 這設定用來指定「我要處理哪些等級的日誌」。
  • 比如說設為 info,意思就是「我要處理 INFO 等級以上的日誌」,也就是 INFO、WARNING、ERROR 等等。

logstash_format:

  • 設定 logstash_formattrue,表示 Fluentd 會用 Logstash 的格式來發送日誌到 Elasticsearch。這樣可以讓 Elasticsearch 更容易讀取和處理這些日誌。

host / port:

  • 這裡設定要把日誌送到的 Elasticsearch 伺服器的「地址 host」和「端口(port)」。
  • 如果你的 Elasticsearch 沒有需要帳號或密碼,就直接設定地址和端口即可。

buffer:

  • 緩衝區」的意思,當 Elasticsearch 無法使用或網路出問題時,Fluentd 可以暫時把日誌存起來,等到可以傳送時再發送,這樣不會漏掉任何日誌。
  • 這也有助於減少對磁碟的讀寫壓力,讓系統更穩定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<match **> 
@id elasticsearch
@type elasticsearch
@log_level info

include_tag_key true
type_name fluentd

host "#{ENV['OUTPUT_HOST']}"

port "#{ENV['OUTPUT_PORT']}"

logstash_format true

<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 "#{ENV['OUTPUT_BUFFER_CHUNK_LIMIT']}"

queue_limit_length "#{ENV['OUTPUT_BUFFER_QUEUE_LIMIT']}"

overflow_action block

</buffer>

filter

由於k8s cluster中應用太多,也有很多歷史數據,所以我們希望只有某些pod的log進行收集。比如我們只採集logging=true這個label標籤的pod log這時候就可以使用filter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 删除无用的属性
<filter kubernetes.**>
@type record_transformer
remove_keys $.docker.container_id,$.kubernetes.container_image_id,$.kubernetes.pod_id,$.kubernetes.namespace_id,$.kubernetes.master_url,$.kubernetes.labels.pod-template-hash
</filter>
# 只保留 $.kubernetes.labels.logging=true 的 pod 日誌
<filter kubernetes.**>
@id filter_log
@type grep
<regexp>
key $.kubernetes.labels.logging
pattern ^true$
</regexp>
</filter>

實作

要收集k8s cluster log直接使用DaemonSet來部屬Fluentd以確保在cluster中每個node上始終運行fluentd容器,當然也可以直接使用helm安裝但這裡先使用手動方式進行。

首先我們先通過ConfigMap指定Fluentd.config文件

  • ConfigMap 是 Kubernetes 用來管理應用程序設定資料的一種資源。它讓你可以將應用程式的設定與程式碼分開管理,這樣可以讓應用程式更靈活、更容易被配置和更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
```yaml 
kind: ConfigMap
apiVersion: v1
metadata:
name: fluentd-config
namespace: logging
data:
system.conf: |-
<system>
root_dir /tmp/fluentd-buffers/
</system>
containers.input.conf: |-
<source>
@id fluentd-containers.log
@type tail # Fluentd 内置的输入方式,其原理是不停地从源文件中获取新的日志。
path /var/log/containers/*.log # 挂载的服务器Docker容器日志地址
pos_file /var/log/es-containers.log.pos
tag raw.kubernetes.* # 设置日志标签
read_from_head true
<parse> # 多行格式化成JSON
@type multi_format # 使用 multi-format-parser 解析器插件
<pattern>
format json # JSON解析器
time_key time # 指定事件时间的时间字段
time_format %Y-%m-%dT%H:%M:%S.%NZ # 时间格式
</pattern>
<pattern>
format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
time_format %Y-%m-%dT%H:%M:%S.%N%:z
</pattern>
</parse>
</source>
# 在日志输出中检测异常,并将其作为一条日志转发
# https://github.com/GoogleCloudPlatform/fluent-plugin-detect-exceptions
<match raw.kubernetes.**> # 匹配tag为raw.kubernetes.**日志信息
@id raw.kubernetes
@type detect_exceptions # 使用detect-exceptions插件处理异常栈信息
remove_tag_prefix raw # 移除 raw 前缀
message log
stream stream
multiline_flush_interval 5
max_bytes 500000
max_lines 1000
</match>

<filter **> # 拼接日志
@id filter_concat
@type concat # Fluentd Filter 插件,用于连接多个事件中分隔的多行日志。
key message
multiline_end_regexp /\n$/ # 以换行符“\n”拼接
separator ""
</filter>

# 添加 Kubernetes metadata 数据
<filter kubernetes.**>
@id filter_kubernetes_metadata
@type kubernetes_metadata
</filter>

# 修复 ES 中的 JSON 字段
# 插件地址:https://github.com/repeatedly/fluent-plugin-multi-format-parser
<filter kubernetes.**>
@id filter_parser
@type parser # multi-format-parser多格式解析器插件
key_name log # 在要解析的记录中指定字段名称。
reserve_data true # 在解析结果中保留原始键值对。
remove_key_name_field true # key_name 解析成功后删除字段。
<parse>
@type multi_format
<pattern>
format json
</pattern>
<pattern>
format none
</pattern>
</parse>
</filter>

# 删除一些多余的属性
<filter kubernetes.**>
@type record_transformer
remove_keys $.docker.container_id,$.kubernetes.container_image_id,$.kubernetes.pod_id,$.kubernetes.namespace_id,$.kubernetes.master_url,$.kubernetes.labels.pod-template-hash
</filter>

# 只保留具有logging=true标签的Pod日志
<filter kubernetes.**>
@id filter_log
@type grep
<regexp>
key $.kubernetes.labels.logging
pattern ^true$
</regexp>
</filter>

###### 监听配置,一般用于日志聚合用 ######
forward.input.conf: |-
# 监听通过TCP发送的消息
<source>
@id forward
@type forward
</source>

output.conf: |-
<match **>
@id elasticsearch
@type elasticsearch
@log_level info
include_tag_key true
host elasticsearch
port 9200
logstash_format true
logstash_prefix k8s # 设置 index 前缀为 k8s
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>

接下來建立 fluentd DaemonSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
---
# A cluster role in kubernetes contains rules that represent a set of permissions.
# For fluentd, we want to give permissions for pods and namespaces.
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:
name: fluentd-es
namespace: logging
labels:
k8s-app: fluentd-es
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
selector:
matchLabels:
k8s-app: fluentd-es
template:
metadata:
labels:
k8s-app: fluentd-es
kubernetes.io/cluster-service: "true"
# 此注释确保如果节点被驱逐,fluentd不会被驱逐,支持关键的基于 pod 注释的优先级方案。 v1.16已經成priorityClassName: high-priority
# annotations:
# scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
serviceAccountName: fluentd-es
priorityClassName: high-priority
containers:
- name: fluentd-es
image: quay.io/fluentd_elasticsearch/fluentd:v3.0.1
env:
- name: FLUENTD_ARGS
value: --no-supervisor -q
- name: FLUENT_ELASTICSEARCH_HOST
value:
resources:
limits:
memory: 500Mi
requests:
cpu: 100m
memory: 200Mi
volumeMounts:
- name: varlog
mountPath: /var/log
# 建議使用docker info 查看Docker Root Dire的位置是否為/var/lib/docker
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: config-volume
mountPath: /etc/fluent/config.d
# 為了能夠靈活控制那些node的log可以被收集 所以添加了nodeSelector屬性
nodeSelector:
beta.kubernetes.io/fluentd-ds-ready: "true"
# 我們cluster用kubeadm搭建因此default master有汙點 如果想要收集master node的log需要添加tolerations
tolerations:
- operator: Exists
terminationGracePeriodSeconds: 30
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: config-volume
configMap:
name: fluentd-config

坑1: pvc pending not bound

  1. 發現pvc顯示waiting for a volume to be created 代表該pvc沒有對應到pv
1
Normal  ExternalProvisioning  13s (x2 over 25s)  persistentvolume-controller  waiting for a volume to be created, either by external provisioner "nfs-client" or manually created by system administrator
  1. 然後檢視kubectl logs nfs-client-provisioner的日誌有如下資訊selfLink was empty, can't make reference
  • 表示 Kubernetes 嘗試為 PVC 創建 PV 時,發生了錯誤,因為 PVC 的 selfLink 是空的,無法創建引用。
  • 會出現這個錯誤很大一部分的原因是,selfLink 被移除了,從 Kubernetes 1.20 開始,selfLink 屬性被逐步廢棄,並且在更高版本中(例如 1.24+)完全移除。因此,某些較舊的外部 provisioner 或控制器如果仍然依賴於 selfLink 屬性來處理資源,就會遇到這個錯誤
1
E1022 07:01:24.615869       1 controller.go:1004] provision "default/test-claim" class "nfs-storage": unexpected error getting claim reference: selfLink was empty, can't make reference

selfLink was empty 在k8s叢集 v1.20之前都存在,在v1.20之後被刪除,需要在/etc/kubernetes/manifests/kube-apiserver.yaml 新增引數增加 --feature-gates=RemoveSelfLink=false

1
2
3
4
5
6
7
8
9
10
11
12
# 編輯
vim /etc/kubernetes/manifests/kube-apiserver.yaml

# 添加
spec:
containers:
- command:
- kube-apiserver
- --feature-gates=RemoveSelfLink=false

# 重新啟動
kubectl apply -f /etc/kubernetes/manifests/kube-apiserver.yaml

Solution2: 換 Helm

更換能夠支持新版本的Helm Chart,如果你使用的是stable 的provisioner,我發現會有問題,因此改用另一個

使用nfs-subdir-external-provisioner

1
2
3
4
5
6
7
8
9
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner

helm install nfs-subdir-external-provisioner \
nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--set nfs.server=<ip> \
--set nfs.path=/data/es \
--set storageClass.onDelete=true

helm uninstall nfs-subdir-external-provisioner

Solution3: 把 PVC 刪掉重跑 StatefulSet

前提: 如果你發現上述兩個解法試用過後還是不行

  1. provisioner 是正常running的狀態
  2. 沒有selfLink的問題

試試看以下方法

  1. 先讓StatefulSet 下去,沒有statefulSet在跑
  2. 目前沒有任何 pvc 使用
  3. 確認storageClass的名稱有對應到StatefulSet的storageCalss
  4. 重新執行StatefulSet

坑2: CPU 不夠

1
2
3
$ kubectl describe node | less 
// CPI Requests 去看一下到底哪些pod佔用太多服務... 去調整他
// 像是上面kibana因為占用1000m 也就是1整顆cpu太多了...

坑3: NFS 連線不到

1
2
3
4
5
6
7
# 查看目前有哪些service有防火牆
firewall-cmd --get-service

# 添加以下服務
firewall-cmd --add-service=nfs
firewall-cmd --add-service=rpc-bind
firewall-cmd --add-service=mountd

補充: StatefulSet

通常我們平常所使用的Deployment 和 ReplicaSet 建立的 Pod 是「無狀態」(stateless)的,意思是這些 Pod 的狀態和資料不會被記錄下來或保留,重新啟動後就像全新的 Pod 一樣。如果你需要建立會保存狀態的服務,例如資料庫,則需要使用 StatefulSet 來創建,這樣它可以記住每個 Pod 的狀態和資料。

特點如下:
儲存配置

  • 當你使用 StatefulSet 建立 Pod 時,需要一個永久儲存空間(Persistent Volume, PV)。PV 會根據事先設定的儲存類型(storage class)自動配置。
  • 即使你刪除或縮減 StatefulSet,這些儲存空間也不會被刪除,以確保你的數據不會被意外刪除。

需要 Headless Service

  • 為了讓每個 Pod 可以正常在網路上通訊,StatefulSet 需要一個特殊的服務類型,稱為 Headless Service,來幫助管理 Pod 的網路連線,主要原因是 每個 Pod 都需要一個固定且獨特的網路身份,使它們能夠被其他 Pod 和服務以特定的名稱進行尋找和通訊。這與 Deployment 中的 Pod 有很大的不同,因為 Deployment 中的 Pod 是「無狀態的」,不需要擁有固定的網路身份。
  • Headless Service 可以確保 StatefulSet 中的每一個 Pod 都有一個固定的名稱(例如:app-0, app-1, app-2),並且需要有一個固定的網路位置,這樣其他 Pod 和服務才能找到它們。
  • Headless Service 的特點是 clusterIP: None,這樣Kubernetes就不會給這個服務分配一個單一的Cluster IP。舉例來說,當你有一個 my-app 的 Headless Service 並且有三個 StatefulSet 的 Pod(my-app-0my-app-1my-app-2),如果其他 Pod 或服務向 my-app 發送請求,這些請求會被分配到 所有三個 Pod 中的一個,我們不需要去知道到底要送到my-app-0還是my-app-1,這是由 Kubernetes 的 DNS 負責的,但是不管是0,1,2對我們來說都無所謂,我們只要送到其中一個即可。

有序性和正常終止

  • StatefulSet 會按順序創建和刪除 Pod,確保每個 Pod 有一個固定的編號和順序。如果你想要確保所有 Pod 都按照正確的順序關閉,最好先將 StatefulSet 的副本數(replicas)縮減為 0,再刪除它。

補充:SelfLink

什麼是 SelfLink?
selfLink 是 Kubernetes 資源物件的一個屬性,它就像是資源的「網址」,用來表示這個資源在 Kubernetes API 中的完整路徑。當你想要查詢、訪問或修改某個資源時,selfLink 可以幫助你找到它的確切位置,就像使用網址找到網頁一樣。Kubernetes 自己也會用這個 selfLink 來跟蹤資源的狀態,方便 API 與資源進行互動。

假設你有一個叫做 my-pod 的 Pod,selfLink 可能長這樣:

1
/api/v1/namespaces/default/pods/my-pod