在 K3S 集羣外監控集羣內的指標
【導讀】本文中作者記錄了 k3s 集羣外抓取了集羣內數據指標的全程操作。
喫飽了撐的,嘗試一下 Prometheus 在 K3S 集羣外抓取集羣內指標的若干姿勢。
背景
前一陣子收了塊樹莓派 4,順手在上面搭了一個單節點的 K3S. 幾個月前在家裏的服務器上搭過一個 Prometheus 的實例,於是就決定研究下如何在集羣外收集 K3S 集羣內 Pod 的指標。
先上一個簡單的網絡拓撲圖:
衆所周知(?),Pod Network 和 Node Network 是兩個不同的網段,所以在 Node 之外是無法直接訪問到 Pod 的。所以我們需要通過一些方法,讓我們直接或間接地訪問 Pod 中提供的 HTTP 接口,進而完成指標抓取。
我們在 k3s 集羣中部署了一個暴露接口的 Deployment 用於指標抓取測試,它的指標端點爲 http://localhost/metrics
. Deployment 配置如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: promtest
namespace: default
spec:
selector:
matchLabels:
app: promtest
replicas: 1
template:
metadata:
labels:
app: promtest
annotations:
prometheus.io/scrape: "true"
spec:
containers:
- name: main
image: prometheus-test:v0.1
command: ["/bin/promtest"]
args: ["-listen", "0.0.0.0:80"]
ports:
- containerPort: 80
NodePort Service
最簡單、最直觀的方法,是將 metrics endpoint 通過 NodePort Service 或 Ingress 暴露出來,然後在 Prometheus 中通過配置 static_config
來抓取。
比如我們可以配置如下的 Service 和 Ingress:
apiVersion: v1
kind: Service
metadata:
name: promtest
namespace: default
labels:
app: promtest
annotations:
prometheus.io/scrape: "true"
spec:
selector:
app: promtest
ports:
- protocol: TCP
port: 80
targetPort: 80
type: NodePort
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: promtest
namespace: default
spec:
rules:
- host: promtest.k3s
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: promtest
port:
number: 80
部署後查看 Service 的 NodePort(如 30080),則可以通過 Node 的端口訪問到指標端點(也就是 http://192.168.1.101:30080/metrics
);類似的,通過配置的 Ingress 也可以正常訪問(http://promtest.k3s/metrics
)。
Prometheus 抓取規則如下:
job_name: "exported-services"
static_configs:
- targets:
- 192.168.1.101:30080 # NodePort
- promtest.k3s # Ingress
這種方式部署比較直觀簡單,但缺陷也比較明顯:
-
由於抓取規則都是靜態的,所以不能做服務發現;
-
每添加一個 Deployment,需要配置對應的 Service 來暴露指標;
-
Service 自帶負載均衡,所以如果 Service 背後的 Endpoint 有多個,那麼多次抓取的數據來源則可能是 Serivce 背後的任意一個 Pod,而且我們也無法對來源進行區分。因此,當我們分析業務指標時,通常都會通過服務發現來抓取所有 Pod 的指標,然後通過 PromQL 根據實際場景對指標進行聚合。
Kubernetes API Proxy
這種方式略微有點奇怪:使用 K8S 提供的 pod/service proxy 接口,通過代理來訪問集羣內的 Pod 指標端點。
假設 K8S API 地址爲 https://k3s:6443
,那麼當我們想訪問 Service proxy 時,就可以通過 https://k3s:6443/api/v1/namespaces/<namespace>/services/<service_name>[:<service_port>]/proxy/metrics
來獲取。相應地,抓取 Pod 指標時,對應的 API 地址爲 https://k8s:6443/api/v1/namespaces/<namespace>/pods/<pod_name>[:<pod_port>]/proxy/metrics
與 K8S API 進行交互時,需要首先配置身份信息。通常我們可以通過兩種方式來訪問:
-
HTTPS 客戶端證書,一般情況下人類用戶會通過這種方式來訪問;
-
不提供 HTTPS 客戶端證書,但在 HTTP 會話中通過 Bearer Token 的方式提供 ServiceAccount 的 JWT Token,而這通常是集羣內的程序訪問 K8S API 的方式。
Prometheus 對這兩種方式均提供了支持,不過我還是選擇了配置 ServiceAccount
來與 K8S 交互。
配置 ServiceAccount 及對應的 RBAC 策略
爲了完成 K8S 身份認證以及接口鑑權,我們需要配置以下資源:
-
ServiceAccount
,用於身份認證; -
ClusterRole
,定義角色和權限; -
ClusterRoleBinding
,將ClusterRole
的權限賦予ServiceAccount
.
Prometheus Operator 文檔 中提供了一套完整的 Service Account 和 RBAC 配置示例,用於進行服務發現。由於我們還需要調用 service 和 pod 的 proxy 接口,所以我們還需要額外添加兩個 API 權限:
- apiGroups: [""]
resources:
- services/proxy
- pods/proxy
verbs: ["get"]
配置好 ServiceAccount
後,我們可以從名爲 <service_account_name>_token
的 Secret 中獲取用於身份驗證的 JWT Token.
配置服務發現和抓取規則
如果 Serivce 或 Pod 名是已經確定好的,那麼可以直接通過配置 static_config
來進行抓取;但如果用到了 K8S 的服務發現,那麼我們還需要通過服務發現的元信息來確定指標抓取的目標地址。
relabel_config
配置 中,有幾個特殊的 label,可以用來給我們動態配置抓取的地址和協議,它們分別是:
-
__address__
,用於配置目標地址的 host 和端口; -
__metrics_path__
,用於配置目標地址的路徑; -
__scheme__
,用於配置抓取時使用的協議(http 或 https); -
__params_<name>
,用於在抓取的 URL 中注入 query.
有了這幾個標籤,我們就可以通過一定的規則來拼湊出目標地址了。
具體抓取規則如下:
job_name: 'k3s-pod-via-api'
scheme: https
tls_config:
insecure_skip_verify: true # 跳過服務器證書驗證,當然也可以用 ca_file 配置服務器證書
authorization:
credentials_file: /etc/prometheus/k8s_token # 文件中存有 ServiceAccount token
kubernetes_sd_configs: # 服務發現配置
- api_server: https://k3s:6443 # K8S API 地址
role: pod
tls_config: # 這部分跟上面差不多
insecure_skip_verify: true
authorization:
credentials_file: /etc/prometheus/k8s_token
namespaces: # 可選的 namespace 配置
names:
- default
selectors: # 可選的 label selector
- role: pod
label: "app=promtest"
relabel_configs:
# 只抓取包含 `prometheus.io/scrape: true` annotation 的 Pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: 'true'
# 如果定義了 `prometheus.io/port` 註解,則用它覆蓋 Pod 定義中的端口號
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: (\d+)
replacement: $1
target_label: __meta_kubernetes_pod_container_port_number
# 動態構建 K8S proxy API 地址
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_name, __meta_kubernetes_pod_container_port_number]
action: replace
regex: (.+);(.+);(.+)
replacement: api/v1/namespaces/$1/pods/$2:$3/proxy/metrics
target_label: __metrics_path__
# 通過 `prometheus.io/path` 註解自定義抓取路徑
- source_labels: [__metrics_path__, __meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
regex: (.+)/metrics;/?(.+)
replacement: $1/$2
target_label: __metrics_path__
# Host 和 Port 是確定的
- source_labels: []
action: replace
regex: ""
replacement: a.r8:6443
target_label: __address__
# 將一些元信息注入到 metrics 標籤中
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: k8s_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: k8s_pod_name
Service 的抓取和 Pod 大同小異,只是目標地址和 meta 標籤名不太一樣,就不贅述了。
這種方式可以使用 K8S 的服務發現功能,但通過 proxy API 來訪問 Pod,也加重了 kube-apiserver
的負擔。而且,說句實話,寫這種拼湊 API 地址的 relabel_config
還是挺蛋疼的。🌚
如果不使用 K8S 的 proxy API 的話,也可以簡單在集羣內部署一個 HTTP 反向代理,然後通過反代來抓取 Pod 或 Service 的指標。這個方案其實跟上一種差不多,只是把 kube-apiserver 的 proxy 換成了集羣內的另外一個 proxy 而已,不過減輕了 kube-apiserver
的負擔。考慮到安全因素,我們可以爲 proxy 配置 egress NetworkPolicy
來控制它可以訪問的 Pod,但這樣也會使權限和選擇策略變得極爲分散。
打通 Node Network 和 Pod/Service Network
這種方法算是從根本上解決問題:打通 Node 和 Pod / Service 網絡,這樣我們就可以直接訪問 Pod 或 Service 的 IP 來抓取指標。
打通網絡的操作主要參考了兩篇文章:《辦公環境下 kubernetes 網絡互通方案》 以及 《打通 Kubernetes 內網與局域網的 N 種方法》,最後選擇從網絡層打通網絡。
操作很簡單,只需要在 server 中配置兩條路由規則即可:
ip route add 10.42.0.0/16 via 192.168.1.101 dev enp1s0
ip route add 10.43.0.0/16 via 192.168.1.101 dev enp1s0
如果想要在局域網內打通的話,可以在路由器的管理後臺來配置靜態路由規則;如果集羣存在多個節點,則還需在 Node 的 iptables 中配置 MASQUERADE
規則用於轉發。配置完成後,Prometheus 就可以直接通過 Pod IP 或 Service 的 Cluster IP 來抓取指標了。
Pod 的抓取規則如下:
job_name: 'k3s-pod'
# 抓取時直接通過 HTTP 協議從 Pod IP 抓取,所以無需鑑權
kubernetes_sd_configs: # 服務發現配置不變
- api_server: https://k8s:6443
role: pod
tls_config:
insecure_skip_verify: true
authorization:
credentials_file: /etc/prometheus/k8s_token
relabel_configs:
# 篩選註解規則同上
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: 'true'
# 根據 `prometheus.io/path` 註解直接覆蓋指標路徑
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
regex: (.+)
target_label: __metrics_path__
# 根據 `prometheus.io/port` 註解覆蓋端口
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
# 元信息規則同上
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: k8s_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: k8s_pod_name
Service 的抓取規則略。
可以看出,如果我們可以直接訪問 Pod,那麼抓取時的 relabel 規則就可以簡化很多。
一個缺乏經驗導致的無謂 troubleshooting
之前通過 Argo CD 安裝腳本 在集羣內安裝了 Argo CD. 當我配完上面的規則以後,我發現 argocd-metrics
Service 的指標無法通過 Cluster IP 抓取(報錯”Connection refused“),而 argocd-server-metrics
就可以。而在 Node 上,兩個服務均可以正常訪問。
查了 Node 上的 iptables
規則,沒有在 Service (argocd-metrics
) 到 Pod (argocd-application-controller-0
) 的轉發鏈路中發現任何異常,直接訪問 Pod IP 也驗證了這一點。但由於缺乏 iptables debug 經驗,並沒有找到訪問 Pod IP 被拒絕的原因。
最後通過玄學 debug,發現 argocd
namespace 的五個 Pod 裏,只有一個可以正常訪問,最後找到了安裝腳本中配置的 NetworkPolicy,發現有五個 NetworkPolicy 限制了每個 Pod 的 ingress 來源。將 Node 所在局域網的 CIDR(192.168.1.1/24
)添加至 argocd-application-controller-network-policy
的 ingress 白名單中,問題解決。
說實話,之前確實沒有怎麼接觸過 NetworkPolicy
,導致這個問題我查了將近四天才查出來…
事後分析完整的轉發鏈如下:
# Service -> Pod with DNAT
-A KUBE-SERVICES -d 10.43.4.242/32 -p tcp -m tcp --dport 8082 -m comment --comment "argocd/argocd-metrics:metrics cluster IP" -j KUBE-SVC-SZWGFJCG7JW62ZG2
-A KUBE-SVC-SZWGFJCG7JW62ZG2 -m comment --comment "argocd/argocd-metrics:metrics" -j KUBE-SEP-VYRHUQXWRJ6MSGOH
-A KUBE-SEP-VYRHUQXWRJ6MSGOH -p tcp -m tcp -m comment --comment "argocd/argocd-metrics:metrics" -j DNAT --to-destination 10.42.0.38:8082
# Pod 轉發至 pod 防火牆
-A KUBE-ROUTER-OUTPUT -d 10.42.0.38/32 -m comment --comment "rule to jump traffic destined to POD name:argocd-application-controller-0 namespace: argocd to chain KUBE-POD-FW-XIOATVM5TOINSO4V" -j KUBE-POD-FW-XIOATVM5TOINSO4V
-A KUBE-POD-FW-XIOATVM5TOINSO4V -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "rule for stateful firewall for pod" -j ACCEPT
# 通過 local mode (也就是從 node ip) 訪問 pod 的包都會被批准
-A KUBE-POD-FW-XIOATVM5TOINSO4V -d 10.42.0.38/32 -m addrtype --src-type LOCAL -m comment --comment "rule to permit the traffic traffic to pods when source is the pod\'s local node" -j ACCEPT
# 接受 argocd-application-controller-network-policy 的規則判斷,通過後會被打上標記
-A KUBE-POD-FW-XIOATVM5TOINSO4V -m comment --comment "run through nw policy argocd-application-controller-network-policy" -j KUBE-NWPLCY-5VLCZNPWIAXAL2HB
-A KUBE-POD-FW-XIOATVM5TOINSO4V -m mark ! --mark 0x10000/0x10000 -m limit --limit 10/min --limit-burst 10 -m comment --comment "rule to log dropped traffic POD name:argocd-application-controller-0 namespace: argocd" -j NFLOG --nflog-group 100
# 沒有標記(沒通過規則判斷),就會拒絕連接
-A KUBE-POD-FW-XIOATVM5TOINSO4V -m mark ! --mark 0x10000/0x10000 -m comment --comment "rule to REJECT traffic destined for POD name:argocd-application-controller-0 namespace: argocd" -j REJECT --reject-with icmp-port-unreachable
# 第一條 namespaceSelector 規則,對應 8082 端口
# namespaceSelector: {}
# 滿足條件則會打上標記,然後 return
-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-DRBIHPAD4OLOF546 src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to mark traffic matching a network policy" -m comment --comment "rule to ACCEPT traffic from source pods to dest pods selected by policy name argocd-application-controller-network-policy namespace argocd" -j MARK --set-xmark 0x10000/0x10000
-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-DRBIHPAD4OLOF546 src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to RETURN traffic matching a network policy" -m mark --mark 0x10000/0x10000 -m comment --comment "rule to ACCEPT traffic from source pods to dest pods selected by policy name argocd-application-controller-network-policy namespace argocd" -j RETURN
# 自己加上去的第二條 ipBlock cidr 規則,8082 端口
# ipBlock:
# cidr: 192.168.1.0/24
-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-MLGAJX4FU64MJPWH src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to mark traffic matching a network policy" -m comment --comment "rule to ACCEPT traffic from specified ipBlocks to dest pods selected by policy name: argocd-application-controller-network-policy namespace argocd" -j MARK --set-xmark 0x10000/0x10000
-A KUBE-NWPLCY-5VLCZNPWIAXAL2HB -p tcp -m set --match-set KUBE-SRC-MLGAJX4FU64MJPWH src -m set --match-set KUBE-DST-DM6ZQPCTKCXEROGZ dst -m tcp --dport 8082 -m comment --comment "rule to RETURN traffic matching a network policy" -m mark --mark 0x10000/0x10000 -m comment --comment "rule to ACCEPT traffic from specified ipBlocks to dest pods selected by policy name: argocd-application-controller-network-policy namespace argocd" -j RETURN
ipset 規則如下:
# 第一條 namespaceSelector 規則
Name: KUBE-SRC-DRBIHPAD4OLOF546
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536 timeout 0
Size in memory: 1008
References: 4
Number of entries: 16
Members:
10.42.0.41 timeout 0
10.42.0.40 timeout 0
# 後面的 pod ip 地址略
# 第二條 ipblock cidr 規則
Name: KUBE-SRC-MLGAJX4FU64MJPWH
Type: hash:net
Revision: 6
Header: family inet hashsize 1024 maxelem 65536 timeout 0
Size in memory: 440
References: 4
Number of entries: 1
Members:
192.168.1.0/24 timeout 0
# 目標地址規則(NetworkPolicy 中 podSelector 列出的所有 IP)
# podSelector:
# matchLabels:
# app.kubernetes.io/name: argocd-application-controller
Name: KUBE-DST-DM6ZQPCTKCXEROGZ
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536 timeout 0
Size in memory: 168
References: 8
Number of entries: 1
Members:
10.42.0.38 timeout 0
……
我只是個小開發。webp
總結
從集羣外訪問集羣內的接口有很多種方式,如果是一般的業務需求,我們通常還是會用 Service / Ingress 來完成。但爲了簡化配置,使用集羣層面的服務發現,我們還需要繞些彎路來訪問指標接口。
如果實在是希望在集羣外收集指標的話(比如使用了指標收集的 PaaS 服務,如阿里雲 SLS),那麼從安全性和便捷性出發,我認爲最合理的架構還是應該在 K8S 集羣內部署一個 Prometheus 用於集羣內的指標抓取。集羣內的 Prometheus 可以通過 Service / Ingress 將暴露出來,這樣集羣外的 Prometheus 實例可以通過 federate 接口 直接抓取到集羣內 Prometheus 的指標。由於集羣內的 Prometheus 主要用於抓取和數據轉發,所以無需保留過多數據,也不需要太關注持久化因素。
那麼,折騰了半天,我爲什麼要在集羣外抓取集羣內的指標呢。
參考資料
除了 Kubernetes 和 Prometheus 官網外,我還參考了以下頁面:
-
Prometheus kuberenetes_sd_config 示例
-
Prometheus RBAC 配置
-
《辦公環境下 kubernetes 網絡互通方案》
-
《打通 Kubernetes 內網與局域網的 N 種方法》
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/_3cLjLY4AhmPr5XeTqGUjw