Observability: nhìn thấy trước khi khách hàng gọi

Ba bài qua bạn đã có bộ công cụ mạnh: Helm đóng gói, ArgoCD để git điều khiển, Rancher quản hạm đội cluster. Hệ thống chạy tốt. Nhưng có một câu hỏi mà…

13 tháng 7, 2026 · 30 phút đọc

Ba bài qua bạn đã có bộ công cụ mạnh: Helm đóng gói, ArgoCD để git điều khiển, Rancher quản hạm đội cluster. Hệ thống chạy tốt. Nhưng có một câu hỏi mà không công cụ nào ở trên trả lời được:

"Ngay lúc này, hệ thống của tôi đang khỏe hay đang ốm?"

Và câu hỏi đau hơn: khi nó ốm, ai báo cho bạn — công cụ giám sát, hay khách hàng?

Ở chặng K8s bạn debug bằng kubectl logsdescribe — tuyệt vời khi bạn đã biết Pod nào hỏng. Nhưng với 4 cluster và 50 service, bạn không biết phải nhìn vào đâu. Tệ hơn: Pod chết là log bay theo (Bài 02 chặng K8s — --previous chỉ cứu được một lần chạy trước).

Bài này dựng hệ thống quan sát: Prometheus (metric) + Grafana (biểu đồ) + Loki (log tập trung) + Alertmanager (báo động). Mục tiêu cuối: bạn biết trước khách hàng.


Vấn đề: bay trong sương mù

   ❌ Không có observability:

   14:32  Đĩa /var trên worker-02 đầy 100% (log không rotate).
          → Không ai biết. Không có gì báo.
   14:47  kubelet không ghi được → node NotReady.
          → Pod bị trục xuất, dồn sang worker-01.
   14:51  worker-01 quá tải → Pod OOMKilled hàng loạt (Bài 08 chặng K8s).
   14:58  Khách hàng gọi tổng đài: "web không vào được".   ← BẠN BIẾT TIN Ở ĐÂY
   15:00  Bạn mở kubectl... và bắt đầu đoán mò. Nhìn Pod nào? Node nào?
          Chuyện bắt đầu từ lúc mấy giờ? Không có lịch sử. Không biết.

Ba lỗ hổng chí mạng:

  1. Không có cảnh báo — sự cố âm ỉ 26 phút mà không ai hay.
  2. Không có lịch sửkubectl top chỉ cho biết hiện tại; bạn cần biết lúc 14:32 đã xảy ra gì.
  3. Log bay theo Pod — Pod bị xóa, log đi theo. Muốn điều tra thì không còn gì để đọc.

Ba trụ cột của Observability

   METRICS   Số liệu theo thời gian (CPU 82%, 340 req/s, 12 lỗi/phút)
             → Nhẹ, lưu được lâu, hợp để CẢNH BÁO và xem xu hướng
             → Trả lời: "CÓ vấn đề không? Từ lúc nào?"          [Prometheus]

   LOGS      Sự kiện dạng văn bản ("ERROR: connection refused to db:3306")
             → Nặng hơn, chi tiết hơn
             → Trả lời: "Vấn đề đó là GÌ, cụ thể?"               [Loki]

   TRACES    Đường đi của MỘT request qua nhiều service
             → Trả lời: "Request chậm — chậm ở ĐOẠN NÀO?"        [Tempo/Jaeger]

→ Quy trình xử lý sự cố kinh điển: Metric báo độngLog cho biết chuyện gìTrace chỉ ra chỗ nào (nếu có microservice). Bài này tập trung hai trụ đầu — chúng giải quyết 90% nhu cầu thực tế.


metrics-server ≠ Prometheus

Hai thứ hay bị nhầm, phân biệt ngay:

   metrics-server        Chgimetric HIN TI, trong RAM, vài phút.
                         → Phc vụ: kubectl top, và HPA (Bài 03 chặng K8s)
                         → KHÔNG có lch sử, KHÔNG cnh báo, KHÔNG biu đồ.

   Prometheus            LƯU TRchui thi gian, hàng tun/tháng.
                         → Truy vn quá khứ, vbiu đồ, đặt cnh báo.
                         → Đây mi là hthng giám sát tht sự.

⚠️ Cài metrics-server rồi tưởng "đã có monitoring" là hiểu nhầm phổ biến. Nó chỉ cho HPA sống — không cứu bạn lúc 3 giờ sáng.


Prometheus — mô hình kéo (pull)

Điểm cốt lõi cần hiểu: Prometheus tự đi kéo metric về, chứ không đợi app đẩy lên.

   Prometheus  ──scrape (HTTP GET /metrics mỗi 30s)──►  app / node-exporter / kubelet
        │                                                (mỗi thứ tự phơi ra 1 endpoint
        ▼                                                 text đơn giản: tên + số)
   Lưu vào TSDB (cơ sở dữ liệu chuỗi thời gian)
        │
        ├──► PromQL: truy vấn (dùng cho Grafana vẽ, và cho luật cảnh báo)
        └──► Alertmanager: khi luật vi phạm → gửi Slack/email/PagerDuty

Nó tự tìm mục tiêu bằng service discovery của K8s — Pod mới sinh ra là nó biết, Pod chết là nó thôi. Rất hợp với thế giới Pod sinh-tử liên tục.

Nguồn metric trong cluster

   node-exporter    CPU/RAM/đĩa/mạng của từng NODE
                    → chạy bằng DaemonSet (Bài 07 chặng K8s — 1 Pod/node!)
   kube-state-metrics  Trạng thái OBJECT K8s (Deployment có đủ replica không?
                    Pod đang CrashLoop? PVC Pending?)
   kubelet/cAdvisor    CPU/RAM của từng CONTAINER
   App của bạn      tự phơi /metrics (thư viện client Prometheus)

ServiceMonitor — CRD, và bài học về Operator

Đây cũng là lúc trả lời câu hỏi treo từ Bài 06 chặng K8s ("dùng operator cho database" — nhưng operator là gì?).

   CRD (Custom Resource Definition)
     = thêm LOẠI OBJECT MỚI vào K8s (ngoài Pod/Service/Deployment)
       vd: ServiceMonitor, Certificate (cert-manager — Bài 09 K8s), Application (ArgoCD)

   Operator
     = CRD + một Controller canh chúng, chạy đúng reconciliation loop (Bài 01 K8s)
       "thấy ServiceMonitor mới → tự cấu hình Prometheus scrape service đó"

   → Đây là cách hệ sinh thái K8s lớn lên: KHÔNG sửa K8s,
     mà THÊM loại object + một controller hiểu chúng.

Nhờ Prometheus Operator, bạn khai báo việc giám sát theo kiểu K8s thay vì sửa file config:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: myapp
  namespace: myapp
  labels:
    release: monitoring          # ⚠️ PHẢI khớp label Prometheus đang tìm!
spec:
  selector:
    matchLabels: { app: myapp }  # chọn Service nào (label — Bài 03/04 K8s)
  endpoints:
    - port: metrics              # tên cổng trong Service
      interval: 30s

⚠️ Pitfall số một của ServiceMonitor: label release: không khớp → Prometheus lặng lẽ không scrape, không báo lỗi. Kiểm tra bằng UI Prometheus → Status → Targets.


PromQL — vừa đủ để dùng

# CPU dùng thực tế của từng Pod (core)
sum(rate(container_cpu_usage_seconds_total{namespace="prod"}[5m])) by (pod)

# RAM đang dùng / gii hnPod nào sp OOMKilled? (Bài 08 K8s)
sum(container_memory_working_set_bytes{namespace="prod"}) by (pod)
  / sum(kube_pod_container_resource_limits{resource="memory"}) by (pod)

# Pod đang CrashLoop (restart tăng trong 15 phút)
increase(kube_pod_container_status_restarts_total[15m]) > 3

# Tỉ lệ lỗi HTTP 5xx
sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m]))

# Đĩa còn dưới 15% — CHÍNH LÀ sự cố ở đầu bài
node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.15

Hai hàm phải nhớ: rate() (tốc độ thay đổi của counter — dùng cho request/s, lỗi/s) và increase() (mức tăng trong khoảng thời gian).


Alerting — nơi observability trả tiền

Biểu đồ đẹp mà không có cảnh báo thì bạn vẫn phải ngồi nhìn màn hình. Cảnh báo mới là thứ đánh thức bạn trước khách hàng.

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: canh-bao-co-ban
  namespace: monitoring
  labels: { release: monitoring }
spec:
  groups:
    - name: cluster
      rules:
        - alert: DiaSapDay
          expr: node_filesystem_avail_bytes{mountpoint="/"} 
                / node_filesystem_size_bytes{mountpoint="/"} < 0.15
          for: 10m                      # phải sai LIÊN TỤC 10 phút mới báo
          labels: { severity: warning }  #  → tránh báo động giả do nhiễu
          annotations:
            summary: "Đĩa node {{ $labels.instance }} còn dưới 15%"
            runbook: "https://wiki.cty.vn/runbook/dia-day"   # ⭐ hướng dẫn xử lý

        - alert: PodCrashLoop
          expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
          for: 5m
          labels: { severity: critical }
          annotations:
            summary: "Pod {{ $labels.pod }} đang CrashLoopBackOff"

        - alert: ChungChiSapHetHan          # ⭐ nhớ Bài 11 chặng K8s!
          expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 21
          for: 1h
          labels: { severity: warning }
          annotations:
            summary: "Chứng chỉ hết hạn trong dưới 21 ngày"

→ Cảnh báo cuối chính là tấm lưới an toàn cho quả bom 1 năm ở Bài 11 chặng K8s. Observability không chỉ bắt sự cố — nó ngăn sự cố.

Nguyên tắc chống "mệt mỏi vì cảnh báo"

Đây là bài học đắt nhất của cả bài (chuyện thật ở dưới):

   ✅ Cảnh báo trên TRIỆU CHỨNG người dùng cảm nhận được
      (web lỗi 5xx, độ trễ tăng, đĩa sắp đầy, node chết)

   ❌ ĐỪNG cảnh báo mọi thứ dao động
      ("CPU pod X 71%") — CPU cao mà app vẫn phục vụ tốt thì KHÔNG phải sự cố.

   Mọi cảnh báo phải trả lời được: "Nhận cái này lúc 3h sáng, tôi PHẢI LÀM GÌ?"
   → Không trả lời được = KHÔNG phải cảnh báo. Cho vào dashboard, đừng đánh thức ai.

   Mỗi cảnh báo phải có RUNBOOK (link hướng dẫn xử lý).
   Dùng `for:` để lọc nhiễu. Phân severity: critical (gọi) vs warning (xem giờ hành chính).

Bốn tín hiệu vàng (Golden Signals)

Nếu không biết bắt đầu từ đâu, giám sát bốn thứ này cho mọi service:

   Latency      độ trễ — request mất bao lâu (p95, p99, không phải trung bình!)
   Traffic      lưu lượng — bao nhiêu request/s
   Errors       tỉ lệ lỗi — bao nhiêu % thất bi
   Saturation   độ bão hòa — tài nguyên còn bao nhiêu (CPU/RAM/đĩa/queue)

Loki — log tập trung, và vì sao cần nó

Nhớ Bài 02 chặng K8s: kubectl logs --previous chỉ cho bạn log của một lần chạy trước. Pod bị xóa? Log biến mất vĩnh viễn. Node chết? Mất sạch.

   Promtail (DaemonSet — 1 Pod/node, Bài 07 K8s)
        │ gom log MỌI container trên node
        ▼
   Loki (lưu trữ tập trung, lâu dài)
        │
        ▼
   Grafana → truy vấn LogQL, xem log cả cluster ở một chỗ
# Log lỗi của app trong namespace prod, 1 giqua
{namespace="prod", app="api"} |= "ERROR"

# Đếm tc độ libiến LOG thành METRIC để cnh báo!
sum(rate({namespace="prod"} |= "ERROR" [5m])) by (app)

💡 Vì sao Loki chứ không phải ELK? Loki chỉ đánh index label (namespace, pod, app) chứ không index toàn bộ nội dung log → nhẹ hơn nhiều, rẻ hơn nhiều, và dùng chung nhãn với Prometheus nên chuyển qua lại giữa metric ↔ log rất mượt. ELK mạnh hơn về tìm kiếm toàn văn nhưng nặng và tốn kém — với đa số team, Loki là điểm cân bằng tốt hơn.


🚀 Lab — dựng cả bộ trong 15 phút

Dùng Helm (Bài 01) — mọi thứ đã được đóng gói sẵn.

1. Cài kube-prometheus-stack (Prometheus + Grafana + Alertmanager + exporters)

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

helm install monitoring prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace \
  --set grafana.adminPassword=admin123

kubectl -n monitoring get pods
# → prometheus, grafana, alertmanager, node-exporter (DaemonSet!), kube-state-metrics

2. Mở Grafana

kubectl -n monitoring port-forward svc/monitoring-grafana 3000:80
# → http://localhost:3000  (admin / admin123)
# Dashboards → Browse: đã có SẴN hàng chục dashboard cho K8s!
#   "Kubernetes / Compute Resources / Namespace (Pods)" — thử ngay

3. Truy vấn PromQL

kubectl -n monitoring port-forward svc/monitoring-kube-prometheus-prometheus 9090:9090
# → http://localhost:9090

# Trong ô Query, thử:
#   up                        (tất cả target đang scrape — 1 = sống)
#   kube_pod_status_phase     (Pod ở phase nào — nhớ Bài 02 K8s)
#   sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace)
#
# Status → Targets: xem Prometheus đang scrape những gì (nơi debug ServiceMonitor)

4. Tạo sự cố và xem nó hiện lên

# Tạo Pod CrashLoop (Bài 02 chặng K8s)
kubectl run crasher --image=busybox -- sh -c "exit 1"

# Đợi vài phút, rồi query trong Prometheus:
#   increase(kube_pod_container_status_restarts_total{pod="crasher"}[15m])
# → Số restart tăng dần. ĐÂY là thứ đáng cảnh báo.
kubectl delete pod crasher

5. Thêm Loki

helm repo add grafana https://grafana.github.io/helm-charts && helm repo update
helm install loki grafana/loki-stack -n monitoring \
  --set promtail.enabled=true

# Trong Grafana: Explore → chọn datasource Loki → query:
#   {namespace="default"}
# → Log của mọi Pod, ở một chỗ, KHÔNG cần biết tên Pod. ✨

6. Dọn

helm uninstall monitoring -n monitoring ; helm uninstall loki -n monitoring
kubectl delete ns monitoring

→ Khoảnh khắc "à há": bạn vừa dựng cả hệ giám sát bằng hai lệnh Helm. Và trong Grafana đã có sẵn dashboard cho mọi thứ chặng K8s dạy — Pod, Deployment, node, PVC. Đây chính là ROI của Helm (Bài 01).


Câu chuyện thực tế

Sự cố đĩa đầy — 26 phút mù. Chính là kịch bản mở đầu bài, và nó là chuyện thật. Log ứng dụng ghi vào emptyDir (Bài 05 chặng K8s), không rotate. Đĩa /var của worker-02 đầy dần suốt ba tuần — một đường thẳng đi lên đẹp đẽ mà không ai nhìn thấy, vì không có gì vẽ nó ra. Rồi nó chạm 100%: kubelet không ghi được → node NotReady → Pod dồn sang node còn lại → OOMKilled dây chuyền. Khách hàng báo trước chúng tôi 26 phút.

Cay đắng nhất: đây là sự cố có ba tuần để phát hiện. Một cảnh báo disk < 15% đơn giản đã cho chúng tôi 21 ngày để xử lý bình thản trong giờ hành chính. Sau vụ đó, chúng tôi cài kube-prometheus-stack trong một buổi chiều. Ba tháng sau, chính cảnh báo đó bắn lần nữa (một service khác rò log) — chúng tôi xử lý trong 15 phút, không ai biết có chuyện gì xảy ra. Đó là lúc observability trả hết vốn.

Cú tát: 200 cảnh báo mỗi ngày. Hào hứng quá đà, chúng tôi cảnh báo mọi thứ: CPU > 70%, RAM > 70%, mọi Pod restart, mọi request 4xx. Kênh Slack biến thành thác nước. Tuần đầu ai cũng đọc. Tuần hai, mọi người mute kênh. Tuần ba... một cảnh báo thật — database sắp hết kết nối — trôi qua giữa 200 cảnh báo rác, không ai đọc. Sự cố xảy ra thật.

Chúng tôi cắt xuống 12 cảnh báo, mỗi cái đều trả lời được câu "nhận lúc 3h sáng thì phải làm gì?", mỗi cái có runbook. Số cảnh báo giảm 94%, và độ tin cậy của cảnh báo về 100% — giờ Slack kêu là mọi người ngẩng đầu lên. Cảnh báo mà không ai đọc thì tệ hơn không có cảnh báo — vì nó cho bạn cảm giác an toàn giả.

Bài học:

  1. Không nhìn thấy = không sửa được. kubectl tuyệt khi bạn biết nhìn đâu. Với 50 service, bạn cần thứ chỉ cho bạn nhìn đâu.

  2. Cảnh báo trên triệu chứng, không trên dao động. "CPU 71%" không phải sự cố. "Web trả 5xx" mới là. Mỗi cảnh báo phải trả lời: nhận lúc 3h sáng thì làm gì?

  3. Alert fatigue là kẻ giết người thầm lặng. 200 cảnh báo/ngày = 0 cảnh báo được đọc. Ít mà tin cậy hơn nhiều mà rác.

  4. Mỗi cảnh báo phải có runbook. Người trực lúc 3 giờ sáng có thể không phải bạn — họ cần biết làm gì, không chỉ có chuyện gì.

  5. Log phải rời khỏi Pod. Pod là vật thể mau hỏng (Bài 02 chặng K8s); log nằm trong nó cũng vậy. Loki/Promtail giữ lại chứng cứ khi Pod đã tan biến.

  6. Metric báo vấn đề; log nói vấn đề gì. Hai trụ cột, dùng cùng nhau. Grafana cho bạn nhảy từ biểu đồ sang log đúng khoảnh khắc đó.


Pitfalls

  1. Tưởng metrics-server là monitoring. Nó chỉ phục vụ kubectl top và HPA — không lịch sử, không cảnh báo. Cần Prometheus.

  2. Cảnh báo quá nhiều → alert fatigue → cảnh báo thật bị bỏ lỡ. Ít mà chất.

  3. Cảnh báo không có runbook → người trực bối rối, xử lý chậm.

  4. Không đặt for: → báo động giả liên tục vì nhiễu nhất thời. for: 5m/10m lọc rất tốt.

  5. Cardinality explosion — nhét label giá trị vô hạn (user_id, request_id, IP) vào metric → Prometheus phình RAM và sập. Label chỉ dùng cho giá trị hữu hạn (namespace, pod, status_code).

  6. Không giới hạn retention/đĩa cho Prometheus → PVC đầy, Prometheus chết đúng lúc bạn cần nó nhất. Đặt retention: 15d và đủ dung lượng.

  7. ServiceMonitor sai label release: → Prometheus im lặng không scrape. Không có lỗi nào báo. Kiểm tra ở Status → Targets.

  8. Dashboard Grafana tạo tay, không lưu git → mất Grafana là mất sạch công sức. Đưa dashboard vào git + ArgoCD (Bài 02!).

  9. Chỉ giám sát hạ tầng, quên giám sát ứng dụng. CPU đẹp nhưng người dùng vẫn lỗi 500 — vì bạn không đo tỉ lệ lỗi. Nhớ 4 tín hiệu vàng.

  10. Dùng giá trị trung bình cho độ trễ. Trung bình 200ms nghe êm, nhưng p99 có thể 8 giây — và 1% khách hàng đó đang rất giận. Dùng p95/p99.

  11. Quên cảnh báo cho chính hệ giám sát. Prometheus chết mà không ai biết = mù hoàn toàn nhưng tưởng "yên tĩnh nghĩa là ổn".


Tóm tắt

  • Ba trụ cột: Metrics (có vấn đề không?) · Logs (vấn đề gì?) · Traces (chậm ở đâu?).
  • metrics-server ≠ Prometheus. Cái đầu chỉ cho kubectl top + HPA; cái sau mới là giám sát thật (lịch sử + cảnh báo).
  • Prometheus kéo (pull) metric qua /metrics, tự khám phá target trong K8s. Nguồn: node-exporter (DaemonSet), kube-state-metrics (trạng thái object), kubelet/cAdvisor, app.
  • CRD + Operator = cách hệ sinh thái mở rộng K8s (ServiceMonitor, Certificate, Application) — thêm object mới + controller chạy reconciliation loop.
  • PromQL: rate(), increase(); nhớ p95/p99 thay vì trung bình.
  • Alerting là nơi trả tiền: cảnh báo trên triệu chứng, có for:, có runbook, phân severity. Alert fatigue giết observability — ít mà tin cậy.
  • 4 tín hiệu vàng: Latency · Traffic · Errors · Saturation.
  • Loki + Promtail (DaemonSet) = log tập trung, sống lâu hơn Pod. Nhẹ hơn ELK vì chỉ index label.
  • Cài cả bộ bằng Helm (kube-prometheus-stack) — hai lệnh, có sẵn dashboard.
  • ⚠️ Cardinality explosion, retention, ServiceMonitor label — ba pitfall làm sập Prometheus.

Bài sau (Bài 05 — CI/CD) khép nốt vòng tròn cuối cùng. Nhìn lại: manifest đã ở git (ArgoCD), cluster đã tự đồng bộ, hệ thống đã có mắt. Nhưng đoạn từ git push code tới image mới nằm trong registry và tag được cập nhật trong manifest — bạn vẫn đang làm bằng tay: build trên laptop, docker push, rồi sửa tag trong repo manifest.

Ta sẽ tự động hóa nốt đoạn đó: test → build → scan → push → bump tag → ArgoCD lo phần còn lại. Và bạn sẽ thấy vì sao tag latest (pitfall Bài 08 chặng K8s) khiến toàn bộ dây chuyền này sụp đổ.

Nếu hôm nay bạn nhận được cảnh báo "đĩa còn 15%" thay vì cuộc gọi "web sập rồi" — bạn đã đi trước khách hàng một bước. Đó là toàn bộ mục đích của observability.

Minh Hưng

← Bài trước
Rancher: quản trị nhiều cluster từ một chỗ
Bài tiếp theo →
CI/CD: từ git push tới production, không chạm tay
Zalo tư vấn