Giám sát Kubernetes: Prometheus stack & dashboard

4 câu hỏi phỏng vấn~30 phútTừ khóa: metrics-server, kube-state-metrics, cAdvisor, kubelet, node-exporter, kube-prometheus-stack, Prometheus Operator, ServiceMonitor, PodMonitor, PrometheusRule, Alertmanager, Grafana, CrashLoopBackOff, OOMKilled, CPU throttling, PVC usage, Kubernetes Dashboard, k9s, Lens, Headlamp

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:

2. Vấn đề

3. Giải pháp: biết metric đến từ đâu

NguồnChạy ở đâuĐo cái gìVí dụ metricDùng cho
metrics-serverDeployment, đọc kubeletCPU/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-metricsDeployment, đọc API serverTrạng thái object: desired vs thực tếkube_deployment_status_replicas_available, kube_pod_status_phase, kube_pod_container_status_restarts_totalPending, restart, replicas thiếu
cAdvisor / kubeletTrong kubelet mỗi nodeTài nguyên từng container (cgroup, xem bài cgroups)container_memory_working_set_bytes, container_cpu_cfs_throttled_periods_totalAi ăn CPU/RAM, throttling
kubelet /metricsMỗi nodeVolume, trạng thái kubeletkubelet_volume_stats_used_bytesPVC sắp đầy
node-exporterDaemonSetMáy chủ: disk, CPU, network, loadnode_filesystem_avail_bytes, node_cpu_seconds_totalNode 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"):

DashboardTrả lời câu hỏi
ClusterCả 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?
PodContainer nào bị throttle, usage so với requests/limits?
WorkloadDeployment 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 ở đâuMạnhRủi ro / hạn chế
Kubernetes DashboardTrong cluster (web)Xem, sửa, xem log, exec qua trình duyệtTừ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
k9sTerminal máy devNhanh, phím tắt, xem log/describe/exec, dùng quyền kubeconfig của bạnChỉ là client, không lịch sử; dễ lỡ tay xóa nếu quyền rộng (có chế độ --readonly)
Lens / HeadlampDesktop app (Headlamp còn chạy được trong cluster)GUI nhiều cluster, biểu đồ, plugin. Headlamp là open source CNCFLens 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 cloudGắn IAM, xem workload, node group, CloudWatch Container Insights / Azure MonitorPhụ thuộc cloud, cần map IAM ↔ RBAC (EKS access entries, AKS Entra ID); metric chi tiết tính tiền
GrafanaTrong clusterLịch sử, alert, so sánh theo thời gianChỉ xem, không thao tác resource

7. Khi nào nên dùng / không nên dùng

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:

  1. Metric khác nhau: kubectl top dùng working set. Nếu panel Grafana dùng container_memory_usage_bytes thì 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ùng container_memory_working_set_bytes.
  2. Cộng trùng: query thiếu container!="" sẽ cộng series cấp Pod với series container.
  3. 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)
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 top và 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 top and 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 for durations 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_total cho container_cpu_cfs_periods_total theo 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_total by container_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


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.