Mục lục
- Mục tiêu bài học
- Metrics vs Logs — hai công cụ khác mục đích
- RED method — 3 metric cốt lõi
- Latency — tại sao cần percentile
- Các loại latency trong AI app
- Counter, Gauge, Histogram — 3 kiểu metric Prometheus
- Instrument code với prometheus-client Python
- FastAPI middleware với prometheus-fastapi-instrumentator
- AI-specific metrics — LLM call
- TTFT và TPS cho streaming LLM
- System metrics — GPU, CPU, RAM
- Histogram vs Summary
- SLI, SLO, SLA và error budget
- AI metrics ngoài latency
- Common pitfalls
- Tóm tắt
- Bài tiếp theo
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-clientPython để 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
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ế.
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.
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.
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.
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.
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ể.
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)
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])
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.
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.
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.
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.
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.
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() và 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.
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-instrumentatortự độ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
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.
Tài liệu tham khảo
- prometheus-client Python — GitHub
- prometheus-fastapi-instrumentator — GitHub
- Prometheus docs — Histograms and summaries
- Prometheus docs — Metric types
- Google SRE Book — Monitoring Distributed Systems
- Google SRE Book — Service Level Objectives
- Weaveworks — The RED Method (2018)
- NVIDIA DCGM — Data Center GPU Manager
- pynvml — Python bindings for NVML
