Observability 4 — Instrumentation & Exporters
Từ "có Prometheus" đến "có dữ liệu để đo"
Ba bài trước dựng nền: tại sao cần observability (tổng quan), Prometheus scrape và lưu time-series ra sao (kiến trúc), và cách đọc dữ liệu bằng PromQL. Nhưng có một khoảng trống chưa lấp: dữ liệu ở đâu ra? Prometheus chỉ scrape được cái gì đó đã sẵn sàng phơi số liệu tại một endpoint HTTP dạng text (thường là /metrics). Việc tạo ra các số liệu đó gọi là instrumentation — nhúng đo đạc vào ứng dụng — và khi ứng dụng/hệ thống không tự phơi được thì ta dùng exporter làm cầu nối.
Bài này trả lời hai câu hỏi thực chiến của một team dữ liệu ngân hàng: (1) khi tôi viết API cấp dữ liệu, tôi nên tạo metric kiểu gì và đặt tên thế nào để không tự bắn vào chân mình? (2) làm sao giám sát những thứ tôi không sửa code được — host, container, PostgreSQL, Redis, Kafka, hay pipeline batch chạy vài giây rồi tắt?
Bốn kiểu metric và bản chất của chúng
Prometheus có đúng bốn kiểu metric. Chọn sai kiểu là lỗi nền tảng khiến toàn bộ PromQL phía sau vô nghĩa, nên phải nắm chắc bản chất.
Counter — chỉ tăng
Counter là bộ đếm đơn điệu tăng: nó chỉ đi lên, và về 0 khi process khởi động lại (restart). Dùng cho những đại lượng "tích lũy": tổng số HTTP request, tổng số lỗi, tổng số byte đã ghi, tổng số bản ghi pipeline đã xử lý.
Điều quan trọng: bạn gần như không bao giờ đọc giá trị thô của counter. Con số http_requests_total = 84.213.559 chẳng nói lên gì. Cái bạn cần là tốc độ thay đổi — dùng rate() hoặc increase() trong PromQL. Chính vì counter reset về 0 khi restart, rate() được thiết kế để tự phát hiện và xử lý reset đó (coi bước tụt là một lần đếm lại từ 0), nên đừng bao giờ tự trừ hai điểm counter bằng tay.
Gauge — lên xuống
Gauge là giá trị tức thời có thể tăng hoặc giảm: dung lượng RAM đang dùng, độ sâu (depth) của message queue, số connection đang mở trong pool, nhiệt độ CPU, số job đang chờ. Với gauge bạn đọc trực tiếp giá trị hiện tại, và các phép tổng hợp phù hợp là avg, max, min, sum chứ không phải rate().
Mẹo phân biệt nhanh: nếu câu hỏi là "bao nhiêu cái tính từ trước đến giờ?" → Counter. Nếu là "ngay bây giờ đang là bao nhiêu?" → Gauge.
Histogram — phân phối vào bucket
Trung bình (average) latency là một cái bẫy. Nếu 99 request mất 10ms và 1 request mất 5 giây, trung bình vẫn đẹp nhưng có khách hàng đang chờ 5 giây. Cái ta cần là quantile: p95, p99 — "95% request nhanh hơn bao nhiêu?".
Histogram giải bài này bằng cách chia miền giá trị thành các bucket (rổ) tích lũy. Khi khai báo histogram với các mốc le (less-or-equal) như 0.1, 0.5, 1, 2.5, 5, mỗi lần quan sát sẽ tăng bộ đếm của mọi bucket có mốc ≥ giá trị đó. Một histogram thực chất được Prometheus phơi ra dưới dạng nhiều series:
<tên>_bucket{le="..."}— counter cho từng rổ tích lũy.<tên>_sum— tổng mọi giá trị quan sát.<tên>_count— tổng số quan sát.
Điểm mạnh chí mạng: quantile được tính phía server bằng hàm histogram_quantile() trong PromQL, sau khi đã cộng gộp các bucket từ nhiều instance. Nghĩa là bạn có thể hỏi "p99 latency của toàn cụm 12 pod API là bao nhiêu?" — điều mà Summary không làm được. Đổi lại, kết quả là xấp xỉ, độ chính xác phụ thuộc vào việc bạn chọn bucket có hợp lý quanh vùng quan tâm hay không.
# p99 latency (giây) của API dữ liệu, gộp toàn cụm, cửa sổ 5 phút
histogram_quantile(
0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="data-api"}[5m]))
)
Summary — quantile tính phía client
Summary cũng phơi _sum và _count, nhưng thay vì bucket, nó phơi thẳng các quantile đã tính sẵn bên trong process qua nhãn quantile:
rpc_duration_seconds{quantile="0.5"} 0.012
rpc_duration_seconds{quantile="0.9"} 0.041
rpc_duration_seconds{quantile="0.99"} 0.180
Khác biệt và đánh đổi cốt lõi so với Histogram:
| Tiêu chí | Histogram | Summary |
|---|---|---|
| Quantile tính ở đâu | Phía server (PromQL) | Phía client (trong app) |
| Gộp được nhiều instance? | Có (cộng bucket rồi mới tính) | Không (không thể trung bình các quantile) |
| Độ chính xác quantile | Xấp xỉ, phụ thuộc bucket | Chính xác trong sai số cấu hình, nhưng cố định |
| Chọn quantile lúc query | Linh hoạt (bất kỳ phân vị nào) | Cố định lúc khai báo |
| Chi phí phía client | Rẻ (chỉ tăng counter) | Đắt hơn (duy trì cửa sổ trượt) |
Quy tắc thực dụng cho hệ thống có nhiều instance sau load balancer — tức gần như mọi API ngân hàng chạy nhiều pod: mặc định dùng Histogram. Chỉ chọn Summary khi bạn cần quantile chính xác của một process đơn lẻ và biết chắc sẽ không cần gộp cụm. Vì không thể lấy trung bình quantile của nhiều pod (p99 của cụm ≠ trung bình các p99), Summary gần như luôn là lựa chọn sai cho dịch vụ scale ngang.
Quy ước đặt tên metric và label
Đặt tên tùy hứng là món nợ kỹ thuật đắt nhất trong observability, vì đổi tên metric sau này sẽ làm gãy hàng loạt dashboard và alert. Prometheus có quy ước rõ ràng, nên tuân thủ ngay từ đầu.
Tên metric:
- Dùng
snake_case, có tiền tố namespace theo hệ thống/subsystem:dataapi_requests_total,pipeline_records_processed_total. - Hậu tố đơn vị bắt buộc rõ ràng. Counter kết thúc bằng
_total. Đại lượng thời gian dùng giây với hậu tố_seconds(không dùng ms — quy ước Prometheus là đơn vị cơ bản). Dung lượng dùng byte với hậu tố_bytes. Ví dụ:http_request_duration_seconds,queue_size_bytes,db_connections_open. - Tên metric mô tả cái được đo, không nhét chiều (dimension) vào tên. Sai:
requests_get_total,requests_post_total. Đúng:requests_totalvới labelmethod.
Label — con dao hai lưỡi:
Label là thứ khiến Prometheus mạnh mẽ (cắt lát dữ liệu theo method, status, endpoint) nhưng cũng là nơi dễ gây thảm họa nhất. Mỗi tổ hợp giá trị label duy nhất tạo ra một time-series riêng. Đây là công thức của cardinality (số lượng series), và cardinality quyết định tiêu tốn RAM/đĩa của Prometheus.
CẢNH BÁO nổ cardinality (cardinality explosion): không bao giờ đặt vào label các giá trị có miền vô hạn hoặc rất lớn:
user_id,customer_id,account_no,transaction_id— mỗi khách hàng, mỗi giao dịch là một series mới. Một ngân hàng có hàng triệu khách → hàng triệu series → Prometheus sập.- URL đầy đủ có tham số (
/api/v1/customer/12345/tx/98765) — hãy chuẩn hóa thành route template/api/v1/customer/:id/tx/:id. - Timestamp, request ID, địa chỉ email, số tiền giao dịch thô.
Label chỉ nên chứa giá trị bounded (miền hữu hạn, biết trước): method (~7 giá trị), status (~vài chục mã HTTP), endpoint đã template hóa (vài chục), currency (VND/USD/EUR...), environment. Ước lượng thô: tổng số series ≈ tích các số giá trị của mọi label. Nếu con số này vượt vài chục nghìn cho một metric, hãy dừng lại và xem lại thiết kế. Muốn tra cứu theo customer_id — đó là việc của log/trace, không phải metric (xem logs & traces).
Instrument ứng dụng bằng client library
Với ứng dụng bạn viết được code (API dữ liệu, worker pipeline), cách chuẩn là dùng client library chính thức. Với Python là gói prometheus_client. Ba việc: (1) khai báo metric, (2) cập nhật chúng trong code, (3) phơi endpoint /metrics.
Ví dụ minh họa (KHÔNG phải SQL chạy được) — một API FastAPI cấp dữ liệu khách hàng, có instrument đủ ba kiểu metric:
from prometheus_client import Counter, Gauge, Histogram, make_asgi_app
from fastapi import FastAPI, Request
import time
app = FastAPI()
# Counter: tổng số request, cắt theo route template + method + status
REQUESTS = Counter(
"dataapi_requests_total",
"Tong so HTTP request toi data API",
["method", "route", "status"], # label BOUNDED — KHONG dat customer_id
)
# Histogram: phan phoi latency, bucket chon quanh SLA cua API ngan hang
LATENCY = Histogram(
"dataapi_request_duration_seconds",
"Do tre xu ly request (giay)",
["route"],
buckets=(0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5),
)
# Gauge: so request dang xu ly dong thoi (in-flight)
IN_FLIGHT = Gauge(
"dataapi_requests_in_flight",
"So request dang duoc xu ly",
)
@app.middleware("http")
async def instrument(request: Request, call_next):
route = request.scope.get("route").path if request.scope.get("route") else "unknown"
IN_FLIGHT.inc()
start = time.perf_counter()
try:
response = await call_next(request)
status = response.status_code
finally:
IN_FLIGHT.dec()
LATENCY.labels(route=route).observe(time.perf_counter() - start)
REQUESTS.labels(method=request.method, route=route, status=status).inc()
return response
# Phoi /metrics duoi dang ASGI sub-app — Prometheus se scrape endpoint nay
app.mount("/metrics", make_asgi_app())
Vài điểm cần nhớ trong đoạn trên:
- Dùng
route(template) chứ không dùngrequest.url.path(đường dẫn thực) — để tránh nổ cardinality vì mỗi ID khách hàng là một path khác nhau. - Với ứng dụng đa tiến trình (Gunicorn nhiều worker) cần cấu hình chế độ multiprocess của
prometheus_client, nếu không mỗi worker giữ counter riêng và số liệu sẽ nhảy khi load balancer chuyển worker. - Với API
asynccần thận trọng chi phí middleware; xem thêm bất đồng bộ & hiệu năng API.
Prometheus sẽ scrape endpoint đó qua cấu hình scrape_configs (minh họa YAML):
scrape_configs:
- job_name: "data-api"
metrics_path: /metrics
static_configs:
- targets: ["data-api.ncb.internal:8000"]
labels:
environment: "prod"
Exporter — khi hệ thống không tự phơi metric
Client library chỉ dùng được khi bạn có mã nguồn. Nhưng bạn không sửa được PostgreSQL, không nhúng code vào kernel Linux. Với chúng, ta chạy exporter: một tiến trình riêng đứng cạnh hệ thống mục tiêu, đọc trạng thái qua giao diện gốc của nó (system call, câu truy vấn quản trị, API nội bộ) rồi dịch thành metric Prometheus tại /metrics.
Các exporter chủ lực cho một stack dữ liệu ngân hàng:
| Exporter | Giám sát cái gì | Metric tiêu biểu |
|---|---|---|
| node_exporter | Host Linux: CPU, RAM, đĩa, network, filesystem | node_cpu_seconds_total, node_filesystem_avail_bytes, node_memory_MemAvailable_bytes |
| cAdvisor | Tài nguyên từng container | container_cpu_usage_seconds_total, container_memory_usage_bytes |
| postgres_exporter | PostgreSQL: connection, replication lag, tuple, kích thước DB | pg_stat_replication_*, pg_stat_database_*, pg_up |
| redis_exporter | Redis: memory, hit ratio, số key, latency | redis_memory_used_bytes, redis_keyspace_hits_total |
| kafka_exporter | Kafka: consumer lag, offset, số partition | kafka_consumergroup_lag, kafka_topic_partition_current_offset |
Trong đó kafka_consumergroup_lag là chỉ số vàng cho pipeline dữ liệu: nó cho biết consumer đang tụt lại bao xa so với dữ liệu mới nhất — lag tăng đều nghĩa là pipeline không nuốt kịp dữ liệu vào. pg_stat_replication cho biết replica có bị trễ so với primary không, cực quan trọng khi bạn phục vụ báo cáo từ replica đọc.
Pushgateway — cho batch job ngắn
Mô hình mặc định của Prometheus là pull (Prometheus chủ động scrape). Điều này gãy với batch job đời ngắn: một job ETL chạy 30 giây rồi tắt sẽ không kịp cho Prometheus scrape đúng lúc. Giải pháp là Pushgateway — job đẩy (push) metric lên Pushgateway trước khi thoát, và Prometheus scrape Pushgateway thay vì scrape job.
# Minh hoa: job ETL day metric ket qua truoc khi ket thuc
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
registry = CollectorRegistry()
g = Gauge("pipeline_last_success_timestamp_seconds",
"Thoi diem lan chay thanh cong gan nhat",
registry=registry)
rows = Gauge("pipeline_rows_processed",
"So dong da xu ly trong lan chay", registry=registry)
g.set_to_current_time()
rows.set(1_284_502)
push_to_gateway("pushgateway.ncb.internal:9091",
job="nightly_customer_etl", registry=registry)
Lưu ý Pushgateway chỉ dành cho batch/service-level job, không dùng thay cho pull ở service chạy dài — vì Pushgateway giữ giá trị cuối cùng mãi mãi (kể cả khi job đã chết) và không phản ánh liveness.
Blackbox exporter — probe từ bên ngoài
Tất cả những gì ở trên là giám sát từ bên trong (whitebox). Nhưng đôi khi bạn muốn hỏi câu của người dùng: "từ ngoài nhìn vào, endpoint này còn sống và trả lời trong bao lâu?". Đó là việc của blackbox exporter: nó probe một mục tiêu qua HTTP/HTTPS/TCP/ICMP/DNS và báo cáo có thành công không, thời gian phản hồi, và (với HTTPS) thời hạn còn lại của chứng chỉ TLS.
# Minh hoa: probe endpoint health cua API qua blackbox exporter
scrape_configs:
- job_name: "blackbox-http"
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://data-api.ncb.internal/health
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
Metric hữu ích: probe_success (1/0), probe_duration_seconds, probe_http_status_code, và probe_ssl_earliest_cert_expiry để cảnh báo trước khi chứng chỉ TLS hết hạn.
Use case thực tế
Bối cảnh: Team dữ liệu NCB vận hành một API cấp dữ liệu khách hàng (đọc từ PostgreSQL replica) và một pipeline ETL đêm nạp giao dịch vào kho. Sự cố: mỗi sáng đội nghiệp vụ báo "số liệu giao dịch hôm qua bị thiếu", nhưng không ai biết pipeline hỏng ở đâu và từ khi nào.
Kế hoạch instrument (làm trong 1 sprint):
- API dữ liệu — thêm
prometheus_clientnhư đoạn code trên:dataapi_requests_total(Counter),dataapi_request_duration_seconds(Histogram, bucket 10ms–5s),dataapi_requests_in_flight(Gauge). Route được template hóa để tránh nổ cardinality. SLO đặt ra: p99 latency < 800ms, tỷ lệ lỗi 5xx < 0.5%. - PostgreSQL replica — chạy
postgres_exporter, theo dõipg_stat_replicationđể phát hiện replica lag; alert khi lag > 30 giây (báo cáo sẽ đọc phải dữ liệu cũ). - Pipeline ETL — đẩy
pipeline_last_success_timestamp_secondsvàpipeline_rows_processedlên Pushgateway ở cuối mỗi lần chạy. Alert vàng:time() - pipeline_last_success_timestamp_seconds > 90000(25 giờ) → job đêm đã lỡ một chu kỳ. - Host & container —
node_exporter+cAdvisorcho toàn bộ node, theo dõi đĩa và RAM. - Từ ngoài nhìn vào —
blackbox exporterprobe/healthmỗi 30 giây, cảnh báoprobe_success == 0và chứng chỉ TLS còn dưới 14 ngày.
Kết quả đo được: Sau khi bật, một sáng alert bắn lúc 4:12 — pipeline_last_success_timestamp không cập nhật. Truy vào thấy kafka_consumergroup_lag của topic giao dịch tăng vọt từ nửa đêm: consumer treo do một partition bị rebalance. Thay vì đợi đội nghiệp vụ phát hiện thiếu số lúc 9 giờ, đội trực xử lý xong trước 5 giờ, dữ liệu kịp cho báo cáo đầu ngày. Đây chính là giá trị của instrument đúng kiểu metric đúng chỗ.
Các metric này sẽ được trực quan hóa trong bài kế (Grafana) và biến thành cảnh báo tự động trong bài về alerting.
Ghi nhớ
- Bốn kiểu metric: Counter (chỉ tăng, dùng với
rate()— số request/lỗi); Gauge (lên xuống, đọc trực tiếp — RAM/queue depth/connection); Histogram (bucket, quantile tính phía server bằnghistogram_quantile()— latency); Summary (quantile tính phía client, không gộp được nhiều instance). - Latency đa-instance → luôn dùng Histogram. Không thể lấy trung bình quantile của nhiều pod, nên Summary sai cho dịch vụ scale ngang.
- Đặt tên chuẩn:
snake_case, counter kết thúc_total, thời gian dùng_seconds, dung lượng dùng_bytes; namespace theo hệ thống. - Nổ cardinality là kẻ thù số một: không bao giờ đặt
user_id,customer_id,transaction_id, URL có tham số, timestamp vào label. Label chỉ chứa giá trị miền hữu hạn. Tra cứu theo ID là việc của log/trace. - Instrument code bạn viết được bằng client library (
prometheus_client) và expose/metrics; template hóa route trước khi gắn vào label. - Không sửa được code thì dùng exporter: node_exporter (host), cAdvisor (container), postgres/redis/kafka_exporter (datastore).
- Batch job ngắn → Pushgateway (push trước khi thoát); probe từ ngoài → blackbox exporter (
probe_success, hạn TLS). Không dùng Pushgateway cho service chạy dài.
Bài viết liên quan
Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.
Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.
Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.
Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.