前言
如果你需要在Kubernetes上搭建EFK日志收集系統,這篇文章將會是你的最佳選擇。本篇文章將會帶你一步一步的搭建EFK日誌收集系統,並且會分享一些踩坑紀錄。整個文章的蓋架構如下:
從上圖來看,這就是我們要搭建的EFK日誌收集系統。主要有以下工作:
建立 NFS Provisioner 服務 :
因為我們希望每次加入一個節點於Kubernetes集群時,都能夠自動在新的節點中建立ElasticSearch,也就是說每個節點都會有一個ElasticSearch駐守,而這群ElasticSearch會形成一個叢集。
然而,每個ElasticSearch都需要儲存資料,因此我們需要一個共享的儲存空間,這個共享的儲存空間就是NFS。
而NFS Provisioner就是可以根據ElasticSearch建立起時,發送一個NFS PVC,那Provisioner就會根據PVC建立一個NFS PV,並且將PV掛載到ElasticSearch的Pod中。
建立 ElasticSearch :
ElasticSearch是一個分散式的搜尋引擎,我們將會在Kubernetes上建立一個ElasticSearch叢集。
但是我們希望讓每個 Kubernetes 叢集都有一個 Elasticsearch 節點,可以採用 StatefulSet
來部署 Elasticsearch,這樣可以確保每個節點都有一個 Elasticsearch Pod,並且能夠保留數據狀態。
建立Kibana :
這沒什麼好說的,Kibana是一個用於視覺化Elasticsearch數據的工具,我們將會在Kubernetes上建立一個Kibana服務。
建立fluentd DaemonSet :
fluentd是一個用於收集日誌的工具,我們將會在Kubernetes上建立一個fluentd DaemonSet,這樣可以收集所有節點上的日誌。
使用 DaemonSet 是因為我們希望每個節點都有一個fluentd Pod,這樣可以確保每個節點上的日誌都能被收集。
1 建立ns以及svc
要先建立名為es-cluster-svc
的headless 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 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 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 helm repo add stable http://mirror.azure.cn/kubernetes/charts/ 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 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: - name: cluster.name value: k8s-logs - name: node.name valueFrom: fieldRef: fieldPath: metadata.name - name: discovery.seed_hosts value: "es-cluster-svc" - name: cluster.initial_master_nodes value: "es-cluster-0,es-cluster-1,es-cluster-2" - 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" resources: requests: storage: 5Gi
透過以下指令建立帳號密碼
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 部分
建立 PVC(PersistentVolumeClaim) :
應用程序發出一個「我要儲存空間」的請求(建立 PVC)。
呼叫 NFS Provisioner :
Kubernetes 根據 StorageClass 的配置,呼叫 NFS Provisioner,請求它去為 PVC 創建儲存空間。
NFS Provisioner 是你之前已經設定好的(在 step1 中已經部署),它知道如何在 NFS 伺服器上為 PVC 建立儲存空間。
k get sc
確認 StorageClass :
使用 k get sc
命令,你可以查看到 Kubernetes 中的 StorageClass 列表,並確認有一個叫 nfs-client
的 StorageClass,這代表 Kubernetes 會使用這個 StorageClass 來配置儲存。
k get pvc
確認建立的 PVC :
使用 k get pvc
,你會看到 Kubernetes 已經依照應用程式需求,建立了三個 PVC,分別是 data-es-cluster-0
、data-es-cluster-1
、data-es-cluster-2
。這些 PVC 代表 Elasticsearch 的三個節點需要各自的儲存空間。
2. NFS Provisioner 部分
NFS Provisioner 收到通知並開始建立 PV(PersistentVolume) :
當 NFS Provisioner 收到 PVC 請求時,它開始在 NFS 伺服器上創建對應的目錄,並配置 PV,為這些 PVC 提供儲存空間。
建立 PV 與 NFS 之間的連結 :
每個 PV 都指向 NFS 上的一個對應目錄,這樣 PVC 可以直接存取它需要的儲存空間。
k get pv
確認建立的 PV :
使用 k get pv
,你會看到三個 PV 已經被創建,分別用於 data-es-cluster-0
、data-es-cluster-1
和 data-es-cluster-2
的儲存需求。
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-0
、data-es-cluster-1
、data-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
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 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: - 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 $ 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_format
為 true
,表示 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 path /var/log/containers/*.log pos_file /var/log/es-containers.log.pos tag raw.kubernetes.* read_from_head true <parse> @type multi_format <pattern> format 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 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> <filter **> @id filter_concat @type concat key message multiline_end_regexp /\n$/ separator "" </filter> <filter kubernetes.**> @id filter_kubernetes_metadata @type kubernetes_metadata </filter> <filter kubernetes.**> @id filter_parser @type parser key_name log reserve_data true remove_key_name_field true <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> <filter kubernetes.**> @id filter_log @type grep <regexp> key $.kubernetes.labels.logging pattern ^true $ </regexp> </filter> forward.input.conf: |- <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 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 --- 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" 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 - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true - name: config-volume mountPath: /etc/fluent/config.d nodeSelector: beta.kubernetes.io/fluentd-ds-ready: "true" 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
Problem1: SelfLink was empty
發現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
然後檢視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
Solution1: 增加--feature-gates=RemoveSelfLink=false
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
前提: 如果你發現上述兩個解法試用過後還是不行
provisioner 是正常running的狀態
沒有selfLink的問題
試試看以下方法
先讓StatefulSet 下去,沒有statefulSet在跑
目前沒有任何 pvc 使用
確認storageClass的名稱有對應到StatefulSet的storageCalss
重新執行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-0
、my-app-1
、my-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