Observability: Logs, Metrics, và Traces – Đừng để hệ thống chạy \"mù\"
7/19/2026 · 12p đọc
title: "Observability: Logs, Metrics, và Traces – Đừng để hệ thống chạy "mù""
series: "Software Architecture Masterclass"
part: "Phần 3 — Hệ thống Phân tán & Hạ tầng"
order: 28
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags:
- observability
- logs
- metrics
- distributed-tracing
- production-incident
- correlation-id
- sre
Observability: Logs, Metrics, và Traces – Đừng để hệ thống chạy "mù"
3 giờ sáng, alert bắn liên tục: p99 latency của API checkout tăng gấp 8 lần trong 5 phút. On-call engineer mở dashboard, thấy CPU service order-service bình thường, RAM bình thường, connection pool bình thường. Anh ta SSH vào một trong hai mươi pod đang chạy, tail -f log, thấy hàng nghìn dòng log trôi qua mỗi giây — không có gì bất thường nổi bật. Anh ta mở log của payment-service, của inventory-service, của notification-service — mỗi service một tab, mỗi tab hàng nghìn dòng không có timestamp đồng bộ, không có cách nào biết dòng log nào ở service B là hệ quả của request đang gây lỗi ở service A.
Đây không phải là một hệ thống thiếu logging. Ngược lại — hệ thống này log rất nhiều, có khi log quá nhiều. Vấn đề nằm ở chỗ khác: không có cách nào tương quan (correlate) các mảnh dữ liệu rời rạc đó thành một câu chuyện. Log tồn tại, metric tồn tại, nhưng khi một request đi qua 12 service để hoàn tất một giao dịch, không ai trả lời được câu hỏi đơn giản nhất: "request này đi qua đâu, và nó chết ở đâu?"
Đây là thất bại kiến trúc, không phải thất bại vận hành. Observability không phải là thứ bạn "thêm vào sau" khi có thời gian rảnh — nó là một quyết định kiến trúc phải được thiết kế cùng lúc với hệ thống phân tán, giống hệt như bạn thiết kế API contract hay data schema.
The Problem
Trong một monolith, debug một request lỗi là chuyện đơn giản: một process, một log file, một stack trace liền mạch từ đầu đến cuối. Bạn đọc log theo thứ tự thời gian, thấy request vào, thấy nó gọi hàm nào, thấy nó chết ở đâu. Toàn bộ context nằm trong một tiến trình duy nhất.
Khi hệ thống tách thành microservices (xem Bài 12 — Microservices), một request nghiệp vụ đơn lẻ — ví dụ "đặt hàng" — có thể chạm vào 10-20 service khác nhau: API Gateway → Auth Service → Order Service → Inventory Service → Payment Service → Notification Service → nhiều event consumer chạy async qua message queue. Mỗi service có log riêng, chạy trên container/pod riêng, có thể trên node vật lý khác nhau, múi giờ hệ thống khác nhau (dù đã chuẩn hóa UTC thì clock skew vẫn tồn tại).
Khi request đó lỗi, câu hỏi "root cause nằm ở đâu" trở thành bài toán tổ hợp: kỹ sư phải đoán service nào liên quan, mở log của từng service đó, tìm bằng tay các dòng log có khả năng liên quan dựa trên timestamp gần đúng và may mắn. Với hệ thống chạy ở quy mô hàng trăm request/giây, việc "đoán" này là bất khả thi — có hàng nghìn request khác chen vào cùng khung thời gian.
Vấn đề không phải là thiếu dữ liệu. Vấn đề là dữ liệu bị phân mảnh theo service trong khi sự cố lại xảy ra theo request. Kiến trúc phân tán phá vỡ đơn vị quan sát tự nhiên (một tiến trình) mà không cung cấp đơn vị quan sát thay thế (một request xuyên suốt nhiều tiến trình) — trừ khi bạn chủ động xây nó.
The Theory
Observability (khả năng quan sát) hiện đại đứng trên ba trụ cột bổ trợ lẫn nhau, mỗi trụ cột trả lời một loại câu hỏi khác nhau:
1. Logs — sự kiện rời rạc, giàu ngữ cảnh
Log là bản ghi của một sự kiện cụ thể tại một thời điểm cụ thể: "user 4471 đăng nhập thất bại", "payment gateway trả về lỗi timeout", "order 88213 chuyển sang trạng thái CANCELLED". Log trả lời câu hỏi "chuyện gì đã xảy ra, chính xác, tại thời điểm này". Log có độ chi tiết cao nhất trong ba trụ cột nhưng cũng tốn chi phí lưu trữ cao nhất — log ở mức DEBUG cho một hệ thống lưu lượng lớn có thể sinh ra terabyte dữ liệu mỗi ngày.
Log hiện đại phải là structured log (JSON hoặc key-value, không phải chuỗi text tự do) để có thể index, filter, và aggregate được bằng công cụ (Elasticsearch, Loki, CloudWatch Logs Insights...). Log dạng console.log("user login failed") là vô dụng ở quy mô — bạn cần {"event": "login_failed", "user_id": 4471, "reason": "invalid_password", "trace_id": "abc123", "timestamp": "..."}.
2. Metrics — xu hướng số liệu tổng hợp theo thời gian
Metric là số liệu được đo đếm và tổng hợp theo thời gian: request rate, error rate, p50/p95/p99 latency, CPU utilization, queue depth, connection pool saturation. Metric trả lời câu hỏi "hệ thống đang khỏe hay không, và xu hướng ra sao" — không phải "chuyện gì đã xảy ra với request cụ thể này". Metric rẻ hơn log rất nhiều về lưu trữ vì bản chất nó đã được nén thành con số tổng hợp (một counter tăng dần không cần lưu từng sự kiện riêng lẻ).
Mô hình phổ biến nhất là bốn loại: counter (chỉ tăng, ví dụ tổng số request), gauge (lên xuống tự do, ví dụ số connection đang mở), histogram/summary (phân phối giá trị, dùng để tính percentile latency). Metric là công cụ chính để dựng alert và dashboard tổng quan sức khỏe hệ thống.
3. Traces — đường đi của một request qua hệ thống phân tán
Trace là bản ghi hành trình của MỘT request cụ thể khi nó đi qua nhiều service. Mỗi bước xử lý (gọi một service, một truy vấn DB, một lần publish message) là một span, có thời gian bắt đầu/kết thúc riêng. Toàn bộ các span của một request được nối lại bằng một trace ID chung, tạo thành cây span cha-con phản ánh chính xác thứ tự và thời lượng xử lý ở từng chặng.
Trace trả lời câu hỏi mà log và metric riêng lẻ không trả lời được: "trong hành trình của request X, chặng nào chậm, chặng nào lỗi, và toàn bộ context của request đó khi đi qua từng service là gì". Đây chính là câu trả lời cho vấn đề mở đầu bài viết.
Ba trụ cột bổ trợ nhau như thế nào: Metric cho biết CÓ vấn đề (p99 latency tăng đột biến) và ai cần phải xem xét (alert bắn cho on-call). Trace cho biết vấn đề NẰM Ở ĐÂU trong chuỗi service (span nào chiếm phần lớn thời gian, span nào lỗi). Log cho biết TẠI SAO nó xảy ra ở đó (thông điệp lỗi cụ thể, giá trị tham số cụ thể). Không có trụ cột nào một mình đủ để debug sự cố phân tán — bạn cần cả ba, và quan trọng hơn cả là chúng phải liên kết được với nhau bằng một định danh chung.
The Trade-off
Đầu tư observability đầy đủ có cái giá thật, không phải chỉ là "công sức thêm cho có":
Được gì:
- Giảm MTTR (Mean Time To Resolve) từ hàng giờ xuống hàng phút cho sự cố phân tán phức tạp — thay vì đoán mò, kỹ sư tra trace ID và thấy ngay span nào chết.
- Phát hiện sự cố ở giai đoạn "triệu chứng nhẹ" trước khi nó leo thang thành outage toàn phần (nhờ alerting dựa trên xu hướng metric).
- Cho phép capacity planning dựa trên dữ liệu thật thay vì phỏng đoán (biết chính xác service nào là bottleneck khi tải tăng).
- Là bằng chứng khách quan trong postmortem — không còn tranh cãi "team A nói do team B, team B nói do hạ tầng" mà có trace chỉ thẳng vào span cụ thể.
Mất gì — và đây là phần thường bị lờ đi:
- Chi phí lưu trữ và ingest tăng phi tuyến theo lưu lượng. Ở quy mô nhỏ, chi phí này không đáng kể. Ở quy mô lớn (hàng triệu request/ngày), chi phí ingest log + trace có thể vượt cả chi phí compute của chính hệ thống đang được quan sát — đây là lý do các nền tảng observability thương mại (Datadog, New Relic...) tính phí theo dữ liệu ingest và nhiều tổ chức bị "hoá đơn sốc" (bill shock) khi bật full tracing cho production mà không có sampling strategy.
- Chi phí instrument hoá là chi phí kỹ sư thật, trả trước. Thêm trace context vào mọi lời gọi HTTP, mọi message queue producer/consumer, mọi DB call — đây là công việc lặp đi lặp lại trên từng service, dễ bị bỏ sót ở service mới hoặc code cũ chưa migrate, và nó không sinh ra tính năng nào cho người dùng cuối nhìn thấy trực tiếp (khó bán cho product/business khi so ưu tiên với feature mới).
- Độ trễ (overhead) runtime. Ghi log, tạo span, export dữ liệu telemetry đều tốn CPU cycle và có thể tốn I/O — nếu implement cẩu thả (đồng bộ, blocking), instrumentation chính nó có thể trở thành nguồn gây chậm hệ thống mà bạn đang cố quan sát.
- Nhiễu tín hiệu nếu làm không có kỷ luật. Log quá nhiều ở mức DEBUG trong production, alert dựa trên nguyên nhân thay vì triệu chứng, dashboard quá nhiều biểu đồ không ai nhìn — tất cả tạo ra "observability theater": có vẻ đầy đủ nhưng không ai dùng được khi khủng hoảng thật sự xảy ra.
Chi phí thay thế (opportunity cost) của việc KHÔNG đầu tư là debug mù trong sự cố thật — và đó là cái giá trả bằng downtime, bằng uy tín, bằng SLA vi phạm, thường đắt hơn nhiều lần so với chi phí observability tích lũy. Nhưng đây là một sự đánh đổi có thật, không phải "cứ làm càng nhiều observability càng tốt" — mức đầu tư phải tương xứng với độ phức tạp phân tán và mức độ chấp nhận rủi ro downtime của tổ chức.
The Architecture Diagram
Sơ đồ dưới đây minh hoạ một request đặt hàng đi qua bốn service, với trace ID xuyên suốt liên kết span ở từng chặng, đồng thời log và metric ở mỗi service đều gắn cùng trace ID đó để tương quan ngược lại.
sequenceDiagram
participant Client
participant Gateway as API Gateway
participant Order as Order Service
participant Inventory as Inventory Service
participant Payment as Payment Service
participant Collector as Telemetry Collector<br/>(logs+metrics+traces)
Note over Client,Collector: trace_id = "7f3a9c" được sinh tại Gateway, truyền xuống mọi service
Client->>Gateway: POST /orders (request)
activate Gateway
Gateway->>Gateway: sinh trace_id=7f3a9c, span=gateway
Gateway-->>Collector: log{trace_id=7f3a9c, event=request_received}
Gateway-->>Collector: metric{http_requests_total++}
Gateway->>Order: gọi tạo order (header: trace_id=7f3a9c, parent_span=gateway)
activate Order
Order-->>Collector: log{trace_id=7f3a9c, span=order-create}
Order->>Inventory: kiểm tra tồn kho (trace_id=7f3a9c, parent_span=order-create)
activate Inventory
Inventory-->>Collector: log{trace_id=7f3a9c, span=inventory-check, duration=1200ms}
Note right of Inventory: Chậm bất thường — span này<br/>là root cause thật sự
Inventory-->>Order: OK (còn hàng)
deactivate Inventory
Order->>Payment: xử lý thanh toán (trace_id=7f3a9c, parent_span=order-create)
activate Payment
Payment-->>Collector: log{trace_id=7f3a9c, span=payment-charge, status=success}
Payment-->>Order: OK
deactivate Payment
Order-->>Collector: metric{order_processing_duration=1850ms}
Order-->>Gateway: order created
deactivate Order
Gateway-->>Client: 201 Created
deactivate Gateway
Note over Collector: Kỹ sư tra trace_id=7f3a9c<br/>→ thấy ngay span inventory-check<br/>chiếm 1200ms/1850ms tổng thời gian<br/>→ root cause: Inventory Service chậm
Điểm mấu chốt của sơ đồ: trace_id=7f3a9c được sinh ra một lần ở điểm vào hệ thống (Gateway) và truyền xuyên suốt qua header của mọi lời gọi tiếp theo — kể cả khi các service này ghi log và metric hoàn toàn độc lập, tất cả dữ liệu telemetry đều mang cùng một định danh để hệ thống quan sát (Collector) có thể tái dựng lại toàn bộ hành trình và chỉ thẳng vào chặng gây chậm.
Implementation Guardrails
Mọi service phải truyền trace/correlation ID qua toàn bộ lời gọi xuống dưới — không có ngoại lệ. ID này phải được sinh ra (hoặc kế thừa nếu đã có sẵn từ upstream) ngay tại điểm vào hệ thống, đặt vào context của request (ví dụ qua middleware ở tầng framework), và tự động đính kèm vào mọi HTTP header outbound, mọi message queue header, mọi dòng log. Một service "quên" propagate ID này tạo ra một lỗ đen (blind spot) trong trace — toàn bộ nỗ lực observability của các service khác coi như vô nghĩa tại điểm đó.
Alert phải dựa trên triệu chứng (symptom) người dùng cảm nhận được, không phải nguyên nhân (cause) nội bộ. Alert "error rate của API checkout > 1%" hoặc "p99 latency > 2s" phản ánh trực tiếp trải nghiệm người dùng và luôn đáng để đánh thức on-call. Alert "CPU service X > 80%" là một chỉ báo nguyên nhân tiềm năng, không phải bằng chứng có sự cố — CPU cao có thể hoàn toàn bình thường khi hệ thống đang xử lý tải cao mà vẫn phục vụ tốt. Alert dựa trên nguyên nhân sinh ra alert fatigue (báo động giả liên tục khiến kỹ sư bỏ qua alert thật) — nguyên nhân nên nằm trong dashboard điều tra, không nằm trong điều kiện kích hoạt page.
Structured logging là bắt buộc, không phải tuỳ chọn. Log dạng chuỗi text tự do không thể filter, aggregate, hay join với trace một cách đáng tin cậy ở quy mô lớn. Mọi log entry tối thiểu phải có:
timestamp(UTC, định dạng chuẩn),trace_id,service_name,level,event/message, và các trường ngữ cảnh nghiệp vụ liên quan (user_id, order_id...). Không log dữ liệu nhạy cảm (mật khẩu, token, PII không cần thiết) — đây vừa là guardrail bảo mật vừa là guardrail chi phí lưu trữ.Sampling phải được thiết kế có chủ đích, không phải bật 100% tracing rồi vỡ trận ngân sách. Với hệ thống lưu lượng cao, trace 100% mọi request là không khả thi về chi phí. Chiến lược phổ biến: sample tail-based (giữ lại toàn bộ trace của các request lỗi hoặc chậm bất thường, sample ngẫu nhiên phần còn lại theo tỷ lệ thấp) đảm bảo bạn không bỏ lỡ chính xác những trace có giá trị điều tra nhất trong khi vẫn kiểm soát được chi phí.
Architect's Verdict
Observability không phải là công cụ tùy chọn gắn thêm vào cuối dự án khi có ngân sách dư — nó là hệ thần kinh của một hệ phân tán, và hệ thần kinh phải được xây dựng cùng lúc với hệ xương (kiến trúc service) chứ không phải cấy ghép sau khi cơ thể đã hình thành. Đầu tư vào correlation ID, structured logging, và tracing contract phải diễn ra ở sprint đầu tiên khi service thứ hai ra đời, không phải sau sự cố production đầu tiên khiến cả team thức trắng đêm mà vẫn không tìm ra root cause. Chi phí observability là chi phí bạn trả trước, có kế hoạch, kiểm soát được — chi phí debug mù là chi phí bạn trả sau, không có kế hoạch, và luôn luôn đắt hơn.
🔗 Bài viết liên quan
Bài trước: Infrastructure as Code (IaC) · Bài tiếp theo: Disaster Recovery