Danh sách bài viết

Bài 46: Metrics — latency, throughput, error rate

3 metric cốt lõi của AI service theo RED method: Rate (throughput), Errors (tỷ lệ lỗi), Duration (latency phân phối P50/P95/P99). Bài này trình bày cách instrument code với Prometheus client Python, FastAPI middleware tự động, AI-specific metrics cho LLM (TTFT, tokens/s, cost per call), phân biệt Counter/Gauge/Histogram, SLO/SLI/SLA với error budget, và các lỗi phổ biến khi thiết kế metrics.

27/05/2026
2 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • ✅ Hiểu RED method và 3 metric cần đo đầu tiên cho bất kỳ service nào
  • ✅ Phân biệt average latency với P95/P99 và biết khi nào average gây hiểu sai
  • ✅ Dùng prometheus-client Python để instrument Counter, Gauge, Histogram
  • ✅ Tích hợp Prometheus metrics vào FastAPI qua prometheus-fastapi-instrumentator
  • ✅ Đo AI-specific metrics: token count, cost per call, TTFT cho streaming LLM
  • ✅ Hiểu SLI/SLO/SLA và tính error budget
2

Metrics vs Logs — hai công cụ khác mục đích

Bài 45 đã trình bày structured logging để capture sự kiện rời rạc. Metrics khác với log ở chỗ chúng là số liệu tổng hợp theo thời gian, không phải event riêng lẻ.

Tiêu chí Log Metric
Đơn vị Event rời rạc (1 dòng) Số đo tổng hợp theo thời gian
Storage Tăng tuyến tính với traffic Cố định — không phụ thuộc request count
Query Full-text search, filter Aggregation, percentile, rate
Dùng để Debug chi tiết từng request Alert, dashboard, trend, capacity planning
Ví dụ "Request req-abc123 failed: timeout" P99 latency = 4.2s trong 5 phút qua

Metrics phù hợp để trả lời "service đang khỏe không?" trong real-time và đặt alert. Log phù hợp để trả lời "request X bị gì?" khi cần debug cụ thể. Cả hai bổ sung cho nhau, không thay thế.

3

RED method — 3 metric cốt lõi

RED method (popularized trong Google SRE book và Weaveworks, 2018) đặt ra 3 câu hỏi tối thiểu cho bất kỳ service nào:

R — Rate

Số request service xử lý mỗi giây (RPS) hoặc mỗi phút (RPM). Cho biết service đang chịu tải bao nhiêu. Giảm đột ngột = upstream có vấn đề hoặc traffic drop. Tăng đột biến = cần scale.

E — Errors

Tỷ lệ request thất bại, tính bằng phần trăm. Bao gồm HTTP 5xx và lỗi nghiệp vụ (LLM trả content_filter, timeout, validation fail). SLO phổ biến: error rate dưới 0.1% hoặc 1% tùy loại service.

D — Duration

Latency phân phối của request. Không phải average — mà là P50, P95, P99 (chi tiết ở Section 4). Duration cao = user trải nghiệm chậm.

Chỉ cần 3 metric này là đủ để phát hiện phần lớn vấn đề production trước khi user phàn nàn. Dashboard hiển thị realtime 3 chỉ số này thường là đủ cho on-call triage.

4

Latency — tại sao cần percentile

Average latency là metric phổ biến nhất nhưng dễ gây hiểu nhầm.

Ví dụ cụ thể

Giả sử trong 1 phút có 100 request:

  • 99 request hoàn thành trong 100ms
  • 1 request mất 10s (LLM call gặp cold start)

Average = (99 × 100 + 1 × 10000) / 100 = 199ms. Nhìn vào average, service có vẻ ổn. Nhưng 1% user đợi 10 giây — đó là tail latency và average che giấu hoàn toàn.

Percentile giải quyết vấn đề này

  • P50 (median): 50% request nhanh hơn giá trị này. Biểu thị trải nghiệm của user điển hình.
  • P95: 95% request nhanh hơn giá trị này. 5% còn lại chậm hơn. Phát hiện nhóm user bị ảnh hưởng đáng kể.
  • P99: 99% request nhanh hơn giá trị này. 1% còn lại — tail latency. Với service 1000 RPS, 1% = 10 request/giây trải nghiệm tệ.

Với ví dụ trên: P50 = 100ms, P95 = 100ms, P99 = 10s. Dashboard ngay lập tức thấy P99 bất thường.

AI app có tail latency cao hơn app thông thường

LLM call có thời gian rất biến động: 500ms đến 20s cùng một prompt. Nguyên nhân gồm cold start của serverless inference, queue tại provider, độ dài output khác nhau. P99 quan trọng hơn P50 khi monitor LLM service.

5

Các loại latency trong AI app

Một request đến AI service đi qua nhiều bước. Đo từng lớp giúp pinpoint bottleneck:

End-to-end latency

Từ khi client gửi request đến khi nhận response cuối cùng. Bao gồm network đến server, xử lý, network về. Đây là thứ user thực sự cảm nhận.

Server latency

Từ khi server nhận đến khi trả response. Loại trừ network overhead và TLS handshake. Đây là phần bạn kiểm soát được trực tiếp.

Component latency

Từng step trong pipeline:

  • Embedding latency: thời gian chuyển text thành vector.
  • Retrieval latency: thời gian query vector DB.
  • LLM call latency: thời gian gửi prompt đến provider và nhận full response.
  • Post-processing latency: parse, validate, format output.

TTFT — Time To First Token

Với streaming LLM, đây là thời gian từ khi gửi request đến khi nhận token đầu tiên. TTFT quyết định perceived responsiveness — user thấy chữ xuất hiện nhanh hay phải chờ blank screen.

TPS — Tokens Per Second

Tốc độ sinh token sau khi đã có first token. TPS thấp = chữ "nhảy" chậm, user cảm giác lag dù TTFT tốt.

6

Counter, Gauge, Histogram — 3 kiểu metric Prometheus

Prometheus định nghĩa 4 metric type, trong đó 3 loại dùng phổ biến nhất:

Counter

Chỉ tăng, không bao giờ giảm. Reset về 0 khi process restart. Dùng cho: số request tổng cộng, số lỗi tổng cộng, số token đã dùng. Truy vấn qua rate(metric[5m]) để lấy tốc độ thay đổi.

from prometheus_client import Counter

REQUEST_COUNT = Counter(
    "api_requests_total",
    "Tổng số request",
    ["method", "endpoint", "status"],
)

# Tăng 1 mỗi khi request hoàn thành
REQUEST_COUNT.labels(method="POST", endpoint="/inference", status="ok").inc()

Gauge

Tăng hoặc giảm tùy thời điểm. Dùng cho: số connection đang mở, RAM usage hiện tại, queue size. Không cần tính rate — đọc trực tiếp giá trị hiện tại.

from prometheus_client import Gauge

ACTIVE_CONNECTIONS = Gauge("active_connections", "Số connection đang mở")
GPU_MEMORY_USED_MB = Gauge("gpu_memory_used_mb", "GPU memory đang dùng (MB)", ["device"])

ACTIVE_CONNECTIONS.inc()       # connection mới
ACTIVE_CONNECTIONS.dec()       # connection đóng
GPU_MEMORY_USED_MB.labels(device="0").set(4096)  # cập nhật giá trị

Histogram

Phân phối giá trị theo pre-defined buckets. Dùng cho: latency, response size. Tự động tính _count, _sum, và từng bucket. Từ đó Prometheus tính percentile qua histogram_quantile().

from prometheus_client import Histogram

REQUEST_LATENCY = Histogram(
    "api_request_duration_seconds",
    "Latency request",
    ["method", "endpoint"],
    buckets=[0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10, 30],
)

# Observe 1 giá trị
REQUEST_LATENCY.labels(method="POST", endpoint="/inference").observe(1.34)

Lưu ý về buckets: phải chọn range phù hợp với loại service. LLM call thường 0.5s đến 30s — bucket [0.01, 0.05, 0.1] không có ý nghĩa, mọi giá trị đều rơi vào bucket lớn nhất và percentile tính sai.

7

Instrument code với prometheus-client Python

pip install prometheus-client

Decorator pattern để track metrics cho bất kỳ function nào:

from prometheus_client import Counter, Histogram, start_http_server
import time

REQUEST_COUNT = Counter(
    "api_requests_total",
    "Total requests",
    ["method", "endpoint", "status"],
)
REQUEST_LATENCY = Histogram(
    "api_request_duration_seconds",
    "Request latency",
    ["method", "endpoint"],
    buckets=[0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10, 30],
)

def track_metrics(method, endpoint):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            status = "ok"
            try:
                result = func(*args, **kwargs)
                return result
            except Exception:
                status = "error"
                raise
            finally:
                REQUEST_COUNT.labels(method, endpoint, status).inc()
                REQUEST_LATENCY.labels(method, endpoint).observe(time.time() - start)
        return wrapper
    return decorator

# Expose metrics endpoint trên port riêng
start_http_server(9090)  # http://localhost:9090/metrics

Sử dụng decorator:

@track_metrics("POST", "/inference")
def run_inference(prompt: str):
    # ... gọi model
    return result

Context manager — cách linh hoạt hơn

prometheus-client cung cấp context manager .time() cho Histogram:

with REQUEST_LATENCY.labels(method="POST", endpoint="/inference").time():
    result = run_inference(prompt)

Cách này ngắn hơn decorator khi chỉ cần đo latency của 1 block code cụ thể.

8

FastAPI middleware với prometheus-fastapi-instrumentator

Viết decorator thủ công cho mỗi endpoint tốn công. Với FastAPI, có thể dùng package tích hợp sẵn:

pip install prometheus-fastapi-instrumentator
from fastapi import FastAPI
from prometheus_fastapi_instrumentator import Instrumentator

app = FastAPI()

# Instrument app và expose /metrics endpoint tự động
Instrumentator().instrument(app).expose(app)

Sau khi setup, endpoint GET /metrics expose tất cả metric dạng Prometheus text format. Các metric tự động có sẵn:

  • http_requests_total — Counter, label: method, handler, status.
  • http_request_duration_seconds — Histogram, latency từng endpoint.
  • http_request_size_bytes — Histogram, kích thước request body.
  • http_response_size_bytes — Histogram, kích thước response.

Tuỳ chỉnh buckets cho AI service

Default buckets của Prometheus (0.005, 0.01, 0.025, 0.1, 0.25, 0.5, 1, 2.5, 5, 10) phù hợp với web app thông thường. Với LLM endpoint cần buckets lớn hơn:

from prometheus_fastapi_instrumentator import Instrumentator
from prometheus_fastapi_instrumentator.metrics import default, latency

custom_latency = latency(
    buckets=[0.1, 0.5, 1, 2, 5, 10, 20, 30, 60]
)

Instrumentator(
    should_group_status_codes=False,
    excluded_handlers=["/health", "/metrics"],
).instrument(app, metric_namespace="ai_service").expose(app)
9

AI-specific metrics — LLM call

HTTP metrics từ middleware không đủ để hiểu AI service. Cần thêm lớp metrics đặc thù cho LLM:

from prometheus_client import Counter, Histogram
import time

LLM_TOKENS = Counter(
    "llm_tokens_total",
    "Tokens đã dùng",
    ["model", "direction"],  # direction: input / output
)
LLM_LATENCY = Histogram(
    "llm_call_duration_seconds",
    "Latency LLM call",
    ["model"],
    buckets=[0.5, 1, 2, 3, 5, 8, 12, 20, 30, 60],
)
LLM_COST = Counter(
    "llm_cost_usd_total",
    "Chi phí LLM",
    ["model"],
)
LLM_ERRORS = Counter(
    "llm_errors_total",
    "Lỗi LLM call",
    ["model", "error_type"],  # error_type: timeout, content_filter, rate_limit, etc.
)

def calculate_cost(model: str, usage) -> float:
    """Tính cost USD từ token usage. Cập nhật theo pricing provider."""
    pricing = {
        "gpt-4o-mini": {"input": 0.15 / 1e6, "output": 0.60 / 1e6},
        "gpt-4o": {"input": 2.50 / 1e6, "output": 10.00 / 1e6},
    }
    p = pricing.get(model, {"input": 0, "output": 0})
    return p["input"] * usage.prompt_tokens + p["output"] * usage.completion_tokens

def call_llm(model: str, messages: list[dict]):
    start = time.time()
    try:
        response = openai_client.chat.completions.create(
            model=model,
            messages=messages,
        )

        LLM_LATENCY.labels(model).observe(time.time() - start)
        LLM_TOKENS.labels(model, "input").inc(response.usage.prompt_tokens)
        LLM_TOKENS.labels(model, "output").inc(response.usage.completion_tokens)
        LLM_COST.labels(model).inc(calculate_cost(model, response.usage))

        return response

    except Exception as e:
        LLM_ERRORS.labels(model, type(e).__name__).inc()
        raise

Từ các Counter này, Grafana tính được:

  • Tokens/phút: rate(llm_tokens_total[5m]) * 60
  • Cost/giờ: rate(llm_cost_usd_total[1h]) * 3600
  • Error rate theo model: rate(llm_errors_total[5m]) / rate(llm_tokens_total[5m])
10

TTFT và TPS cho streaming LLM

Khi dùng streaming (bài 8), response không trả về 1 lần. Latency tổng không nói lên UX — cần đo TTFT và TPS riêng:

from prometheus_client import Histogram
import time

TTFT = Histogram(
    "llm_ttft_seconds",
    "Time To First Token",
    ["model"],
    buckets=[0.1, 0.2, 0.5, 1, 1.5, 2, 3, 5],
)
TPS = Histogram(
    "llm_tokens_per_second",
    "Token generation speed",
    ["model"],
    buckets=[5, 10, 20, 30, 50, 80, 120, 200],
)

async def stream_llm(model: str, messages: list[dict]):
    start = time.time()
    first_token_recorded = False
    first_token_time = None
    token_count = 0

    async for chunk in openai_client.chat.completions.create(
        model=model,
        messages=messages,
        stream=True,
    ):
        delta = chunk.choices[0].delta.content
        if delta:
            if not first_token_recorded:
                first_token_time = time.time()
                TTFT.labels(model).observe(first_token_time - start)
                first_token_recorded = True
            token_count += 1
            yield delta

    # Tính TPS dựa trên thời gian từ first token đến last token
    if first_token_time and token_count > 0:
        generation_duration = time.time() - first_token_time
        if generation_duration > 0:
            TPS.labels(model).observe(token_count / generation_duration)

TTFT thường quan trọng hơn TPS với chat interface. TPS quan trọng hơn khi output dài (code generation, document summarization). Nên monitor cả hai để đánh giá đúng bottleneck.

11

System metrics — GPU, CPU, RAM

Application metrics cần kết hợp với system metrics để hiểu đầy đủ. Các tool collect system metrics:

CPU, RAM, disk, network

node_exporter (Prometheus ecosystem) chạy như sidecar trên host, tự động expose /metrics với hàng trăm metric OS-level. Không cần viết code thêm. Chạy bằng Docker:

docker run -d \
  --net="host" \
  --pid="host" \
  -v "/:/host:ro,rslave" \
  prom/node-exporter:latest \
  --path.rootfs=/host

Container metrics

cAdvisor collect CPU, RAM, network I/O từng container Docker. Tích hợp sẵn khi chạy Kubernetes — kubelet expose cAdvisor metrics ở /metrics/cadvisor.

GPU metrics

Khi chạy model local (không qua API), GPU utilization và VRAM là metric quan trọng:

import pynvml
from prometheus_client import Gauge

GPU_UTIL = Gauge("gpu_utilization_percent", "GPU utilization", ["device"])
GPU_MEMORY_USED = Gauge("gpu_memory_used_mb", "GPU memory used (MB)", ["device"])
GPU_MEMORY_TOTAL = Gauge("gpu_memory_total_mb", "GPU memory total (MB)", ["device"])

def collect_gpu_metrics():
    pynvml.nvmlInit()
    device_count = pynvml.nvmlDeviceGetCount()
    for i in range(device_count):
        handle = pynvml.nvmlDeviceGetHandleByIndex(i)
        util = pynvml.nvmlDeviceGetUtilizationRates(handle)
        mem = pynvml.nvmlDeviceGetMemoryInfo(handle)

        GPU_UTIL.labels(device=str(i)).set(util.gpu)
        GPU_MEMORY_USED.labels(device=str(i)).set(mem.used / 1024 / 1024)
        GPU_MEMORY_TOTAL.labels(device=str(i)).set(mem.total / 1024 / 1024)

Package: pip install pynvml. Cần NVIDIA driver cài sẵn. Gọi collect_gpu_metrics() theo interval (vd mỗi 15 giây trong background thread) hoặc trong Prometheus custom collector.

Với cluster NVIDIA trong production, NVIDIA DCGM Exporter (Data Center GPU Manager) là lựa chọn enterprise — expose hàng chục GPU metric bao gồm temperature, power, PCIe bandwidth.

12

Histogram vs Summary

Prometheus có 2 metric type tính percentile: Histogram và Summary. Cần hiểu sự khác biệt để chọn đúng.

Tiêu chí Histogram Summary
Tính percentile ở đâu Prometheus server (query time) Client side (tại app)
Aggregate nhiều instance Được — cộng bucket counts Không — percentile không cộng được
Buckets Cần định nghĩa trước Không cần
Percentile chính xác Xấp xỉ (phụ thuộc bucket) Chính xác hơn nhưng tốn CPU client
Khuyến nghị Hầu hết trường hợp Không khuyến nghị cho cluster

Prometheus documentation khuyến nghị dùng Histogram cho latency. Summary không aggregate được giữa các instance — nếu có 3 replica, bạn không thể tính P99 chung cho cả cluster từ Summary. Histogram thì tính được vì chỉ cần cộng bucket counts từ 3 instance.

13

SLI, SLO, SLA và error budget

Metrics cần có target để có ý nghĩa. Google SRE book (2016) định nghĩa 3 khái niệm liên quan:

SLI — Service Level Indicator

Metric cụ thể bạn đo. Ví dụ: P99 latency của endpoint /inference, error rate của toàn service, availability (% thời gian service up).

SLO — Service Level Objective

Target nội bộ cho SLI. Ví dụ: "99.9% request hoàn thành trong 2s" hoặc "error rate dưới 0.1% tính trên rolling 30 ngày". SLO là cam kết team với nhau, không phải với customer.

SLA — Service Level Agreement

Cam kết chính thức với customer, thường kèm penalty khi vi phạm (refund, credit). SLA thường dễ hơn SLO một chút để có buffer.

Error budget

Phần còn lại giữa 100% và SLO. Nếu SLO là 99.9% availability trong tháng:

  • Error budget = 100% - 99.9% = 0.1%
  • Trong 1 tháng (43,200 phút): 0.1% = 43.2 phút downtime cho phép

Error budget là ngôn ngữ chung giữa product và engineering. Nếu budget còn nhiều, có thể deploy nhanh hơn và chấp nhận rủi ro. Nếu gần hết, ưu tiên stability hơn feature. Bài 49 sẽ trình bày cách setup alert khi burn rate quá cao.

14

AI metrics ngoài latency

AI service có nhiều metric đặc thù mà web service thông thường không có:

Refusal rate

Tỷ lệ request mà LLM trả về finish_reason = "content_filter". Refusal rate cao bất thường có thể do: user đang test jailbreak, prompt system quá restrictive, hoặc provider thay đổi safety policy.

LLM_FINISH_REASON = Counter(
    "llm_finish_reason_total",
    "LLM finish reason distribution",
    ["model", "reason"],  # stop, length, content_filter, tool_calls
)

# Trong call_llm()
LLM_FINISH_REASON.labels(
    model=model,
    reason=response.choices[0].finish_reason,
).inc()

Cache hit rate

Nếu có semantic cache hoặc exact-match cache (bài 50), tỷ lệ hit ảnh hưởng trực tiếp đến cost và latency. Cache hit = không tốn LLM call.

CACHE_HITS = Counter("cache_hits_total", "Cache hits", ["cache_type"])
CACHE_MISSES = Counter("cache_misses_total", "Cache misses", ["cache_type"])

# cache_hit_rate = rate(cache_hits_total[5m]) / (rate(cache_hits_total[5m]) + rate(cache_misses_total[5m]))

Fallback rate

Tỷ lệ request phải chuyển sang model rẻ hơn hoặc fallback strategy (do timeout, rate limit, cost cap). Theo dõi fallback rate giúp đánh giá độ ổn định của primary path.

Truncation rate

Tỷ lệ response bị cắt vì finish_reason = "length". Truncation cao = max_tokens config quá thấp hoặc prompt quá dài. Output bị cắt giữa chừng thường không dùng được.

15

Common pitfalls

Chỉ đo average thay vì percentile

Average che giấu tail latency. Setup dashboard chỉ có average nhưng không có P95/P99 thì bỏ qua toàn bộ nhóm user trải nghiệm tệ nhất.

Bucket histogram không phù hợp

Nếu gọi LLM endpoint với default buckets [0.005, 0.01, ..., 10], và hầu hết call mất 5–30s, 95% observation rơi vào bucket +Inf. histogram_quantile(0.95) trả về giá trị không chính xác. Phải chọn buckets bao phủ đúng range thực tế.

Cardinality cao — memory explode

Mỗi tổ hợp label tạo ra 1 time series riêng trong Prometheus. Nếu dùng user_id làm label và có 100,000 user, Prometheus giữ 100,000 time series cho metric đó — có thể làm server hết RAM. Rule: không dùng giá trị high-cardinality (user ID, request ID, IP) làm label Prometheus. Thay vào đó, đặt chúng vào log.

Counter không persist qua restart

Prometheus Counter reset về 0 khi process restart. Đây là hành vi bình thường — Prometheus có hàm increase()rate() tự xử lý reset. Đừng cố persist Counter vào file hay DB vì không cần thiết và gây phức tạp.

Đo latency ở vị trí sai

Nếu đo latency sau middleware nhưng trước khi giải mã request body, bạn bỏ qua thời gian parse JSON. Nếu đo chỉ trong handler mà không tính middleware authentication, số đo không phản ánh thực tế. Cần xác định rõ điểm bắt đầu và kết thúc đo.

Không exclude health check endpoint

Load balancer probe /health mỗi 5s → 720 request/giờ cho mỗi instance. Nếu tính vào latency distribution, latency trung bình giảm xuống giả tạo vì health check trả về ngay lập tức. Luôn exclude /health, /metrics, /ping khỏi request metrics.

16

Tóm tắt

  • ✅ RED method: Rate (throughput), Errors (tỷ lệ lỗi), Duration (latency) — 3 metric tối thiểu cho bất kỳ service nào
  • ✅ Dùng P50/P95/P99 thay vì average — average che giấu tail latency
  • ✅ AI app cần đo thêm TTFT, TPS, token count, cost per call
  • ✅ Counter chỉ tăng, Gauge tăng/giảm, Histogram phân phối — chọn đúng loại
  • prometheus-fastapi-instrumentator tự động expose HTTP metrics với vài dòng code
  • ✅ Histogram nên dùng thay Summary vì aggregate được giữa nhiều instance
  • ✅ Buckets histogram phải phù hợp với range thực tế của service — LLM cần buckets 0.5s–60s
  • ✅ Không dùng high-cardinality value (user_id, request_id) làm label Prometheus
  • ✅ SLI là metric đo, SLO là target nội bộ, SLA là cam kết với customer. Error budget = 100% - SLO
17

Bài tiếp theo

Bài 47: Data Drift — khi distribution input thay đổi — khi input đến model phân phối khác với lúc train, model có thể degraded mà metrics latency/error không phản ánh.