Xây dựng Pipeline Giám sát và Chẩn đoán Tự động cho Hệ thống Microservices với Prometheus và Grafana
Trong môi trường kiến trúc Microservices hiện đại, việc giám sát (Monitoring) chỉ đơn thuần là xem các biểu đồ CPU hay bộ nhớ là chưa đủ để đảm bảo sự ổn định của hệ thống. Chúng ta cần một giải pháp toàn diện kết hợp giữa các chỉ số hiệu suất (Metrics), nhật ký hoạt động (Logs), và truy vết phân tán (Traces) để có cái nhìn rõ ràng về hành vi của ứng dụng. Bài viết này sẽ hướng dẫn bạn xây dựng một pipeline giám sát cụ thể: Tự động phát hiện sự cố khi lỗi HTTP 500 tăng đột biến, sau đó tự động kích hoạt một cảnh báo trên Grafana và cho phép bạn drill-down trực tiếp vào các dòng log chi tiết của lỗi đó mà không cần phải biết trước ID của service hay request. Đây là mô hình kết hợp giữa Prometheus để thu thập metrics và Loki để quản lý logs, được hiển thị thống nhất trên dashboard của Grafana.
Kiến trúc tổng quan và vai trò của từng thành phần
Để đạt được mục tiêu trên, chúng ta sẽ sử dụng bộ công cụ của CNCF. Prometheus đóng vai trò là engine chính để thu thập các metrics từ ứng dụng thông qua endpoint scrape. Khi các metrics này vượt quá ngưỡng an toàn, Prometheus sẽ gửi cảnh báo (Alert) sang Alertmanager, và cuối cùng là đẩy về Grafana. Điểm mấu chốt trong bài viết này là sự liên kết giữa Prometheus và Loki. Thông thường, người làm SysAdmin phải chạy hai hệ thống song song và mất thời gian để tương quan thời gian sự cố giữa hai hệ thống đó. Với cách tiếp cận hiện đại, chúng ta sẽ khai thác cơ chế "Label Propagation" và cấu hình datasource của Grafana để các cảnh báo từ Prometheus tự động trỏ đến các query log tương ứng trong Loki. Điều này giúp giảm thời gian phản ứng (MTTR) xuống còn vài giây thay vì vài phút.
Cấu hình ứng dụng để xuất Metrics và Logs chuẩn
Trước khi đi vào cấu hình phần mềm giám sát, bước quan trọng nhất là chuẩn bị dữ liệu đầu ra từ ứng dụng. Nếu ứng dụng của bạn không xuất ra log có cấu trúc (structured logging) với các trường label quan trọng, thì mọi nỗ lực giám sát sau này sẽ trở nên khó khăn. Chúng ta cần đảm bảo mỗi dòng log đều chứa các thông tin như `timestamp`, `level`, `service_name`, và đặc biệt là `trace_id` hoặc `request_id`. Dưới đây là ví dụ về cấu hình trong một ứng dụng Node.js sử dụng thư viện Winston để định dạng log ra dạng JSON, đồng thời sử dụng thư viện prom-client để export metrics.
const winston = require('winston');
const format = winston.format.combine(
winston.format.timestamp(),
winston.format.json()
);
const logger = winston.createLogger({
level: 'info',
format: format,
defaultMeta: { service: 'api-gateway' },
transports: [
new winston.transports.Console({
format: winston.format.printf(info => {
return JSON.stringify(info);
})
})
]
});
const promClient = require('prom-client');
const register = new promClient.Registry();
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'status', 'route'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.5, 1, 2.5, 5, 10],
});
register.setDefaultRegistry(register);
// Trong middleware của Express
app.use((req, res, next) => {
const end = httpRequestDurationMicroseconds.startTimer({ method: req.method, route: req.path });
const reqId = crypto.randomUUID();
logger.info({ message: 'Request started', request_id: reqId });
res.on('finish', () => {
end({ status: res.statusCode });
logger.info({ message: 'Request finished', request_id: reqId, status_code: res.statusCode });
if (res.statusCode >= 500) {
logger.error({ message: 'Internal server error', request_id: reqId, error: res.body.error || 'Unknown error' });
}
});
next();
});
app.get('/metrics', (req, res) => {
res.set('Content-Type', register.contentType);
res.end(register.metrics());
});
Đoạn code trên minh họa việc gắn thẻ (label) vào metric và đồng bộ hóa thông tin vào log. Lưu ý cách chúng ta tạo `request_id` và truyền nó vào cả metric histogram lẫn dòng log. Khi có lỗi xảy ra, metric sẽ tăng lên (do bucket status 500) và log sẽ ghi lại chi tiết lỗi cùng với `request_id` đó. Sự nhất quán này là nền tảng để Prometheus và Loki nói chuyện với nhau.
Cấu hình Prometheus để thu thập Metrics và gửi Alert
Sau khi ứng dụng đã sẵn sàng, bước tiếp theo là cấu hình Prometheus. Chúng ta cần định nghĩa nguồn dữ liệu (targets) và đặc biệt là cấu hình alert rules. File cấu hình `prometheus.yml` sẽ chỉ định việc scrape endpoint `/metrics` của ứng dụng, đồng thời khai báo thư mục chứa các file rule. Để cảnh báo có tác dụng, chúng ta cần định nghĩa rõ ràng ngưỡng nào là sự cố. Ví dụ, nếu tỉ lệ lỗi 500 trong vòng 5 phút vượt quá 5%, chúng ta sẽ kích hoạt cảnh báo.
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'api-gateway'
static_configs:
- targets: ['localhost:3000']
labels:
service: 'api-gateway'
environment: 'production'
rule_files:
- 'alerts/*.yml'
alerting:
alertmanagers:
- static_configs:
- targets: ['localhost:9093']
Đối với file rule cảnh báo, chúng ta sử dụng file `alerts/api-errors.yml` với định dạng YAML. Ở đây, chúng ta sử dụng hàm `rate()` để tính số lượng lỗi trong một khoảng thời gian và so sánh với ngưỡng cho phép. Quan trọng hơn, trong phần `labels` của alert, chúng ta cần đảm bảo rằng các label từ metric (như `service`, `environment`) được truyền qua để Alertmanager có thể sử dụng cho việc định tuyến cảnh báo.
groups:
- name: api-errors
rules:
- alert: HighErrorRate
expr: |
(
sum(rate(http_request_duration_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))
) > 0.05
for: 2m
labels:
severity: critical
team: backend
annotations:
summary: "Tỷ lệ lỗi HTTP 500 cao"
description: "Tỷ lệ lỗi HTTP 500 của {{ $labels.service }} đang ở mức {{ $value | humanizePercentage }} (ngưỡng: 5%)."
dashboard_url: "http://localhost:3000/d/grafana-logs?var-service={{ $labels.service }}&var-environment={{ $labels.environment }}"
Bạn hãy chú ý đến phần `annotations`. Đây là nơi chứa URL dẫn trực tiếp đến dashboard Grafana. Chúng ta sử dụng các biến template `{{ $labels.service }}` để khi cảnh báo được gửi đi, nó tự động điền giá trị service cụ thể vào URL, giúp bạn click vào link là thấy đúng biểu đồ của service đó.
Tích hợp Loki vào Grafana để Drill-down từ Metrics sang Logs
Bây giờ là phần thú vị nhất: kết nối giữa cảnh báo và log. Chúng ta giả định rằng bạn đã có Loki chạy để thu thập logs từ file stdout của ứng dụng. Trong Grafana, bạn cần thêm datasource là Prometheus và Loki. Tuy nhiên, để có thể nhìn thấy log ngay khi xem cảnh báo, chúng ta cần cấu hình một Dashboard có cơ chế liên động (Linkage) hoặc sử dụng tính năng "Explore" với các biến động. Một cách phổ biến và hiệu quả trong môi trường production là sử dụng biến (Variables) trong Dashboard của Grafana.
Chúng ta sẽ tạo một dashboard mới trong Grafana. Bước đầu tiên là tạo các biến dựa trên label từ Prometheus. Ví dụ, biến `$service` sẽ lấy danh sách các service đang chạy từ query Prometheus. Sau đó, trong panel hiển thị Log, chúng ta sẽ dùng biến này để lọc query trong Loki. Điều này đảm bảo rằng khi bạn chọn một service từ dropdown hoặc khi bạn vào dashboard qua link alert (như cấu hình ở trên), thì panel log sẽ tự động chỉ hiển thị log của service đó.
# Cấu hình Query cho Panel Log trong Grafana (Dữ liệu từ Loki)
{job=~"$service"} | json | __error__=$status_code
Tuy nhiên, để làm điều này tự động hơn, chúng ta có thể tận dụng tính năng của Grafana Alerting để "link" trực tiếp vào view Logs. Khi thiết lập Alert Rule trong Grafana (Grafana Alerting v2), bạn có thể cấu hình "Annotations" để chứa một link sâu vào Explore view với query log cụ thể. Dưới đây là ví dụ về cách viết query trong Loki để tìm lỗi 500 dựa trên request_id nếu bạn đã lưu trace_id trong log, hoặc đơn giản hơn là lọc theo level error và thời gian sự cố.
{service="api-gateway"} | level="error" | status_code="500"
Để tối ưu hóa trải nghiệm, bạn nên sử dụng tính năng "Log Context" của Loki trong Grafana. Khi bạn click vào một dòng log cụ thể, Grafana có thể tự động lấy ra các dòng log xung quanh (before and after) cùng với các metrics tương ứng tại thời điểm đó. Điều này đòi hỏi cấu hình `loki` trong file `promtail.yml` của ứng dụng để gán các label giống nhau vào cả log và metric scrape. Prometheus sẽ scrape các label này và Loki cũng sẽ lưu trữ chúng, tạo ra một "bridge" hoàn hảo.
Xử lý thực tế: Khi cảnh báo đến và quy trình phản hồi
Hãy tưởng tượng một kịch bản: vào lúc 02:00 sáng, server xử lý thanh toán bị lỗi do hết kết nối database. Metric `http_request_duration_seconds_count` với label `status="500"` tăng vọt. Prometheus đánh giá rule `HighErrorRate`, xác nhận sự cố kéo dài hơn 2 phút, và gửi cảnh báo tới Alertmanager. Alertmanager gửi thông báo qua Slack hoặc Email cho team backend, kèm theo link dashboard Grafana đã được cấu hình sẵn với biến `service=payment-service`.
Engineer nhận được thông báo, click vào link. Dashboard mở ra, biểu đồ hiển thị đỉnh lỗi rõ ràng. Nhưng quan trọng hơn, panel "Logs" bên dưới đã tự động lọc và hiển thị các dòng log lỗi 500 của dịch vụ thanh toán. Engineer không cần gõ lệnh, không cần truy cập server SSH để tail file log. Engineer nhìn vào dòng log, thấy thông báo lỗi "Connection refused from database server". Nhờ có `request_id` trong log, họ có thể lọc thêm theo ID đó để xem toàn bộ luồng request (tracing) nếu có tích hợp Jaeger hay Tempo. Quy trình này giúp loại bỏ hoàn toàn công đoạn "tìm kiếm" (hunting) thông tin, chuyển từ việc phản ứng thụ động sang xử lý chủ động ngay lập tức.
Tối ưu hóa hiệu năng và tránh các lỗi thường gặp
Việc xây dựng pipeline giám sát mạnh mẽ cũng đi kèm với thách thức về hiệu năng. Một lỗi phổ biến là cấu hình scrape interval quá thấp (ví dụ 5s) cho tất cả các target, dẫn đến tải CPU cao cho Prometheus và mạng. Bạn nên cân nhắc tăng interval lên 15s hoặc 30s cho các metric ít quan trọng, chỉ giữ 5s cho các chỉ số critical. Ngoài ra, trong Loki, hãy cẩn thận với việc lưu trữ log ở mức DEBUG. Chỉ nên log DEBUG trong môi trường Dev/Test hoặc khi đang debug sự cố cụ thể. Trong Production, nên để mức INFO và WARN, trừ khi có lỗi nghiêm trọng. Việc lưu log quá nhiều sẽ làm tăng chi phí lưu trữ và làm chậm tốc độ query của Loki, khiến trải nghiệm drill-down trở nên ì ạch.
Một điểm lưu ý nữa là về "Cardinality Explosion" trong Prometheus. Đừng bao giờ thêm các label có giá trị động, thay đổi liên tục như `user_id`, `session_id` hoặc `request_body` vào metric. Điều này sẽ làm số lượng series metric tăng theo cấp số nhân, gây sập Prometheus. Chỉ nên dùng các label tĩnh hoặc có số lượng giá trị giới hạn như `service`, `environment`, `region`, `method`, `status_code`. Các thông tin chi tiết về request nên đưa vào Log (Loki) hoặc Trace (Jaeger/Tempo), nơi chúng được xử lý tốt hơn cho việc lưu trữ các trường dữ liệu lớn.
Kết luận
Hệ thống giám sát hiện đại không còn là những bảng dashboard rời rạc mà là một hệ sinh thái liên kết chặt chẽ. Bằng cách kết hợp Prometheus cho Metrics, Loki cho Logs, và sử dụng Grafana làm điểm hội tụ, chúng ta tạo ra một công cụ chẩn đoán mạnh mẽ. Khả năng tự động liên kết cảnh báo với log chi tiết giúp giảm thiểu thời gian mất mát và tăng tốc độ khắc phục sự cố. Hãy bắt đầu bằng việc chuẩn hóa format log của ứng dụng, sau đó cấu hình Prometheus và Loki để chia sẻ các label chung, và cuối cùng là xây dựng các dashboard thông minh trên Grafana. Đó là con đường dẫn đến một hệ thống Microservices ổn định và đáng tin cậy.