Mở đầu — vì sao bài này quan trọng
Ở bài trước bạn đã hiểu "Three Pillars" của observability: metrics, logs và traces. Bài này chúng ta đi sâu vào công cụ số một để thu thập metrics trong hệ sinh thái cloud-native: Prometheus.
Vì sao Prometheus quan trọng đến mức phải dành riêng một bài? Vì trong thực tế, khi bạn xin việc DevOps hoặc SRE ở Việt Nam — từ các công ty như VNG, Tiki, MoMo, Grab, cho tới các startup fintech nhỏ — Prometheus gần như luôn xuất hiện trong yêu cầu công việc. Nó là dự án tốt nghiệp thứ hai của CNCF (sau Kubernetes), là "de-facto standard" cho monitoring hạ tầng container. Nếu bạn chạy Kubernetes mà không có Prometheus, bạn gần như đang lái xe ban đêm không đèn pha.
Điều làm nhiều bạn mới học lúng túng là Prometheus hoạt động theo một triết lý khá khác biệt so với những công cụ monitoring truyền thống (kiểu Nagios, Zabbix): nó pull metrics thay vì chờ ứng dụng push lên. Hiểu đúng mô hình này ngay từ đầu sẽ giúp bạn tránh hàng loạt sai lầm về kiến trúc sau này. Bài học này tập trung vào việc thu thập metrics — nghĩa là làm sao Prometheus lấy được số liệu, số liệu đó có định dạng ra sao, và bạn viết truy vấn để moi thông tin ra thế nào. Phần dashboard (Grafana) và cảnh báo (Alerting) sẽ để dành cho các bài sau.
Khái niệm cốt lõi
Mô hình Pull-based — Prometheus tự đi "hỏi thăm"
Khác biệt căn bản nhất của Prometheus là pull-based. Thay vì ứng dụng của bạn phải chủ động gửi số liệu đi đâu đó, Prometheus tự động scrape (cào) dữ liệu bằng cách gọi HTTP GET tới một endpoint — thường là /metrics — của ứng dụng hoặc exporter, cứ mỗi N giây một lần (thường là 15s).
Hãy tưởng tượng một giáo viên điểm danh. Mô hình push giống như học sinh tự lên bảng ghi tên khi có mặt; nếu học sinh quên ghi hoặc ghi sai, bạn không biết. Mô hình pull giống như giáo viên gọi tên từng người theo danh sách — nếu ai không trả lời, bạn biết ngay người đó vắng. Đây chính là ưu điểm lớn: Prometheus tự động phát hiện được khi một service "chết" (endpoint không phản hồi), thông qua metric đặc biệt tên là up (bằng 1 nếu scrape thành công, 0 nếu thất bại).
Ưu điểm của pull-based:
- Dễ biết service sống hay chết nhờ metric
up. - Kiểm soát tập trung: bạn cấu hình scrape ở một chỗ (Prometheus), không cần cấu hình từng app biết gửi đi đâu.
- An toàn hơn về mạng: app chỉ cần mở một endpoint HTTP read-only, không cần credentials để đẩy dữ liệu ra ngoài.
- Dễ debug: bạn có thể tự
curl http://app:8080/metricsđể xem chính xác Prometheus nhìn thấy gì.
Định dạng metric (OpenMetrics / Prometheus exposition format)
Khi bạn curl vào /metrics, bạn sẽ thấy text đơn giản kiểu này:
# HELP http_requests_total Tong so HTTP request da xu ly
# TYPE http_requests_total counter
http_requests_total{method="POST", handler="/checkout", status="200"} 18240
http_requests_total{method="GET", handler="/products", status="200"} 95120
http_requests_total{method="POST", handler="/checkout", status="500"} 37
Cấu trúc một dòng metric gồm: tên metric + labels (trong dấu ngoặc nhọn) + giá trị số. Dòng # HELP mô tả ý nghĩa, # TYPE khai báo loại metric. Labels chính là "gia vị" của Prometheus — chúng cho phép bạn cắt lát dữ liệu theo nhiều chiều (method, endpoint, status code, instance...).
Bốn loại metric type
Bạn phải nắm chắc bốn loại này vì chọn sai loại là lỗi rất phổ biến:
- Counter — chỉ tăng, không bao giờ giảm (reset về 0 khi restart). Dùng cho: tổng số request, tổng số lỗi, tổng số byte đã gửi. Không bao giờ đọc giá trị tuyệt đối của counter — luôn dùng
rate()để tính tốc độ tăng. - Gauge — có thể tăng và giảm. Dùng cho: mức sử dụng RAM hiện tại, số connection đang mở, nhiệt độ, số message trong queue.
- Histogram — chia dữ liệu vào các "buckets" (khoảng giá trị). Dùng cho: đo phân bố độ trễ (latency). Cho phép tính percentile (p95, p99) phía server bằng
histogram_quantile(). - Summary — giống histogram nhưng tính percentile ngay tại client. Ít linh hoạt hơn histogram khi bạn muốn tổng hợp nhiều instance, nên thực tế đa số người dùng ưu tiên histogram.
PromQL — ngôn ngữ truy vấn
Prometheus lưu dữ liệu dưới dạng time-series (chuỗi thời gian): mỗi tổ hợp tên metric + labels là một chuỗi riêng, mỗi chuỗi là một dãy điểm (timestamp, value). Để hỏi dữ liệu, bạn dùng PromQL. Vài ví dụ cốt lõi:
- Tốc độ request mỗi giây, trung bình trong 5 phút gần nhất:
rate(http_requests_total[5m])
- Tỷ lệ lỗi 5xx:
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
- Độ trễ p95:
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))Hàm rate() cực kỳ quan trọng: nó biến một counter (số cứ tăng mãi) thành "tốc độ tăng mỗi giây" — thứ mà con người thực sự quan tâm.
Exporter — cầu nối cho những thứ không tự nói tiếng Prometheus
Không phải hệ thống nào cũng tự expose /metrics. Với những thứ đó, bạn dùng exporter — một chương trình nhỏ đọc số liệu từ hệ thống rồi dịch sang định dạng Prometheus:
- node_exporter: metrics của máy chủ Linux (CPU, RAM, disk, network).
- blackbox_exporter: probe từ bên ngoài (ping, HTTP check, TLS cert expiry).
- mysqld_exporter, postgres_exporter, redis_exporter: cho database.
Tình huống thực tế
Ví dụ 1 — Startup fintech ở TP.HCM và sự cố "im lặng đáng sợ"
Một startup ví điện tử tại TP.HCM (gọi là PayZ, khoảng 15 kỹ sư) chạy backend trên Kubernetes ở AWS Singapore. Ban đầu họ chỉ dựa vào log để biết hệ thống có ổn không. Một chiều thứ Sáu, API /checkout bắt đầu trả về lỗi 500 lác đác — khoảng 2% request. Log thì ngập, nhưng không ai nhận ra vì 98% vẫn thành công.
Sau khi cài Prometheus, họ instrument counter http_requests_total có label status. Chỉ với truy vấn sum(rate(http_requests_total{status="500"}[5m])), họ thấy rõ đường lỗi nhích lên từ 0 vào lúc 14h03 — đúng thời điểm một dependency (service tỷ giá) bị chậm. Điều quan trọng: metric up{job="checkout"} vẫn bằng 1, nghĩa là service không chết hẳn, chỉ lỗi một phần — thứ mà health-check đơn giản không bắt được.
Bài học: metrics dạng counter theo label status cho bạn thấy vấn đề "một phần" mà log và health-check thô bỏ sót. Và mô hình pull với metric up giúp phân biệt "chết hẳn" với "sống nhưng lỗi".
Ví dụ 2 — Công ty thương mại điện tử và bài học chọn sai metric type
Một đội e-commerce (giả định, quy mô tương tự Tiki thời kỳ đầu) muốn theo dõi độ trễ trang sản phẩm. Kỹ sư mới dùng một gauge tên page_latency_ms, mỗi request ghi đè giá trị mới. Kết quả: khi Prometheus scrape mỗi 15s, nó chỉ chộp được đúng giá trị latency của một request ngẫu nhiên tại thời điểm đó — bỏ sót toàn bộ các request giữa hai lần scrape. Trong đợt sale 12/12, dashboard hiển thị latency "đẹp" 80ms trong khi khách hàng thực tế chờ 3 giây.
Họ sửa lại thành histogram http_request_duration_seconds với buckets hợp lý (0.1, 0.3, 0.5, 1, 2.5, 5 giây). Giờ mọi request đều được đếm vào bucket tương ứng, và truy vấn histogram_quantile(0.99, ...) cho ra p99 thật là 3.1s. Họ phát hiện ra một truy vấn N+1 vào database.
Bài học: gauge dùng cho giá trị "hiện tại", còn latency của từng request phải dùng histogram. Chọn sai type khiến dashboard đẹp mà vô dụng.
Ví dụ 3 — Cardinality explosion làm sập Prometheus
Một team ở Hà Nội thêm label user_id vào metric api_calls_total để "tiện phân tích theo người dùng". Với 800.000 user active, mỗi user tạo ra một time-series riêng, nhân với vài label khác — con số nổ lên hàng chục triệu series. Prometheus ngốn hết RAM (16GB) và bị OOMKilled liên tục, dashboard trắng xóa đúng lúc cần nhất.
Họ gỡ label user_id ra (phân tích theo user thuộc về công cụ analytics, không phải Prometheus), giữ lại các label có cardinality thấp như endpoint, method, status. Số series tụt về vài nghìn, Prometheus chạy nhẹ nhàng.
Bài học: mỗi tổ hợp label là một time-series tốn bộ nhớ. Không bao giờ đưa các giá trị có cardinality cao (user_id, email, request_id, full URL có tham số) vào label. Đây là lỗi số một khiến Prometheus sập trong thực tế.
Hướng dẫn từng bước
Ta dựng một Prometheus tối thiểu để scrape node_exporter, chạy bằng Docker.
Bước 1 — Viết file cấu hình prometheus.yml:
global:
scrape_interval: 15s # cứ 15 giay scrape mot lan
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus" # Prometheus tu monitor chinh no
static_configs:
- targets: ["localhost:9090"]
- job_name: "node" # monitor may chu Linux
static_configs:
- targets: ["node-exporter:9100"]
Bước 2 — Chạy node_exporter và Prometheus bằng docker-compose:
services:
node-exporter:
image: prom/node-exporter:latest
ports: ["9100:9100"]
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports: ["9090:9090"]
Chạy: docker compose up -d
Bước 3 — Kiểm tra target đã UP chưa:
Mở trình duyệt vào http://localhost:9090/targets. Bạn sẽ thấy hai job prometheus và node, cột State phải là UP (màu xanh). Nếu là DOWN, kiểm tra lại tên host/port và network.
Bước 4 — Tự tay xem raw metrics:
Chạy curl http://localhost:9100/metrics | head -40. Đây là đúng những gì Prometheus nhìn thấy. Thói quen curl vào endpoint để debug sẽ cứu bạn rất nhiều lần.
Bước 5 — Chạy vài truy vấn PromQL ở tab http://localhost:9090/graph:
up— xem service nào sống.rate(node_cpu_seconds_total{mode="idle"}[5m])— thời gian CPU rảnh.100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)— phần trăm CPU đang dùng.node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100— phần trăm RAM còn trống.
prometheus-client cho Python, prom-client cho Node.js). Thêm một counter cho request, một histogram cho latency, expose ra /metrics, rồi thêm job mới vào prometheus.yml.Lỗi thường gặp & mẹo
- Đọc giá trị tuyệt đối của counter. Counter chỉ tăng và reset khi restart, nên con số
18240bản thân nó vô nghĩa. Luôn bọc bằngrate()hoặcincrease(). - Cardinality explosion. Như ví dụ 3 — đừng bao giờ đặt user_id, request_id, email, hay URL kèm query param làm label. Ước lượng số series = tích các cardinality của mọi label.
- Chọn sai metric type. Latency → histogram. Số hiện tại (RAM, queue size) → gauge. Tổng tích lũy (request, lỗi) → counter.
- Scrape interval quá ngắn. Đặt 1s cho mọi thứ nghe có vẻ "real-time" nhưng làm nổ dung lượng lưu trữ và tải CPU. 15s là mặc định hợp lý; chỉ giảm khi thật sự cần.
- Quên rằng Prometheus không phải để lưu trữ vĩnh viễn. Mặc định giữ dữ liệu 15 ngày. Muốn giữ lâu (cost analysis, capacity planning) thì cần remote storage như Thanos, Cortex, hoặc VictoriaMetrics.
- Nhầm Pushgateway là mặc định. Chỉ dùng Pushgateway cho batch/cron job ngắn hạn. Dùng nó cho service thường trực là anti-pattern (mất khả năng phát hiện service chết).
- Mẹo — dùng
up == 0để nhanh chóng phát hiện target nào đang chết. Đây là một trong những alert đầu tiên bạn nên viết. - Mẹo — kiểm tra cardinality bằng truy vấn
topk(10, count by (__name__)({__name__=~".+"}))để tìm metric nào đang phình to nhất.
Bài tập thực hành
- Dựng stack cơ bản: Dùng docker-compose ở phần hướng dẫn để chạy Prometheus + node_exporter. Xác nhận cả hai target đều UP ở
/targets. - Khám phá PromQL: Viết truy vấn tính phần trăm CPU đang dùng và phần trăm RAM còn trống. Chụp màn hình đồ thị.
- Instrument một app nhỏ: Viết một web service Python/Node đơn giản, thêm counter
http_requests_total{status}và histogram cho latency. Expose/metrics, thêm vàoprometheus.yml, và viết truy vấn tính tỷ lệ lỗi 5xx cùng p95 latency. - Thử nghiệm cardinality: Cố tình thêm một label ngẫu nhiên có nhiều giá trị (ví dụ số random 1..10000) vào một counter, quan sát số series tăng qua truy vấn
count({__name__="ten_metric_cua_ban"}). Rút ra kết luận về nguy cơ. - (Nâng cao) Cài
blackbox_exporterđể probe một website VN bất kỳ (ví dụ trang chủ công ty bạn), viết truy vấn kiểm tra thời gian phản hồi HTTP và ngày hết hạn chứng chỉ TLS.
Tóm tắt
Prometheus thu thập metrics theo mô hình pull: nó tự động scrape endpoint /metrics của app/exporter mỗi N giây, nhờ đó biết ngay service sống hay chết qua metric up. Dữ liệu được lưu dưới dạng time-series với tên metric + labels, theo định dạng exposition đơn giản. Bạn cần nắm chắc bốn metric type — counter (tích lũy, luôn bọc rate()), gauge (giá trị hiện tại), histogram (phân bố latency, tính percentile), summary — và chọn đúng type cho từng bài toán. PromQL là công cụ để moi thông tin, với rate() và histogram_quantile() là hai hàm bạn dùng nhiều nhất. Với những hệ thống không tự nói tiếng Prometheus, dùng exporter làm cầu nối.
Ba cạm bẫy lớn nhất cần nhớ: đừng đọc giá trị thô của counter, đừng nhét label cardinality cao (đây là nguyên nhân số một làm sập Prometheus), và đừng chọn sai metric type. Nắm vững phần thu thập metrics này là nền tảng để bạn bước sang Grafana (dashboard) và Alerting ở các bài tiếp theo.