Prometheus & Grafana: metric types, PromQL, dashboard

4 câu hỏi phỏng vấn~30 phútTừ khóa: pull model, scrape, ServiceMonitor, counter, gauge, histogram, summary, label, cardinality, rate, irate, increase, sum by, histogram_quantile, recording rule, exemplar, Grafana, dashboard as code, remote write

Tiếp đi: Prometheus và Grafana. Mình đã có /actuator/prometheus rồi, giờ dựng phần còn lại trên Kubernetes thế nào?

Phần app (Micrometer, Counter, Timer, endpoint) đã ở Actuator. Bài này đi phía bên kia: Prometheus lấy số về thế nào, bốn loại metric khác nhau ra sao, viết PromQL đúng, và dashboard cho từng service.

1. Bài toán

order, payment, inventory mỗi service 4 Pod, Pod sinh ra chết đi liên tục theo HPA. Cần:

2. Vấn đề

3. Giải pháp: pull model + 4 loại metric

Prometheus kéo (pull) số liệu: định kỳ 15–30s gọi HTTP /actuator/prometheus của từng target. Target tìm qua Kubernetes service discovery (với Prometheus Operator: ServiceMonitor/PodMonitor). Mỗi chuỗi số duy nhất theo tên metric + bộ label là một time series.

LoạiĐặc điểmVí dụ shop-appĐọc bằng
CounterChỉ tăng, reset khi restartorders_created_total, http_server_requests_seconds_countrate(), increase()
GaugeLên xuống tự do, giá trị tức thờihikaricp_connections_active, jvm_memory_used_bytesGiá trị trực tiếp, avg_over_time
HistogramĐếm số quan sát rơi vào từng bucket le + _sum + _counthttp_server_requests_seconds_buckethistogram_quantile() trên server
SummaryPercentile tính sẵn trong app..._seconds{quantile="0.99"}Đọc trực tiếp, không gộp được

4. Code từng bước

Bước 1: Cài stack

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace \
  --set grafana.adminPassword="${GRAFANA_ADMIN_PASSWORD}"

Stack gồm Prometheus Operator, Prometheus, Alertmanager, Grafana, node-exporter, kube-state-metrics.

Bước 2: Cho Prometheus tìm order/payment/inventory

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: shop-services
  namespace: shop
  labels: { release: monitoring }      # để Prometheus của stack chọn
spec:
  selector:
    matchLabels: { part-of: shop-app } # Service của 3 app đều có label này
  endpoints:
  - port: http
    path: /actuator/prometheus
    interval: 15s
# application.yml (mỗi service)
management:
  endpoints.web.exposure.include: health,prometheus
  metrics:
    tags:
      app: ${spring.application.name}     # label chung, cardinality thấp
    distribution:
      percentiles-histogram:
        http.server.requests: true        # histogram, không phải summary

Bước 3: Metric nghiệp vụ có label đúng cách

@Component
public class PaymentMetrics {
    private final MeterRegistry registry;
    public PaymentMetrics(MeterRegistry registry) { this.registry = registry; }

    public void recordCharge(String provider, ChargeResult result) {
        Counter.builder("payment.charges")
               .tag("provider", provider)          // vnpay, momo, stripe: ~5 giá trị
               .tag("result", result.name())       // SUCCESS, DECLINED, TIMEOUT
               // KHÔNG .tag("customerId", ...): mỗi khách 1 series
               .register(registry)
               .increment();
    }
}

Số series = tích số giá trị của mỗi label: 5 provider × 3 result × 4 Pod = 60. Thêm customerId với 1 triệu khách = 60 triệu series.

Bước 4: PromQL cơ bản và kiểm tra

# Request/s theo service (counter → luôn qua rate)
sum by (app) (rate(http_server_requests_seconds_count{namespace="shop"}[5m]))

# Tỷ lệ lỗi 5xx
sum by (app) (rate(http_server_requests_seconds_count{outcome="SERVER_ERROR"}[5m]))
/ sum by (app) (rate(http_server_requests_seconds_count[5m]))

# p99 latency: gộp bucket của mọi Pod TRƯỚC, rồi mới tính quantile
histogram_quantile(0.99,
  sum by (app, le) (rate(http_server_requests_seconds_bucket{namespace="shop"}[5m])))

# Số đơn tạo trong 1 giờ qua
increase(orders_created_total[1h])

# Connection pool sắp cạn (USE: saturation)
max by (pod) (hikaricp_connections_pending{app="payment-service"})

# Kiểm tra target có UP không
kubectl port-forward svc/monitoring-kube-prometheus-prometheus 9090 -n monitoring
curl -s 'localhost:9090/api/v1/query?query=up{namespace="shop"}' | jq '.data.result[] | [.metric.pod, .value[1]]'

5. Cách hoạt động

          ┌──────── Kubernetes API (danh sách Pod/Endpoint) ────────┐
          ▼                                                          │
   Prometheus ──scrape 15s──► order-service Pod :8080/actuator/prometheus
      │        ──scrape──────► payment-service Pod ...
      │        ──scrape──────► node-exporter, kube-state-metrics
      │ TSDB (2h block trên disk, giữ ~15 ngày)
      ├──► rule: recording + alert ──► Alertmanager ──► Slack / PagerDuty
      ├──► remote_write ──► Thanos / Mimir (lưu dài hạn, HA)
      ▼
   Grafana (datasource Prometheus, Loki, Tempo)

rate() xử lý counter reset:
giá trị: 100 150 200 | 10 60      (Pod restart ở giữa)
rate thấy giảm → coi như reset, cộng bù → tốc độ vẫn đúng

histogram_quantile tính từ bucket:
le=0.1: 600   le=0.3: 900   le=0.8: 985   le=2: 998   le=+Inf: 1000
p99 = request thứ 990 → nằm giữa bucket 0.8 và 2 → nội suy ≈ 1.25s
(độ chính xác phụ thuộc cách chia bucket)

Dashboard mỗi service nên theo một bố cục cố định để on-call nhìn quen mắt:

┌── order-service ─────────────── $env  $pod ─┐
│ Row 1 RED:  req/s │ error % │ p50/p95/p99    │
│ Row 2 SLO:  budget còn lại │ burn rate        │
│ Row 3 Phụ thuộc: gọi payment/inventory (RED) │
│ Row 4 USE JVM: heap, GC pause, threads, CPU  │
│ Row 5 USE pool: Hikari active/pending, Tomcat│
└───────────── deploy annotation trên đồ thị ──┘

6. So sánh: histogram vs summary

HistogramSummary
Percentile tính ởPrometheus (query time)App (client)
Gộp nhiều PodĐược (sum by (le))Không (trung bình p99 ≠ p99)
Đổi percentile sau nàyĐượcPhải deploy lại
Độ chính xácPhụ thuộc bucketChính xác trong một Pod
Chi phíNhiều series (mỗi bucket 1 series)Ít series
Khuyến nghịMặc định cho microservicesChỉ khi một instance duy nhất

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

Mình thấy có người viết avg(http_server_requests_seconds{quantile="0.99"}) cho p99 toàn service. Sai ở đâu nhỉ?

Sai về toán. Percentile không trung bình được:

Pod A: 1000 request, p99 = 100ms
Pod B:   10 request, p99 = 5s       (Pod mới, JIT chưa warm)
avg(p99) = 2.55s   ← không phải p99 của 1010 request
p99 thật ≈ 100ms   (10 request chậm của B chỉ ~1%)

Kết quả có thể lệch theo cả hai chiều, nên không tin được. Cách đúng: xuất histogram, cộng bucket của mọi Pod rồi mới tính quantile:

histogram_quantile(0.99, sum by (le) (rate(http_server_requests_seconds_bucket{app="order-service"}[5m])))

Lưu ý thứ tự: rate trên từng series trước, sum by (le) sau, phải giữ label le. Nếu bỏ le khỏi by, histogram_quantile trả về rỗng.

Prometheus của mình tự nhiên dùng 30GB RAM rồi bị OOMKilled. Tìm thủ phạm kiểu gì?

Gần như chắc chắn là cardinality nổ. Memory Prometheus tỷ lệ với số series đang hoạt động. Tìm như sau:

# 1. Tổng số series
prometheus_tsdb_head_series

# 2. Metric nào nhiều series nhất
topk(10, count by (__name__) ({__name__=~".+"}))

# 3. Label nào của metric đó có nhiều giá trị
count(count by (uri) (http_server_requests_seconds_count{app="order-service"}))

# Hoặc xem trang Status → TSDB Status trong UI Prometheus

Thủ phạm hay gặp ở Spring Boot:

Chặn từ app bằng MeterFilter, và chặn ở Prometheus bằng sample_limit/metricRelabelings:

@Bean
MeterFilter limitUriCardinality() {
    return MeterFilter.maximumAllowableTags("http.server.requests", "uri", 100,
            MeterFilter.deny());
}

Phỏng vấn hỏi về Prometheus thì trả lời sao?

Q1. Pull model của Prometheus có ưu nhược điểm gì?

🇻🇳 Tiếng Việt:

Prometheus chủ động scrape endpoint của từng target theo chu kỳ. Ưu điểm: biết ngay target chết qua metric up, app không cần biết địa chỉ Prometheus, dễ kiểm soát tải, và có thể curl endpoint để debug. Nhược điểm: job ngắn có thể kết thúc trước khi bị scrape, phải dùng Pushgateway hoặc push OTLP; và Prometheus phải với tới được target qua mạng. Trên Kubernetes em dùng ServiceMonitor để tự tìm Pod.

🇬🇧 English:

Prometheus actively scrapes each target's endpoint on an interval. Pros: a dead target shows up immediately through the up metric, apps don't need to know where Prometheus is, load is easy to control, and you can curl the endpoint to debug. Cons: short-lived jobs may finish before being scraped, so you need a Pushgateway or OTLP push, and Prometheus must be able to reach targets over the network. On Kubernetes I use ServiceMonitors for automatic Pod discovery.

Q2. Phân biệt counter, gauge, histogram, summary.

🇻🇳 Tiếng Việt:

Counter chỉ tăng, như tổng request, đọc qua rate hay increase. Gauge lên xuống, như số connection đang dùng hay heap. Histogram đếm quan sát theo bucket, percentile tính ở server bằng histogram_quantile và gộp được giữa các instance. Summary tính percentile sẵn trong app nên chính xác nhưng không gộp được giữa các Pod. Với microservices nhiều replica em dùng histogram.

🇬🇧 English:

A counter only goes up, like total requests, and you read it with rate or increase. A gauge goes up and down, like active connections or heap usage. A histogram counts observations into buckets and computes percentiles on the server with histogram_quantile, so it aggregates across instances. A summary computes percentiles inside the app, which is precise but can't be aggregated across Pods. For replicated microservices I use histograms.

Q3. Vì sao không đặt userId làm label?

🇻🇳 Tiếng Việt:

Mỗi tổ hợp label là một time series riêng, Prometheus giữ series đang hoạt động trong memory. userId làm số series tăng theo số người dùng, dẫn tới memory tăng, query chậm, có thể OOM. Label chỉ nên có giá trị giới hạn như method, status, route template, provider. Thông tin theo từng user để trong log hoặc span attribute.

🇬🇧 English:

Every label combination is a separate time series, and Prometheus keeps active series in memory. A userId label makes the series count grow with your user base, which means more memory, slow queries, and possibly OOM. Labels should have bounded values like method, status, route template or provider. Per-user details belong in logs or span attributes.

Q4. Tính p99 latency của một service nhiều Pod thế nào?

🇻🇳 Tiếng Việt:

Bật histogram trong Micrometer, rồi dùng histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m]))): rate từng series, cộng bucket của mọi Pod giữ label le, rồi tính quantile. Không lấy trung bình p99 của từng Pod vì percentile không cộng được. Độ chính xác phụ thuộc bucket, nên em thêm bucket đúng ngưỡng SLO.

🇬🇧 English:

Enable histograms in Micrometer, then use histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m]))): rate each series, sum buckets across Pods while keeping the le label, then compute the quantile. Never average per-Pod p99s because percentiles don't aggregate. Accuracy depends on bucket boundaries, so I add buckets at the SLO thresholds.

Cách nhớ ngắn gọn


Tự kiểm tra

Vì sao vẽ trực tiếp giá trị counter là vô nghĩa?

Nó chỉ tăng và reset khi restart; cần rate/increase để ra tốc độ.

scrape interval 30s, cửa sổ rate(...[30s]) có ổn không?

Không: cần ít nhất 2 mẫu, nên dùng ≥ 4× interval (2m).

Prometheus tìm Pod mới của payment-service thế nào?

Qua Kubernetes service discovery; với Operator là ServiceMonitor chọn Service theo label.

Bỏ le khỏi sum by trước histogram_quantile thì sao?

Mất thông tin bucket, kết quả rỗng/sai.

Lưu metric 1 năm thì dùng gì?

remote_write sang Thanos/Mimir/Cortex hoặc dịch vụ managed.