iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Kubernetes

從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警系列 第 8

Day 8:Prometheus Operator 安裝與 ServiceMonitor:第一次抓到指標

  • 分享至 

  • xImage
  •  

昨天的結論是四句話,今天要把第一句劃掉:只有現在,沒有歷史

也就是裝 Prometheus。但是這篇的重點不是安裝指令,那其實只有兩行,重點是另外兩件事:Operator 這個模式到底在幹嘛,以及為什麼照著做卻抓不到資料。後面那個坑我幾乎確定大家都會踩到,因為它不會報錯。

今天先把故障關掉

kubectl set env deploy/pricing BUG_SILENT_DISCOUNT=false LEAK_KB_PER_REQUEST=0
kubectl set env deploy/catalog BUG_N_PLUS_ONE=false

昨天為了示範「查不出來」把三個故障全開了,今天要的是一條乾淨的基準線。不關的話 pricing 每四分鐘就 OOM 重啟一次,指標圖會全是斷點,很難拿來講解。

服務怎麼把指標交出來

在裝 Prometheus 之前有個前提:我們的程式得先把指標印出來給人家看

前面說過 Prometheus 是用拉的,我們的程式開一個網址,上面印著當下的數字,Prometheus 每隔一段時間自己跑來讀。慣例上這個網址叫 /metrics

這段其實 Day 6 就寫進 common.py 了,只是當時沒解釋:

requests_total = Counter(
    "http_requests_total", "HTTP 請求數", ["service", "path", "status"],
)

@app.get("/metrics")
def metrics():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

打開來看看:

kubectl port-forward svc/gateway 8080:80
curl localhost:8080/metrics

https://ithelp.ithome.com.tw/upload/images/20260908/20180570hiP9DrgCp0.png

第一次看到這個畫面我有點意外——原來所謂的「暴露指標」就是印一份純文字出來,沒有什麼神奇的協定。Prometheus 就是定期來抓這份文字,解析之後存進自己的資料庫。

格式也很單純,每個指標前面有兩行以 # 開頭的說明:HELP 是這個指標在幹嘛,TYPE 是它的型別。然後才是資料行,長成「指標名稱{標籤} 數值」,例如:

python_gc_objects_collected_total{generation="0"} 100793.0

generation="0" 就是標籤,同一個指標名稱可以靠不同的標籤值分成好幾條線。

不過我第二個反應是「怎麼都不是我的東西」。整個第一頁全是 python_gc_*python_info 這些 Python 執行環境自己的指標,那是 prometheus-client 免費附送的。我自己寫的 http_requests_totalcheckout_total 在下面,要往下捲,或是直接撈:

curl -s localhost:8080/metrics | grep -E "^(http_requests_total|checkout_total)"

順帶一提,這張圖裡剛好兩種型別都有:python_gc_objects_collected_totalcounter,只會往上加;python_infogauge,值可上可下。這兩種的差別後面講 PromQL 的時候會很重要。

一個我踩到的坑:/metrics 的 307 轉址

上面那段程式碼我不是一次就寫對的。

網路上教的寫法,包括 prometheus-client 自己的範例,是這樣掛:

app.mount("/metrics", make_asgi_app())

我照做之後,curl localhost:8000/metrics 回傳空的。查了才發現,mount 掛出來的路徑會變成 /metrics/,帶一個斜線,而不帶斜線的 /metrics 會回一個 307 轉址:

$ curl -i localhost:8000/metrics
HTTP/1.1 307 Temporary Redirect
location: http://localhost:8000/metrics/

curl 預設不跟隨轉址,所以我看到的是空的。而 Prometheus 抓取的預設路徑就是不帶斜線的那個。改用一般路由加 generate_latest() 就沒這個問題。

順帶還修掉第二個問題。我的 middleware 原本寫 if path == "/metrics" 來把自己排除掉,但實際進來的路徑是 /metrics/,比對不到,於是每次 Prometheus 來抓都被記成一筆流量。判斷要改成 path.startswith("/metrics")監控端點不應該汙染自己的指標。

安裝:兩行指令

用 Helm 裝 kube-prometheus-stack 這個 chart:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kps prometheus-community/kube-prometheus-stack \
  --namespace monitoring --create-namespace

kps 是我給這次安裝取的名字,等一下會發現這個名字很重要--namespace monitoring 是把監控相關的東西全部放進一個叫 monitoring 的命名空間,跟示範服務分開。命名空間可以想成叢集裡的資料夾,用來把東西分門別類。

裝完看看多了什麼:

kubectl get pods -n monitoring

https://ithelp.ithome.com.tw/upload/images/20260908/20180570ZlrqH5I3Xt.png

八個 Pod,六種東西:

Pod 它做什麼
prometheus-kps-...-0 Prometheus 本體,抓取並儲存指標
alertmanager-kps-...-0 收到告警後決定通知誰,後面講告警才會用到
kps-grafana-... 畫圖表的網頁介面,明天用
kps-...-operator-... Operator,今天的主角
kps-prometheus-node-exporter-... 回報這台機器的 CPU、記憶體、磁碟,每個節點一份
kps-kube-state-metrics-... 回報 Kubernetes 自己的狀態

有四個細節值得看一下。

第一,node-exporter 有三個。 因為它的工作是回報「這台機器」的狀況,每個節點都要有一份才問得到全部。這剛好驗證了 Day 5 開三個節點那個決定——如果當初只開一個節點,這裡就只會有一個。

第二,prometheusalertmanager 的名字結尾是 -0 這個編號代表它們不是一般的 Deployment,而是 StatefulSet,因為它們要存資料,重啟之後得認得回自己原本那份。

第三,READY 欄位是 0/20/3 一個 Pod 裡不只跑一個容器,Grafana 甚至有三個。這是我第一次看到 READY 不是 1/1

第四,也是最有意思的:AGE 不一樣。 operator、grafana、node-exporter 都是 83 秒,但 prometheus 和 alertmanager 只有 30 秒。

因為 Helm 並沒有直接建立 Prometheus 的 Pod。它建的是 Operator,以及一份「我要一個 Prometheus」的宣告;Operator 起來之後看到那份宣告,才自己去把 Prometheus 生出來。中間那五十幾秒就是這件事發生的時間。

上一節講的 Operator 模式,證據就在這一欄。

還有一點,截圖裡好幾個 Pod 還是 ContainerCreatingPodInitializingInit:0/1。這不是壞掉,是還在拉映像檔跟啟動,等個一兩分鐘再看一次就都會變成 Running

Operator 到底在做什麼

這是今天真正值得學的部分。

沒有 Operator 的世界是這樣:Prometheus 有一個設定檔,裡面寫著「去抓這些網址」。我們想多監控一個服務,就得手動編輯那個設定檔、然後重新載入。服務一多,那個檔案會變成幾百行,而且改錯一個縮排整個就掛了。

有 Operator 的世界是這樣:Operator 是一個常駐在叢集裡的程式,它一直盯著叢集看,發現我們建立了一個叫 ServiceMonitor 的東西,就自動把它翻譯成 Prometheus 的設定並且套用。

差別在於,我們不再「編輯設定檔」,而是「建立一個 Kubernetes 資源」。好處是設定變成可以用 kubectl apply 管理、可以放進 Git、可以跟服務的 YAML 擺在一起。後面講 IaC 的時候,這個設計會直接派上用場。

ServiceMonitor 就是 Day 6 那三行紅字裡的主角。當時 Operator 還沒裝,所以 Kubernetes 不認得這個字;現在裝好了,kubectl get servicemonitor 就查得到它,跟查 Pod 一樣。

告訴 Prometheus 來抓我的服務

Day 6 那個 k8s/servicemonitors.yaml 現在終於可以套用了。以 gateway 為例:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: gateway
  labels:
    release: kps          # ← 這行最重要,等一下解釋
spec:
  selector:
    matchLabels:
      app: gateway        # ← 找標籤是 app=gateway 的 Service
  endpoints:
    - port: http          # ← Service 裡那個 port 的「名字」
      path: /metrics
      interval: 15s

catalog 和 pricing 照著改,只要動 namematchLabels.app 兩個地方。套用:

kubectl apply -f k8s/servicemonitors.yaml
kubectl get servicemonitor

同一個檔案、同一個指令,Day 6 那次是三行紅字,這次三個都 created,而且 kubectl get servicemonitor 查得到了。差別只在 Operator 裝好了,ServiceMonitor 這個資源類型現在存在。

loadgen 不需要 ServiceMonitor,因為它沒有 Service、也沒有 /metrics,它不是網頁服務,只是一支一直往外送請求的程式。

三個要注意的地方:

  1. selector 找的是 Service,不是 Pod。 這就是 Day 6 說的,監控掛在 Service 上,所以之後把服務擴成三份,三個 Pod 會自動被抓到,不用改設定
  2. port 要填名字,不是數字。 Service 裡那個 port 必須有 name: http,沒取名字的話這裡會對不上
  3. labels 底下那個 release: kps 是最容易出事的一行,下一節專門講

那個一定會卡住的坑

套用之後去看 Prometheus 的畫面。先找出它的 Service 叫什麼:

kubectl get svc -n monitoring
kubectl port-forward -n monitoring svc/kps-kube-prometheus-stack-prometheus 9090

那個 Service 的名字是 <安裝時取的名字>-<chart 名>-prometheus 組出來的,我安裝時打 kps,所以就是 kps-kube-prometheus-stack-prometheus。名字會隨 chart 版本變,先 get svc 看一下比較保險。

打開 localhost:9090,點 Status → Target health

如果我們的服務沒有出現在列表上,不會有任何錯誤訊息。 沒有紅字、沒有警告,kubectl get servicemonitor 也顯示得好好的。它就是不在那裡。

原因是 Prometheus 不會抓走所有的 ServiceMonitor,它只挑符合自己條件的。kube-prometheus-stack 預設的條件是「標籤要有 release: 你安裝時取的名字」。我安裝的時候打的是 helm install kps,所以條件就是 release: kps。漏了那一行,Operator 根本不會看你的 ServiceMonitor 一眼。

這個坑跟昨天那個「算錯價但回 200」是同一種故障:做錯事的時候,系統安靜地什麼都不說。差別只在一個是我故意埋的,一個是真實工具的行為。

要查的話,三層標籤要一路對得上,斷在哪一層資料就到不了:

# 1. Prometheus 在找什麼標籤
kubectl get prometheus -n monitoring -o yaml | grep -A5 serviceMonitorSelector

# 2. 我的 ServiceMonitor 有沒有那個標籤
kubectl get servicemonitor gateway -o yaml | grep -A3 labels

# 3. Service 的標籤跟 selector 對不對得上
kubectl get svc gateway --show-labels

Prometheus 找 ServiceMonitor、ServiceMonitor 找 Service、Service 找 Pod,四層之間靠三組標籤串起來。

第一次看到自己的指標

在 Prometheus 畫面上方的輸入框打:

checkout_total

https://ithelp.ithome.com.tw/upload/images/20260908/20180570dMs0lCFFe6.png

七天以來第一次看到自己服務的數字。

值得看的是那一整排標籤。我在程式裡只寫了一個:

checkout_total = Counter("checkout_total", "結帳請求數", ["status"])

所以 status="success" 是我自己給的。但畫面上還有 containerendpointinstancejobnamespacepodservice —— 這些全是 Prometheus 和 Operator 在抓取的時候自動貼上去的。

這件事之後很有用。因為有 pod 這個標籤,我可以問「這個數字在哪一個 Pod 上」;因為有 service,我可以把同一個服務的多個 Pod 加總起來。我沒有多寫一行程式,這些切面就存在了。

然後切到 Graph 分頁,時間範圍拉到最近一小時:

https://ithelp.ithome.com.tw/upload/images/20260908/20180570OTAGG8H4mI.png

昨天那句「只有現在,沒有歷史」,在這裡被劃掉了。 這條線會一直被記錄下去,而且不會因為 Pod 重啟就歸零——那正是昨天數 discount rule not found 時最痛的地方。

不過這張圖也很誠實地告訴我兩件事。

第一,線只有右邊那一小段。 前面五十幾分鐘是空的,因為 Prometheus 是幾分鐘前才開始抓的。歷史從「有人開始記」的那一刻起算,它不會回頭去補之前的。這也是為什麼可觀測性要提早裝,而不是等出事才裝。

第二,這條線只會往上爬。 checkout_total 是 counter(累計值),從服務啟動到現在總共處理了幾筆,所以它永遠是遞增的。我真正想知道的是「現在每秒幾筆」,而那要靠 PromQL 把它轉換一下,是後面幾天的事。

小結

今天做完的事情:

  • 讓服務吐出 /metrics,並且知道那只是一份純文字
  • 用 Helm 一次裝好整套 Prometheus
  • 理解 Operator 模式:不改設定檔,改成建立資源
  • 用 ServiceMonitor 讓 Prometheus 來抓自己的服務
  • 踩到(或是避開)標籤對不上的靜默失敗

四句天花板劃掉一句。明天接上 Grafana 做第一張圖,而且我想做的是一張能回答問題的圖,不是一張好看的圖。


上一篇
Day 7:kubectl 看得到什麼、看不到什麼
下一篇
Day 9:Grafana 接上來:做一張能回答問題的圖,而不是好看的圖
系列文
從看得到到看得懂:30 天在自架 K8s 上實踐可觀測性與告警10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言