第 9 章:監控與日誌 — Prometheus + Grafana
這一章在做什麼? 我們讓 app 吐出「指標(metrics)」,用 Prometheus 定期收集,再用 Grafana 把它畫成儀表板。做完這章,你能看見服務的請求量、延遲、錯誤率——出事能即時發現,而不是等使用者來罵。
為什麼要監控? 服務上線後,「它現在健康嗎?慢不慢?有沒有在噴錯?」如果看不見,你就是瞎子開車。可觀測性就是替你的服務裝上儀表板。
💸 本章在「本機 kind」叢集做,完全免費(第 8 章的 GKE 已 destroy)。若你刪了 kind 叢集,先用第 7 章的步驟重建並把 app 部署回去。 前置:第 7 章的本機叢集與 manifest。
步驟 1:先搞懂四個名詞
| 名詞 | 白話 |
|---|---|
| 可觀測性 / Metrics(指標) | 用數字描述服務狀態:每秒幾個請求、延遲多少、錯誤幾%。指標是「可被監控的數據」。 |
| Prometheus | 主流的監控系統。它**主動定期去「抓取(scrape)」**各服務的 /metrics 端點,把數字存成「時序資料」(隨時間變化的數列)。 |
| Grafana | 把 Prometheus 的數據畫成漂亮的儀表板與圖表,還能設警報。 |
| Helm | Kubernetes 的「套件管理器」(像 K8s 的 apt/npm)。一個「chart」打包了一整套 K8s 資源,一行指令就裝好。 |
流程一句話:你的 app 開一個
/metrics端點 → Prometheus 定時去抓 → Grafana 把抓來的數據畫成圖。
步驟 2:讓 app 吐出 /metrics
我們用官方的 Prometheus Go 客戶端,加一個中介層記錄「請求數」與「請求耗時」,並開一個 /metrics 端點。
- 取得套件:
cd code
go get github.com/prometheus/client_golang@v1.20.5
- 建立
code/internal/middleware/metrics.go:
// code/internal/middleware/metrics.go
package middleware
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
// 兩個指標:請求總數(counter)與請求耗時(histogram)
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "HTTP 請求總數",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP 請求耗時(秒)",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
// Metrics 中介層:替每個請求記錄次數與耗時
func Metrics() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next() // 先讓請求被處理
// 用「路由樣板」當 path 標籤(/api/quotes/:symbol),
// 不用實際 URL,避免標籤值爆炸(high cardinality)。
path := c.FullPath()
if path == "" {
path = "unmatched"
}
status := strconv.Itoa(c.Writer.Status())
httpRequestsTotal.WithLabelValues(c.Request.Method, path, status).Inc()
httpRequestDuration.WithLabelValues(c.Request.Method, path).Observe(time.Since(start).Seconds())
}
}
重點:
promauto.NewCounterVec/NewHistogramVec:定義帶標籤的指標,並自動註冊。- counter(只增不減)記「總請求數」;histogram 記「耗時分布」(之後能算出 p95 延遲)。
[]string{"method","path","status"}:標籤維度,讓你能「依方法/路徑/狀態碼」切分。c.FullPath()(關鍵):用路由樣板(/api/quotes/:symbol)當標籤,而不是每個實際 URL。否則像/api/quotes/AAPL、/api/quotes/TSLA… 每個都變成不同標籤值,指標會爆量(這是 Prometheus 最常見的坑,叫 high cardinality)。
- 在
code/cmd/api/main.go掛上中介層與/metrics端點:
// code/cmd/api/main.go(import 區追加)
"github.com/prometheus/client_golang/prometheus/promhttp"
// code/cmd/api/main.go(在 r := gin.Default() 之後加)
// 記錄每個請求的指標,並開一個 /metrics 端點給 Prometheus 抓取
r.Use(middleware.Metrics())
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
r.Use(middleware.Metrics()):每個請求都會經過這個中介層被記錄。gin.WrapH(promhttp.Handler()):把 Prometheus 官方的 HTTP handler「包」成 Gin 能用的格式,掛在/metrics。
本機先確認還能 build / 測試:
go build ./... && go test ./...
# 預期:build 無誤、internal/handler 測試 ok
步驟 3:重 build image、重新部署到叢集
叢集裡跑的是舊 image(沒有 /metrics),要更新:
cd code
docker build -t nlt-api:local .
kind load docker-image nlt-api:local --name nlt
kubectl rollout restart -n nlt deployment/nlt-api # 讓 Pod 換成新 image
kubectl rollout status -n nlt deployment/nlt-api
確認 /metrics 有東西(port-forward app 直接看):
kubectl port-forward -n nlt svc/nlt-api 8080:80
# 另開終端機:
curl -s http://localhost:8080/metrics | grep http_requests_total | head
# 預期:看到 http_requests_total{...} 的指標輸出
步驟 4:用 Helm 裝 Prometheus + Grafana
我們用一個打包好的 chart:kube-prometheus-stack(一次裝好 Prometheus、Grafana、以及讓 Prometheus 知道「要抓誰」的 Operator)。
- 安裝 Helm:
brew install helm
helm version
- 建立
code/monitoring/values.yaml(給 chart 的自訂設定):
# code/monitoring/values.yaml
grafana:
adminPassword: admin # 教學用密碼;正式環境請改強密碼
prometheus:
prometheusSpec:
# 預設 Prometheus 只抓「自己這個 release 標記過」的 ServiceMonitor。
# 設成 false 讓它抓「叢集裡所有」ServiceMonitor(包含我們等下建的)。
serviceMonitorSelectorNilUsesHelmValues: false
serviceMonitorSelectorNilUsesHelmValues: false是新手最容易卡的一行:不設它,Prometheus 不會理你自己寫的 ServiceMonitor,永遠抓不到 app。
- 安裝(會拉不少元件,第一次要等幾分鐘):
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
-f code/monitoring/values.yaml
確認元件起來:
kubectl get pods -n monitoring
# 預期:prometheus、grafana、kube-state-metrics、operator 等都 Running
步驟 5:告訴 Prometheus「來抓我的 app」
Prometheus 靠一個叫 ServiceMonitor 的物件(由 Operator 提供)知道要抓哪個 Service 的 /metrics。
先確認 code/k8s/app.yaml 的 Service 有標籤、且 port 有名字(ServiceMonitor 要靠它們對上):
# code/k8s/app.yaml 的 Service 改成這樣
apiVersion: v1
kind: Service
metadata:
name: nlt-api
namespace: nlt
labels:
app: nlt-api # ★ ServiceMonitor 會用這個標籤找到本 Service
spec:
selector:
app: nlt-api
ports:
- name: http # ★ 替 port 命名,ServiceMonitor 會引用它
port: 80
targetPort: 8080
再建立 code/k8s/servicemonitor.yaml:
# code/k8s/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: nlt-api
namespace: nlt
spec:
selector:
matchLabels:
app: nlt-api # 找帶這個標籤的 Service(上面那個)
namespaceSelector:
matchNames: [ nlt ]
endpoints:
- port: http # 對應 Service 那個名為 http 的 port
path: /metrics # 抓 /metrics
interval: 15s # 每 15 秒抓一次
套用:
kubectl apply -f code/k8s/app.yaml
kubectl apply -f code/k8s/servicemonitor.yaml
逐段:selector.matchLabels 找到 app 的 Service → endpoints 指定抓它名為 http 的 port 上的 /metrics,每 15 秒一次。
✅ 如何驗證這一章成功
驗證 1:先製造一些流量
# 確保 app 的 port-forward 還開著(步驟 3);打幾十次讓指標有數據
for i in $(seq 1 50); do curl -s http://localhost:8080/health > /dev/null; done
驗證 2:Prometheus 有抓到 app(target = up)
kubectl port-forward -n monitoring svc/monitoring-kube-prometheus-prometheus 9090:9090
瀏覽器開 http://localhost:9090 → 上方 Status → Targets → 找 nlt-api,狀態應為 UP。
或在 Graph 輸入 http_requests_total 查詢,看到數據。
驗證 3:在 Grafana 看圖
kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80
瀏覽器開 http://localhost:3000,帳號 admin、密碼 admin(values 設的)。進去後左側 Explore → 資料源選 Prometheus → 輸入查詢:
# 每秒請求數,依路徑與狀態碼切分
sum(rate(http_requests_total[5m])) by (path, status)
再試試 p95 延遲:
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, path))
應該能看到曲線。(也可在 Grafana Dashboards → Import 輸入社群儀表板 ID 直接套版,例如 Go/Gin 相關的 dashboard。)
- [ ]
curl /metrics看得到http_requests_total。 - [ ] Prometheus 的 Targets 裡
nlt-api為 UP。 - [ ] Grafana Explore 用上面的 PromQL 查得到曲線。
看日誌(順帶一提)
指標是「數字」,日誌是「事件文字」。最直接:
kubectl logs -n nlt deployment/nlt-api --tail=50
kubectl logs -n nlt deployment/nlt-api -f # 即時跟著看
正式環境會用集中式日誌(如 Loki + Grafana)把所有 Pod 的日誌集中查詢——本教學先用 kubectl logs 即可。
收拾(免費,保持乾淨)
helm uninstall monitoring -n monitoring
kubectl delete namespace monitoring
# 要全清就連叢集一起:kind delete cluster --name nlt
🧯 常見錯誤 / 踩雷點
| 症狀 | 原因 | 解法 |
|---|---|---|
Prometheus Targets 裡沒有 nlt-api |
沒設 serviceMonitorSelectorNilUsesHelmValues: false,或 ServiceMonitor 標籤對不上 |
確認 values 那行;ServiceMonitor 的 selector.matchLabels 要對上 Service 的 metadata.labels |
Target 有但狀態 down |
port 名稱對不上、或路徑錯 | Service 的 port 要命名 http,ServiceMonitor endpoints.port: http、path: /metrics |
kind: ServiceMonitor apply 報錯(unknown kind) |
Operator/CRD 還沒裝好 | 先 helm install kube-prometheus-stack(它會帶 CRD),再 apply ServiceMonitor |
| Grafana 進不去 / 忘記密碼 | 沒設或記錯 | `kubectl get secret -n monitoring monitoring-grafana -o jsonpath='{.data.admin-password}' |
| 指標標籤暴量、Prometheus 吃很多記憶體 | 用了實際 URL 當標籤(high cardinality) | 用 c.FullPath() 路由樣板當 path 標籤(本章已這樣做) |
| 整台電腦變很卡 | kube-prometheus-stack 在小機器上吃資源 | 給 Docker Desktop 多一點記憶體(4GB+);或只裝 Prometheus 子集 |
| port-forward 服務名找不到 | 各家 chart 的 service 名不同 | kubectl get svc -n monitoring 看實際名稱再 forward |
下一步 → 第 10 章:收尾 — 完整 pipeline 全貌 + 安全拆除 最後我們把前面所有環節串成一條「push → 測試 → build → 推 GHCR → 自動部署 → 監控」的完整自動化線(用 Workload Identity 讓 GitHub Actions 安全地部署到 GCP),給出更新後的架構圖,並附上一份完整的資源拆除清單,確保你不會留下任何在偷偷計費的東西。