Giám sát Kubernetes: Prometheus stack & dashboard
Tiếp đi: giám sát chính cái cluster Kubernetes.
Ở bài 19.3 mình đã có metric của app (/actuator/prometheus, PromQL, ServiceMonitor cơ bản). Bài này đổi góc nhìn: không hỏi "order-service chậm không" mà hỏi "cluster đang khỏe không".
Metric app → order-service p99, error rate, số đơn/phút (19.3)
Metric cluster → node nào đầy, Pod nào Pending, ai ăn RAM namespace (bài này)
1. Bài toán
Cluster chạy order-service, payment-service và postgres (StatefulSet). Dashboard app xanh hết, nhưng sáng thứ Hai:
- Deploy payment mới, 2 Pod đứng
Pending10 phút. Không ai biết cho tới khi deploy timeout. - Node
worker-2đầy disk, kubelet bắt đầu evict Pod. - Namespace
shoptự nhiên dùng thêm 6Gi RAM. Ai ăn? - PVC của postgres còn 3% dung lượng.
2. Vấn đề
- Metric app chỉ thấy bên trong JVM. Pod chưa từng chạy (Pending) thì không có metric app nào.
kubectl topchỉ cho số hiện tại, không có lịch sử, không alert.kubectl get podstừng namespace, từng lúc: không ai ngồi nhìn 24/7.
3. Giải pháp: biết metric đến từ đâu
| Nguồn | Chạy ở đâu | Đo cái gì | Ví dụ metric | Dùng cho |
|---|---|---|---|---|
| metrics-server | Deployment, đọc kubelet | CPU/RAM hiện tại của Pod/node | (Metrics API, không phải Prometheus) | kubectl top, HPA (17.6). Không lưu lịch sử |
| kube-state-metrics | Deployment, đọc API server | Trạng thái object: desired vs thực tế | kube_deployment_status_replicas_available, kube_pod_status_phase, kube_pod_container_status_restarts_total | Pending, restart, replicas thiếu |
| cAdvisor / kubelet | Trong kubelet mỗi node | Tài nguyên từng container (cgroup, xem bài cgroups) | container_memory_working_set_bytes, container_cpu_cfs_throttled_periods_total | Ai ăn CPU/RAM, throttling |
| kubelet /metrics | Mỗi node | Volume, trạng thái kubelet | kubelet_volume_stats_used_bytes | PVC sắp đầy |
| node-exporter | DaemonSet | Máy chủ: disk, CPU, network, load | node_filesystem_avail_bytes, node_cpu_seconds_total | Node nào đầy disk/quá tải |
Nhớ một câu: kube-state-metrics nói "nên là gì / đang là gì", cAdvisor nói "đang tốn bao nhiêu", node-exporter nói "máy còn bao nhiêu".
4. Code từng bước
Bước 1: tạo Secret cho Grafana admin (không để password trong values)
kubectl create namespace monitoring
kubectl -n monitoring create secret generic grafana-admin \
--from-literal=admin-user=admin \
--from-literal=admin-password="$GRAFANA_ADMIN_PASSWORD"
Production thì Secret này do External Secrets Operator đồng bộ từ Vault (xem 17.8).
Bước 2: values cho kube-prometheus-stack
# monitoring-values.yaml
prometheus:
prometheusSpec:
retention: 15d
retentionSize: 45GB
resources:
requests: { cpu: 500m, memory: 2Gi }
limits: { memory: 4Gi }
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
resources:
requests:
storage: 50Gi
# Mặc định chỉ chọn ServiceMonitor có label release=<tên release>.
# Tắt để scrape ServiceMonitor/PodMonitor của mọi namespace.
serviceMonitorSelectorNilUsesHelmValues: false
podMonitorSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
alertmanager:
alertmanagerSpec:
storage:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 5Gi
grafana:
admin:
existingSecret: grafana-admin
userKey: admin-user
passwordKey: admin-password
persistence:
enabled: true
size: 5Gi
Bước 3: cài bằng Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
-n monitoring -f monitoring-values.yaml --atomic
kubectl -n monitoring get pods
# monitoring-kube-prometheus-operator-... Running
# prometheus-monitoring-kube-prometheus-prometheus-0 Running
# alertmanager-monitoring-kube-prometheus-alertmanager-0 Running
# monitoring-grafana-... Running
# monitoring-kube-state-metrics-... Running
# monitoring-prometheus-node-exporter-xxxxx Running (1 Pod / node)
Bước 4: cho Prometheus scrape app của mình
ServiceMonitor đã nói ở 19.3. Ở đây thêm PodMonitor cho trường hợp không có Service (ví dụ Job batch, worker):
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: payment-worker
namespace: shop
spec:
selector:
matchLabels:
app: payment-worker
podMetricsEndpoints:
- port: http # tên port trong container spec
path: /actuator/prometheus
interval: 30s
Bước 5: mở Grafana và kiểm tra
kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
curl -s -u "admin:$GRAFANA_ADMIN_PASSWORD" http://localhost:3000/api/health
# {"database":"ok", ...}
kubectl -n monitoring port-forward svc/monitoring-kube-prometheus-prometheus 9090
curl -s 'http://localhost:9090/api/v1/query?query=up{namespace="shop"}' | jq '.data.result | length'
Vào Prometheus UI → Status → Targets: mọi target phải UP.
5. Cách hoạt động
┌────────────────────── namespace monitoring ──────────────────────┐
│ │
CRD: │ Prometheus Operator ── đọc ──► ServiceMonitor / PodMonitor / │
ServiceMonitor ─┼─► │ PrometheusRule (mọi namespace) │
PrometheusRule │ ▼ sinh config │
│ Prometheus (StatefulSet + PVC 50Gi, retention 15d) │
│ │ scrape │ alert │
│ │ ▼ │
│ │ Alertmanager ──► Slack/PagerDuty
│ │ │
│ Grafana ── query PromQL ──► Prometheus (dashboard có sẵn) │
└────┼─────────────────────────────────────────────────────────────┘
│
┌──────────────────┼───────────────────────────────────────────┐
▼ ▼ ▼ ▼
kube-state-metrics kubelet/cAdvisor node-exporter order/payment
(API server → (mỗi node: (DaemonSet: /actuator/prometheus
trạng thái object) container CPU/RAM) disk, CPU, network) (ServiceMonitor)
metrics-server ── riêng ──► Metrics API ──► kubectl top, HPA (không vào Prometheus)
Dashboard có sẵn nên mở trước (thư mục "Kubernetes / Compute Resources"):
| Dashboard | Trả lời câu hỏi |
|---|---|
| Cluster | Cả cluster dùng bao nhiêu % CPU/RAM so với requests và capacity? |
| Node (Node Exporter / Nodes) | worker-2 có đầy disk, load cao, network bão hòa không? |
| Namespace (Pods) | Trong shop, Pod nào ăn RAM nhiều nhất? |
| Pod | Container nào bị throttle, usage so với requests/limits? |
| Workload | Deployment payment-service tổng tốn bao nhiêu, mấy replica? |
PromQL quan trọng cho cluster
# Pod restart nhiều trong 1 giờ
increase(kube_pod_container_status_restarts_total{namespace="shop"}[1h]) > 3
# Container đang CrashLoopBackOff
kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"} == 1
# Lần chết gần nhất do OOMKilled
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
# Pod Pending
sum by (namespace) (kube_pod_status_phase{phase="Pending"}) > 0
# Node NotReady
kube_node_status_condition{condition="Ready", status="true"} == 0
# Tỷ lệ CPU throttling của container (> 25% là đáng lo)
sum by (namespace, pod, container) (rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m]))
/
sum by (namespace, pod, container) (rate(container_cpu_cfs_periods_total{container!=""}[5m]))
# PVC đã dùng bao nhiêu %
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes
# Ai ăn RAM trong namespace shop (top 5)
topk(5, sum by (pod) (container_memory_working_set_bytes{namespace="shop", container!=""}))
# Node sắp đầy disk
node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1
Lưu ý container!="": cAdvisor có thêm series cấp Pod (container rỗng) và container POD (pause), không lọc là cộng trùng.
PrometheusRule cho các alert trên
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: shop-cluster-alerts
namespace: monitoring
spec:
groups:
- name: shop-workloads
rules:
- alert: PodCrashLooping
expr: max by (namespace, pod, container) (kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff", namespace="shop"}) == 1
for: 5m # 1 lần restart lúc deploy chưa đáng gọi ai dậy
labels: { severity: critical }
annotations:
summary: "{{ $labels.pod }} CrashLoopBackOff"
runbook_url: https://wiki.example.com/runbooks/crashloop
- alert: ContainerOOMKilled
expr: |
increase(kube_pod_container_status_restarts_total{namespace="shop"}[15m]) > 0
and on (namespace, pod, container)
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
for: 0m
labels: { severity: warning }
- alert: PodPendingTooLong
expr: sum by (namespace, pod) (kube_pod_status_phase{phase="Pending", namespace="shop"}) > 0
for: 10m # pull image và schedule bình thường mất vài phút
labels: { severity: warning }
- alert: CPUThrottlingHigh
expr: |
sum by (namespace, pod, container) (rate(container_cpu_cfs_throttled_periods_total{namespace="shop", container!=""}[5m]))
/ sum by (namespace, pod, container) (rate(container_cpu_cfs_periods_total{namespace="shop", container!=""}[5m])) > 0.25
for: 15m
labels: { severity: info }
- name: shop-infra
rules:
- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready", status="true"} == 0
for: 5m # node chập chờn vài chục giây là bình thường
labels: { severity: critical }
- alert: PVCAlmostFull
expr: kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.85
for: 15m
labels: { severity: warning }
annotations:
summary: "PVC {{ $labels.persistentvolumeclaim }} dùng {{ $value | humanizePercentage }}"
Chart đã kèm sẵn bộ rule kubernetes-mixin (KubePodCrashLooping, KubeNodeNotReady, KubePersistentVolumeFillingUp…). Bắt đầu bằng rule có sẵn, chỉ viết thêm rule riêng khi cần ngưỡng hoặc routing khác. Cách chọn severity, ai bị page: xem 19.2.
6. So sánh công cụ UI để vận hành cluster
| Công cụ | Chạy ở đâu | Mạnh | Rủi ro / hạn chế |
|---|---|---|---|
| Kubernetes Dashboard | Trong cluster (web) | Xem, sửa, xem log, exec qua trình duyệt | Từng là cửa hậu nổi tiếng (Tesla 2018 bị đào coin). Không expose ra Internet; truy cập qua kubectl port-forward; login bằng token ServiceAccount có RBAC tối thiểu (view), không gán cluster-admin |
| k9s | Terminal máy dev | Nhanh, phím tắt, xem log/describe/exec, dùng quyền kubeconfig của bạn | Chỉ là client, không lịch sử; dễ lỡ tay xóa nếu quyền rộng (có chế độ --readonly) |
| Lens / Headlamp | Desktop app (Headlamp còn chạy được trong cluster) | GUI nhiều cluster, biểu đồ, plugin. Headlamp là open source CNCF | Lens bản mới cần tài khoản/license cho công ty; vẫn dùng quyền kubeconfig |
| Cloud console (EKS/AKS/GKE) | Web của cloud | Gắn IAM, xem workload, node group, CloudWatch Container Insights / Azure Monitor | Phụ thuộc cloud, cần map IAM ↔ RBAC (EKS access entries, AKS Entra ID); metric chi tiết tính tiền |
| Grafana | Trong cluster | Lịch sử, alert, so sánh theo thời gian | Chỉ xem, không thao tác resource |
7. Khi nào nên dùng / không nên dùng
- Nên: cluster production nào cũng cần ít nhất kube-state-metrics + node-exporter + cAdvisor + alert cơ bản. kube-prometheus-stack gom đủ trong một chart.
- Không nên tự vận hành nếu đã dùng managed (Amazon Managed Prometheus, Azure Monitor managed Prometheus, Grafana Cloud) và team nhỏ: vẫn dùng ServiceMonitor/PrometheusRule nhưng để cloud lo storage.
- Không dùng metrics-server để làm dashboard lịch sử, nó không lưu gì.
- Một Prometheus cho nhiều cluster lâu dài thì cần Thanos/Mimir; retention dài trên một Prometheus local rất tốn disk.
kubectl top báo payment dùng 400Mi, mà Grafana lại hiện 650Mi. Cái nào đúng nhỉ?
Thường là cả hai đúng, chỉ đang đo khác thứ. Kiểm tra ba điểm:
- Metric khác nhau:
kubectl topdùng working set. Nếu panel Grafana dùngcontainer_memory_usage_bytesthì nó gồm cả page cache (file đọc gần đây), luôn lớn hơn. OOM killer và eviction nhìn working set, nên dashboard nên dùngcontainer_memory_working_set_bytes. - Cộng trùng: query thiếu
container!=""sẽ cộng series cấp Pod với series container. - Thời điểm: metrics-server lấy mẫu mỗi 15–60s, Grafana đang hiện max trong khoảng zoom.
sum by (pod) (container_memory_working_set_bytes{namespace="shop", pod=~"payment.*", container!=""})
Hai con số gần khớp nhau là đúng.
Team dev muốn tự xem Pod của mình. Mình bật Kubernetes Dashboard cho cả team có ổn không?
Được, nhưng phải khóa chặt. Rủi ro nằm ở quyền, không ở UI:
Dashboard expose Internet + ServiceAccount cluster-admin
= ai vào được URL là chủ cluster (đọc Secret, exec vào postgres)
- Không tạo Ingress/LoadBalancer public. Dev dùng
kubectl port-forwardhoặc đi sau SSO proxy (oauth2-proxy) trong VPN. - Mỗi người đăng nhập bằng danh tính của chính họ (OIDC) hoặc token ngắn hạn
kubectl create token, không dùng chung một token dài hạn. - RoleBinding ClusterRole
viewtrong namespaceshopthôi. Roleviewmặc định không đọc được Secret, tốt.
kubectl -n shop create rolebinding dev-view --clusterrole=view --group=shop-devs
Thực tế nhiều team bỏ Dashboard luôn: dev dùng k9s/Headlamp với kubeconfig của chính họ (cùng RBAC trên), còn xem lịch sử thì vào Grafana.
Phỏng vấn hỏi về giám sát Kubernetes cluster thì trả lời sao?
Q1. metrics-server, kube-state-metrics và node-exporter khác nhau thế nào?
🇻🇳 Tiếng Việt:
metrics-server chỉ giữ CPU/RAM hiện tại của Pod và node, phục vụ
kubectl topvà HPA, không có lịch sử. kube-state-metrics đọc API server và xuất trạng thái object như số replica mong muốn và thực tế, phase của Pod, số lần restart. cAdvisor trong kubelet đo tài nguyên từng container, còn node-exporter là DaemonSet đo máy chủ: disk, CPU, network. Muốn biết Pod Pending thì nhìn kube-state-metrics, ai ăn RAM thì nhìn cAdvisor, node đầy disk thì nhìn node-exporter.
🇬🇧 English:
metrics-server only keeps the current CPU and memory of pods and nodes for
kubectl topand the HPA; it has no history. kube-state-metrics reads the API server and exposes object state such as desired versus available replicas, pod phase and restart counts. cAdvisor inside the kubelet measures per-container resource usage, and node-exporter is a DaemonSet that measures the host: disk, CPU and network. Pending pods come from kube-state-metrics, who is eating memory comes from cAdvisor, and a full node disk comes from node-exporter.
Q2. Bạn dựng monitoring cho một cluster mới thế nào?
🇻🇳 Tiếng Việt:
Em cài kube-prometheus-stack bằng Helm: nó gồm Prometheus Operator, Prometheus, Alertmanager, Grafana với dashboard có sẵn, kube-state-metrics và node-exporter. Em cấu hình retention và PVC cho Prometheus, lấy password Grafana từ Secret, và cho Prometheus chọn ServiceMonitor, PodMonitor, PrometheusRule ở mọi namespace để mỗi team tự khai báo cạnh app của mình. Sau đó bật alert cho CrashLoopBackOff, Pod Pending lâu, node NotReady, PVC gần đầy, với
forđủ dài để không báo nhầm lúc deploy.
🇬🇧 English:
I install kube-prometheus-stack with Helm, which brings the Prometheus Operator, Prometheus, Alertmanager, Grafana with built-in dashboards, kube-state-metrics and node-exporter. I set retention and a PVC for Prometheus, take the Grafana password from a Secret, and let Prometheus select ServiceMonitors, PodMonitors and PrometheusRules from every namespace so each team declares them next to its app. Then I enable alerts for CrashLoopBackOff, long-pending pods, NotReady nodes and nearly full PVCs, with
fordurations long enough not to fire during normal deploys.
Q3. Làm sao phát hiện container bị CPU throttling, và xử lý thế nào?
🇻🇳 Tiếng Việt:
Em chia
container_cpu_cfs_throttled_periods_totalchocontainer_cpu_cfs_periods_totaltheo rate 5 phút. Tỷ lệ trên khoảng 25% nghĩa là container thường xuyên chạm CPU limit, với JVM thì latency và GC pause tăng dù CPU trung bình nhìn thấp. Cách xử lý là tăng hoặc bỏ CPU limit, giữ requests đúng thực tế, hoặc giảm số GC/thread cho khớp số CPU được cấp.
🇬🇧 English:
I divide the 5-minute rate of
container_cpu_cfs_throttled_periods_totalbycontainer_cpu_cfs_periods_total. A ratio above roughly 25% means the container keeps hitting its CPU limit; for a JVM that shows up as higher latency and GC pauses even when average CPU looks low. The fix is to raise or remove the CPU limit, keep requests realistic, or size GC and thread pools to the CPUs actually granted.
Cách nhớ ngắn gọn
- KSM = trạng thái object; cAdvisor = container tốn bao nhiêu; node-exporter = máy còn bao nhiêu; metrics-server = chỉ "bây giờ".
- kube-prometheus-stack: Operator + Prometheus + Alertmanager + Grafana + KSM + node-exporter.
- App tự khai báo ServiceMonitor/PodMonitor/PrometheusRule; Operator lo config.
- Alert có
for:hợp lý: CrashLoop 5m, Pending 10m, NotReady 5m, PVC 15m. - Dashboard không expose; RBAC
viewtheo namespace.
Tự kiểm tra
Pod Pending thì lấy metric từ đâu, vì sao không phải từ app?
kube-state-metrics (kube_pod_status_phase{phase="Pending"}). Pod chưa chạy thì app chưa xuất metric nào.
Xóa metrics-server thì Grafana có mất biểu đồ CPU không?
Không. Grafana đọc Prometheus (cAdvisor). Thứ hỏng là kubectl top và HPA.
Tạo ServiceMonitor mà Prometheus không scrape. Nghi đầu tiên?
Selector: mặc định chart chỉ chọn ServiceMonitor có label release=monitoring, trừ khi đặt serviceMonitorSelectorNilUsesHelmValues: false. Sau đó kiểm tra label Service và tên port.
Vì sao alert CrashLoopBackOff cần for: 5m?
Lúc deploy hoặc dependency khởi động chậm, container có thể restart 1–2 lần rồi ổn. Báo ngay sẽ thành alert nhiễu.
Metric nào cho biết PVC postgres sắp đầy?
kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes.