Observability 4 — Instrumentation & Exporters

13 thg 7, 2026 3 lượt xem
#metrics
#devops
#prometheus
#exporters
#instrumentation

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_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íHistogramSummary
Quantile tính ở đâuPhía server (PromQL)Phía client (trong app)
Gộp được nhiều instance? (cộng bucket rồi mới tính)Không (không thể trung bình các quantile)
Độ chính xác quantileXấp xỉ, phụ thuộc bucketChính xác trong sai số cấu hình, nhưng cố định
Chọn quantile lúc queryLinh hoạt (bất kỳ phân vị nào)Cố định lúc khai báo
Chi phí phía clientRẻ (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_total với label method.

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ùng request.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 async cầ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:

ExporterGiám sát cái gìMetric tiêu biểu
node_exporterHost Linux: CPU, RAM, đĩa, network, filesystemnode_cpu_seconds_total, node_filesystem_avail_bytes, node_memory_MemAvailable_bytes
cAdvisorTài nguyên từng containercontainer_cpu_usage_seconds_total, container_memory_usage_bytes
postgres_exporterPostgreSQL: connection, replication lag, tuple, kích thước DBpg_stat_replication_*, pg_stat_database_*, pg_up
redis_exporterRedis: memory, hit ratio, số key, latencyredis_memory_used_bytes, redis_keyspace_hits_total
kafka_exporterKafka: consumer lag, offset, số partitionkafka_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):

  1. API dữ liệu — thêm prometheus_client như đ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%.
  2. PostgreSQL replica — chạy postgres_exporter, theo dõi pg_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ũ).
  3. Pipeline ETL — đẩy pipeline_last_success_timestamp_secondspipeline_rows_processed lê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ỳ.
  4. Host & containernode_exporter + cAdvisor cho toàn bộ node, theo dõi đĩa và RAM.
  5. Từ ngoài nhìn vàoblackbox exporter probe /health mỗi 30 giây, cảnh báo probe_success == 0 và 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ằng histogram_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.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

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á.

13 thg 7, 2026 3