👋 Chào mừng đến với Nocoem — kiến thức Backend, System Design và lập trình. Khám phá ngay

Backend Performance Best Practices - Monitoring and Logging

TL;DR: Giám sát (monitoring) cho bạn biết backend đang "khỏe" hay "ốm" theo thời gian thực qua các con số (metrics); ghi log (logging) cho bạn biết chính xác chuyện gì đã xảy ra khi sự cố xuất hiện. Bộ ba Prometheus (thu thập metrics), Grafana (trực quan hóa) và ELK Stack (quản lý log) là combo phổ biến để làm việc này. Bài này còn chỉ cách ghi log bất đồng bộ (async) để việc log không làm chậm chính request bạn đang phục vụ.

Bạn từng nhận tin nhắn kiểu "app load chậm quá" lúc nửa đêm chưa? Nếu thứ duy nhất bạn có để tra là cảm giác "chắc do database", nghĩa là bạn đang vận hành hệ thống trong bóng tối. Giám sát và ghi log chính là cặp đèn pin giúp bạn nhìn thấy chuyện gì đang xảy ra bên trong backend, thay vì đoán mò rồi sửa bừa.

Giám sát và ghi log là gì

Hai khái niệm này hay bị nhắc chung nhưng phục vụ mục đích khác nhau:

  • Giám sát (monitoring): liên tục thu thập các con số (metrics) như CPU, độ trễ (latency), tỷ lệ lỗi... theo thời gian, để biết hệ thống đang khỏe hay có dấu hiệu bất thường.
  • Ghi log (logging): ghi lại chi tiết từng sự kiện xảy ra trong ứng dụng (request nào, kết quả ra sao, lỗi gì, ai gọi) để truy vết lại chính xác sau này.

Hãy hình dung monitoring như đồng hồ táp lô trên xe hơi: kim tốc độ, đèn báo dầu, đồng hồ nhiệt độ máy cho bạn biết ngay có gì bất thường trong lúc lái. Logging thì giống hộp đen trên máy bay: nó không giúp bạn né tai nạn, nhưng khi tai nạn xảy ra, thanh tra mở hộp đen ra là biết chính xác 30 giây cuối đã có chuyện gì. Backend khỏe mạnh cần cả hai, một thứ báo động sớm, một thứ điều tra sau.

Vì sao backend cần cả hai, thiếu một cũng khổ

Thử hình dung: hệ thống giám sát báo "API /orders đang chậm gấp 5 lần bình thường". Con số đó rất hữu ích, nhưng nó không nói cho bạn biết tại sao. Lúc này log mới là thứ cứu bạn: log cho biết truy vấn SQL nào đang chạy chậm, request nào bị timeout, exception nào vừa bị ném ra. Nếu chỉ giám sát mà không ghi log, bạn biết có cháy nhưng không biết cháy ở phòng nào. Ngược lại, chỉ ghi log mà không giám sát, bạn có hàng triệu dòng log nhưng chẳng ai biết khi nào cần mở ra đọc, vì không có gì báo hiệu bất thường trước.

Cách hoạt động: Prometheus, Grafana và ELK Stack

Prometheus hoạt động theo mô hình "kéo" (pull): thay vì ứng dụng chủ động đẩy số liệu đi, Prometheus tự định kỳ gọi tới một endpoint (thường là /metrics) của ứng dụng để lấy dữ liệu, rồi lưu lại dưới dạng chuỗi thời gian (time series). Grafana đọc dữ liệu đó (và nhiều nguồn khác) để vẽ thành dashboard, biểu đồ trực quan, giúp bạn nhìn ra xu hướng hoặc điểm bất thường bằng mắt thay vì đọc số thô.

ELK Stack lo phần log: Logstash (hoặc Filebeat) thu thập log từ nhiều nguồn, Elasticsearch lưu trữ và đánh chỉ mục để tìm kiếm nhanh, Kibana là giao diện để bạn gõ từ khóa (như một order_id hay mã lỗi) và lọc ra đúng vài dòng log liên quan giữa hàng triệu dòng khác.

Ứng dụng expose /metrics -> Prometheus kéo (scrape) định kỳ -> lưu time series -> Grafana truy vấn và vẽ dashboard.
Ứng dụng ghi log -> Logstash/Filebeat thu thập -> Elasticsearch lưu và đánh chỉ mục -> Kibana tìm kiếm, trực quan hóa.

Ví dụ: expose metrics và ghi log có cấu trúc

Expose metrics cho Prometheus

Với Python/Flask, bạn chỉ cần định nghĩa vài chỉ số rồi mở một endpoint cho Prometheus tới "kéo":

from flask import Flask
from prometheus_client import Counter, Histogram, generate_latest
import time

app = Flask(__name__)

REQUEST_COUNT = Counter(
    "http_requests_total", "Tong so request", ["method", "endpoint", "status"]
)
REQUEST_LATENCY = Histogram(
    "http_request_duration_seconds", "Thoi gian xu ly request", ["endpoint"]
)

@app.route("/orders/<int:order_id>")
def get_order(order_id):
    start = time.time()
    order = fetch_order_from_db(order_id)  # gia lap truy van database
    REQUEST_LATENCY.labels(endpoint="/orders").observe(time.time() - start)
    REQUEST_COUNT.labels(method="GET", endpoint="/orders", status="200").inc()
    return order

@app.route("/metrics")
def metrics():
    return generate_latest()

Không cần bạn tự đẩy dữ liệu đi đâu cả, Prometheus server (cấu hình sẵn địa chỉ ứng dụng) sẽ tự ghé /metrics mỗi vài giây để lấy số liệu mới nhất.

Ghi log có cấu trúc

Thay vì log một câu văn xuôi khó parse, hãy ghi log dạng có cấu trúc (structured logging) để công cụ như Elasticsearch dễ lập chỉ mục và tìm kiếm sau này:

import logging
import json

logger = logging.getLogger("orders")

def log_order_created(order_id, user_id, amount):
    logger.info(json.dumps({
        "event": "order_created",
        "order_id": order_id,
        "user_id": user_id,
        "amount": amount,
    }))
{"event": "order_created", "order_id": 4821, "user_id": 102, "amount": 250000}

Log dạng JSON như trên, Kibana có thể lọc trực tiếp theo trường order_id hoặc event, nhanh hơn nhiều so với việc phải "mò" chuỗi ký tự trong một câu log tự do.

Ghi log bất đồng bộ: đừng để log làm chậm request

Ghi log tưởng vô hại nhưng nếu làm theo kiểu đồng bộ (request phải chờ log ghi xong xuống đĩa hoặc gửi qua mạng mới được xử lý tiếp), nó có thể trở thành nút thắt cổ chai thật sự khi lưu lượng tăng cao. Giải pháp là ghi log bất đồng bộ: request chỉ cần đẩy message vào một hàng đợi (queue) trong bộ nhớ, gần như tức thời, còn việc ghi xuống đĩa do một luồng (thread) khác đảm nhiệm ở chế độ nền.

Hãy hình dung nhân viên phục vụ ở quán ăn: họ viết phiếu order rồi kẹp lên dây chuyền cho bếp, chứ không đứng đó chờ món ăn ra mới quay lại phục vụ bàn tiếp theo. Ghi log bất đồng bộ hoạt động đúng theo nguyên lý đó.

import logging
import logging.handlers
import queue

log_queue = queue.Queue(-1)  # khong gioi han kich thuoc hang doi
queue_handler = logging.handlers.QueueHandler(log_queue)

file_handler = logging.handlers.RotatingFileHandler(
    "app.log", maxBytes=10_000_000, backupCount=5
)
listener = logging.handlers.QueueListener(log_queue, file_handler)
listener.start()  # luong rieng lo viec ghi file, khong chan request chinh

logger = logging.getLogger("orders")
logger.addHandler(queue_handler)
logger.setLevel(logging.INFO)

logger.info("Đơn hàng %s đã được tạo", 4821)

Đây chính là mẫu (pattern) mà tài liệu chính thức của Python gọi là "dealing with handlers that block": luồng xử lý request chỉ tương tác với QueueHandler (rất nhanh, chỉ là đẩy vào hàng đợi), còn QueueListener chạy ở luồng riêng mới thật sự ghi xuống đĩa. Ở phía Java, Log4j2 cũng cung cấp sẵn cơ chế tương tự qua Async Logger, dựa trên cùng ý tưởng producer-consumer này.

Cạm bẫy và lưu ý thực tế

Cẩn thận với: gắn nhãn (label) có độ phân giải quá cao vào metric Prometheus, ví dụ dùng user_id hoặc order_id làm label. Mỗi giá trị label khác nhau tạo ra một chuỗi thời gian (time series) riêng, nếu số lượng giá trị là hàng trăm nghìn, Prometheus có thể ngốn RAM và chậm hẳn đi. Đây là hiện tượng "bùng nổ số chiều" (cardinality explosion) mà tài liệu chính thức của Prometheus khuyến cáo tránh.
  • Log tràn lan: bật mức debug cho mọi thứ khiến log phình to, tìm dòng log hữu ích giữa hàng triệu dòng noise cũng mệt chẳng kém tìm kim đáy bể, lại tốn thêm chi phí lưu trữ ở Elasticsearch.
  • Log dữ liệu nhạy cảm: đừng bao giờ ghi thẳng mật khẩu, token hay số thẻ vào log dạng thô, hãy che (redact) trước khi ghi.
  • Cảnh báo (alert) quá nhạy: đặt ngưỡng cảnh báo sát quá khiến đội vận hành bị "nhiễu cảnh báo" (alert fatigue), lâu dần họ bỏ qua cả cảnh báo thật.
  • Ghi log bất đồng bộ không miễn phí: nếu ứng dụng crash đột ngột, các message còn nằm trong hàng đợi chưa kịp ghi xuống đĩa có thể bị mất. Cần cân nhắc đánh đổi giữa tốc độ và độ tin cậy tuỳ mức độ quan trọng của log đó.

Tóm tắt

  • Giám sát (monitoring) cho bức tranh tổng thể theo thời gian thực bằng số liệu; ghi log (logging) cho chi tiết từng sự kiện cụ thể để truy vết sau này. Backend khỏe mạnh cần cả hai.
  • Prometheus thu thập metrics theo mô hình pull, Grafana trực quan hóa thành dashboard, ELK Stack (Logstash, Elasticsearch, Kibana) lo việc thu thập, lưu trữ và tìm kiếm log.
  • Ghi log dạng có cấu trúc (structured logging, ví dụ JSON) giúp công cụ tìm kiếm log nhanh và chính xác hơn log dạng câu văn tự do.
  • Ghi log bất đồng bộ (dùng hàng đợi và luồng riêng) giúp việc log không chặn đường xử lý request chính.
  • Cẩn thận label có độ phân giải cao trong metrics, log tràn lan, log dữ liệu nhạy cảm, và đánh đổi độ tin cậy khi log bất đồng bộ.

Nguồn tham khảo

- Kai

Thấy bài viết hữu ích?

0 lượt thích

Đăng nhận xét