在 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

這種方式部署比較直觀簡單,但缺陷也比較明顯:

  1. 由於抓取規則都是靜態的,所以不能做服務發現;

  2. 每添加一個 Deployment,需要配置對應的 Service 來暴露指標;

  3. 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 進行交互時,需要首先配置身份信息。通常我們可以通過兩種方式來訪問:

  1. HTTPS 客戶端證書,一般情況下人類用戶會通過這種方式來訪問;

  2. 不提供 HTTPS 客戶端證書,但在 HTTP 會話中通過 Bearer Token 的方式提供 ServiceAccount 的 JWT Token,而這通常是集羣內的程序訪問 K8S API 的方式。

Prometheus 對這兩種方式均提供了支持,不過我還是選擇了配置 ServiceAccount 來與 K8S 交互。

配置 ServiceAccount 及對應的 RBAC 策略

爲了完成 K8S 身份認證以及接口鑑權,我們需要配置以下資源:

  1. ServiceAccount,用於身份認證;

  2. ClusterRole,定義角色和權限;

  3. 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,可以用來給我們動態配置抓取的地址和協議,它們分別是:

有了這幾個標籤,我們就可以通過一定的規則來拼湊出目標地址了。

具體抓取規則如下:

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 官網外,我還參考了以下頁面:

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_3cLjLY4AhmPr5XeTqGUjw