昨天的結論是四句話,今天要把第一句劃掉:只有現在,沒有歷史。
也就是裝 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

第一次看到這個畫面我有點意外——原來所謂的「暴露指標」就是印一份純文字出來,沒有什麼神奇的協定。Prometheus 就是定期來抓這份文字,解析之後存進自己的資料庫。
格式也很單純,每個指標前面有兩行以 # 開頭的說明:HELP 是這個指標在幹嘛,TYPE 是它的型別。然後才是資料行,長成「指標名稱{標籤} 數值」,例如:
python_gc_objects_collected_total{generation="0"} 100793.0
generation="0" 就是標籤,同一個指標名稱可以靠不同的標籤值分成好幾條線。
不過我第二個反應是「怎麼都不是我的東西」。整個第一頁全是 python_gc_*、python_info 這些 Python 執行環境自己的指標,那是 prometheus-client 免費附送的。我自己寫的 http_requests_total 和 checkout_total 在下面,要往下捲,或是直接撈:
curl -s localhost:8080/metrics | grep -E "^(http_requests_total|checkout_total)"
順帶一提,這張圖裡剛好兩種型別都有:python_gc_objects_collected_total 是 counter,只會往上加;python_info 是 gauge,值可上可下。這兩種的差別後面講 PromQL 的時候會很重要。
上面那段程式碼我不是一次就寫對的。
網路上教的寫法,包括 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

八個 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 開三個節點那個決定——如果當初只開一個節點,這裡就只會有一個。
第二,prometheus 和 alertmanager 的名字結尾是 -0。 這個編號代表它們不是一般的 Deployment,而是 StatefulSet,因為它們要存資料,重啟之後得認得回自己原本那份。
第三,READY 欄位是 0/2、0/3。 一個 Pod 裡不只跑一個容器,Grafana 甚至有三個。這是我第一次看到 READY 不是 1/1。
第四,也是最有意思的:AGE 不一樣。 operator、grafana、node-exporter 都是 83 秒,但 prometheus 和 alertmanager 只有 30 秒。
因為 Helm 並沒有直接建立 Prometheus 的 Pod。它建的是 Operator,以及一份「我要一個 Prometheus」的宣告;Operator 起來之後看到那份宣告,才自己去把 Prometheus 生出來。中間那五十幾秒就是這件事發生的時間。
上一節講的 Operator 模式,證據就在這一欄。
還有一點,截圖裡好幾個 Pod 還是 ContainerCreating、PodInitializing、Init:0/1。這不是壞掉,是還在拉映像檔跟啟動,等個一兩分鐘再看一次就都會變成 Running。
這是今天真正值得學的部分。
沒有 Operator 的世界是這樣:Prometheus 有一個設定檔,裡面寫著「去抓這些網址」。我們想多監控一個服務,就得手動編輯那個設定檔、然後重新載入。服務一多,那個檔案會變成幾百行,而且改錯一個縮排整個就掛了。
有 Operator 的世界是這樣:Operator 是一個常駐在叢集裡的程式,它一直盯著叢集看,發現我們建立了一個叫 ServiceMonitor 的東西,就自動把它翻譯成 Prometheus 的設定並且套用。
差別在於,我們不再「編輯設定檔」,而是「建立一個 Kubernetes 資源」。好處是設定變成可以用 kubectl apply 管理、可以放進 Git、可以跟服務的 YAML 擺在一起。後面講 IaC 的時候,這個設計會直接派上用場。
ServiceMonitor 就是 Day 6 那三行紅字裡的主角。當時 Operator 還沒裝,所以 Kubernetes 不認得這個字;現在裝好了,kubectl get servicemonitor 就查得到它,跟查 Pod 一樣。
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 照著改,只要動 name 和 matchLabels.app 兩個地方。套用:
kubectl apply -f k8s/servicemonitors.yaml
kubectl get servicemonitor
同一個檔案、同一個指令,Day 6 那次是三行紅字,這次三個都 created,而且 kubectl get servicemonitor 查得到了。差別只在 Operator 裝好了,ServiceMonitor 這個資源類型現在存在。
loadgen 不需要 ServiceMonitor,因為它沒有 Service、也沒有 /metrics,它不是網頁服務,只是一支一直往外送請求的程式。
三個要注意的地方:
selector 找的是 Service,不是 Pod。 這就是 Day 6 說的,監控掛在 Service 上,所以之後把服務擴成三份,三個 Pod 會自動被抓到,不用改設定port 要填名字,不是數字。 Service 裡那個 port 必須有 name: http,沒取名字的話這裡會對不上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

七天以來第一次看到自己服務的數字。
值得看的是那一整排標籤。我在程式裡只寫了一個:
checkout_total = Counter("checkout_total", "結帳請求數", ["status"])
所以 status="success" 是我自己給的。但畫面上還有 container、endpoint、instance、job、namespace、pod、service —— 這些全是 Prometheus 和 Operator 在抓取的時候自動貼上去的。
這件事之後很有用。因為有 pod 這個標籤,我可以問「這個數字在哪一個 Pod 上」;因為有 service,我可以把同一個服務的多個 Pod 加總起來。我沒有多寫一行程式,這些切面就存在了。
然後切到 Graph 分頁,時間範圍拉到最近一小時:

昨天那句「只有現在,沒有歷史」,在這裡被劃掉了。 這條線會一直被記錄下去,而且不會因為 Pod 重啟就歸零——那正是昨天數 discount rule not found 時最痛的地方。
不過這張圖也很誠實地告訴我兩件事。
第一,線只有右邊那一小段。 前面五十幾分鐘是空的,因為 Prometheus 是幾分鐘前才開始抓的。歷史從「有人開始記」的那一刻起算,它不會回頭去補之前的。這也是為什麼可觀測性要提早裝,而不是等出事才裝。
第二,這條線只會往上爬。 checkout_total 是 counter(累計值),從服務啟動到現在總共處理了幾筆,所以它永遠是遞增的。我真正想知道的是「現在每秒幾筆」,而那要靠 PromQL 把它轉換一下,是後面幾天的事。
今天做完的事情:
/metrics,並且知道那只是一份純文字四句天花板劃掉一句。明天接上 Grafana 做第一張圖,而且我想做的是一張能回答問題的圖,不是一張好看的圖。