Database Observability: Kết nối SQL Query với Application Trace bằng OpenTelemetry

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Khi Slow Log là chưa đủ

Tôi không thể nhớ hết bao nhiêu đêm mình đã phải căng mắt nhìn vào log slow query của PostgreSQL, cố gắng đoán xem hàm cụ thể nào đã kích hoạt một câu lệnh SELECT “đi lạc”. Trong một hệ thống monolith đơn giản, bạn thường có thể đoán dựa trên tên bảng. Nhưng trong môi trường microservices với hơn 50 service và hàng trăm endpoint, việc đoán mò đó sẽ thất bại. Bạn có thể thấy một truy vấn kéo dài 4,8 giây, nhưng bạn sẽ không biết liệu nó được kích hoạt bởi một yêu cầu ‘Thanh toán’ (Checkout) quan trọng hay một tác vụ chạy ngầm ‘Phân tích’ (Analytics) không thiết yếu.

Sau khi quản lý MySQL, PostgreSQL và MongoDB ở quy mô lớn, tôi nhận thấy tất cả chúng đều có chung một hạn chế khó chịu: cơ sở dữ liệu là một “hộp đen” đối với ngữ cảnh của ứng dụng. Các bản log tiêu chuẩn cho bạn biết cái gì đã thực thi và mất bao lâu. Chúng hiếm khi cho bạn biết ai đã bắt đầu hoặc tại sao nó lại xảy ra.

Database observability (khả năng quan sát cơ sở dữ liệu) sẽ thay đổi điều này. Bằng cách kết hợp OpenTelemetry (OTel) để tracing phân tán với SQLCommenter để chèn metadata, cuối cùng chúng ta có thể ghép nối các trace ở cấp độ ứng dụng trực tiếp vào các SQL query gửi đến server của bạn.

Giải pháp: SQLCommenter và Trace Context

Giám sát truyền thống tập trung vào các chỉ số hạ tầng như đột biến CPU hoặc áp lực bộ nhớ. Observability thì khác; nó tập trung vào ngữ cảnh (context). OpenTelemetry theo dõi một request khi nó nhảy qua các service. Thông thường, khả năng hiển thị đó sẽ biến mất ngay khi request chạm đến database driver. Database engine không có khái niệm về trace_id đang tồn tại trong backend Python hoặc Go của bạn.

SQLCommenter là một thư viện mã nguồn mở khắc phục điều này bằng cách bổ sung các comment vào câu lệnh SQL. Các comment này chứa metadata như tên controller, route và OpenTelemetry trace context. Ví dụ, một truy vấn tiêu chuẩn trông như thế này:

SELECT * FROM users WHERE id = 10;

Với SQLCommenter, nó sẽ chuyển đổi thành:

SELECT * FROM users WHERE id = 10 /* traceparent='00-84b54...-01', action='get_user', service='identity-provider' */;

Các database engine sẽ bỏ qua những comment này trong quá trình thực thi, nhưng chúng vẫn ghi lại chúng trong các bản log như log_statement của PostgreSQL. Điều này cho phép bạn lấy một truy vấn chậm từ file log và ngay lập tức tìm thấy trace tương ứng của nó trên dashboard.

Thiết lập bộ công cụ

Trong ví dụ này, chúng ta sẽ sử dụng stack Python với SQLAlchemy và FastAPI. Đây là môi trường phổ biến nơi các vấn đề N+1 query và nút thắt cổ chai hiệu suất thường ẩn náu. Bạn sẽ cần OpenTelemetry SDK và tích hợp SQLCommenter cho ORM của mình.

1. Cài đặt các Dependency của OpenTelemetry

Bắt đầu bằng cách cài đặt các gói OTel cốt lõi cùng với instrumentation cho web framework và database driver của bạn:

bash
pip install opentelemetry-api \
            opentelemetry-sdk \
            opentelemetry-instrumentation-fastapi \
            opentelemetry-instrumentation-sqlalchemy \
            opentelemetry-exporter-otlp

2. Cài đặt SQLCommenter

Google cung cấp các plugin SQLCommenter cho nhiều ngôn ngữ khác nhau. Đối với Python và SQLAlchemy, hãy chạy:

bash
pip install google-cloud-sqlcommenter

Kết nối tích hợp

Việc tích hợp sẽ hoàn tất khi bạn cấu hình SQLAlchemy engine để sử dụng wrapper thực thi của SQLCommenter. Điều này đảm bảo mọi truy vấn do ORM tạo ra đều mang metadata trace cần thiết.

Khởi tạo Tracer

Tôi thích gói phần thiết lập OTel vào một hàm helper. Nó đảm bảo tracer hoạt động trước khi ứng dụng bắt đầu xử lý traffic.

python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

def setup_otel(service_name):
    provider = TracerProvider()
    processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)

# Thiết lập OTel cho order-service
setup_otel("order-service")

Instrumenting SQLAlchemy

Bây giờ, chúng ta yêu cầu SQLAlchemy đính kèm comment vào các truy vấn. Sử dụng plugin SQLCommenter là cách sạch nhất. Nó giúp tránh việc phải sửa đổi thủ công mọi truy vấn trong codebase của bạn.

python
from sqlalchemy import create_engine
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from google.cloud.sqlcommenter.sqlalchemy.executor import BeforeExecuteFactory

DATABASE_URL = "postgresql://user:password@localhost/dbname"
engine = create_engine(DATABASE_URL)

# Cấu hình instrumentation OTel tiêu chuẩn
SQLAlchemyInstrumentor().instrument(engine=engine)

# Gắn SQLCommenter metadata thông qua event listener
from sqlalchemy import event

@event.listens_for(engine, "before_cursor_execute", retval=True)
def add_sql_comment(conn, cursor, statement, parameters, context, executemany):
    # Logic SQLCommenter sẽ thêm /* key='value' */ vào câu lệnh
    # Trong ứng dụng thực tế, hãy dùng factory có sẵn của thư viện để code sạch hơn
    return statement, parameters

Trong các phiên bản hiện đại của các thư viện này, bạn thường có thể sử dụng middleware để tự động bắt tên route. Đối với FastAPI, điều này đảm bảo rằng mọi SQL query đều biết chính xác API endpoint nào đã kích hoạt nó.

Xác minh: Hoàn tất quy trình

Sau khi triển khai, hãy xác minh rằng dữ liệu đang chảy vào hai luồng riêng biệt: Tracing Backend của bạn (như Jaeger hoặc Honeycomb) và Database Log.

1. Kiểm tra Database Log

Kiểm tra cấu hình PostgreSQL hoặc MySQL của bạn. Đảm bảo tính năng ghi log truy vấn đang hoạt động. Trong PostgreSQL, đặt log_min_duration_statement = 0 sẽ ghi lại mọi truy vấn, điều này rất hữu ích cho việc kiểm tra ban đầu.

bash
# Theo dõi log Postgres trong thời gian thực
tail -f /var/log/postgresql/postgresql-15-main.log

Bạn sẽ thấy các dòng log trông như thế này:

LOG:  duration: 15.210 ms  statement: SELECT * FROM orders WHERE status = 'pending' 
/* action='list_orders', traceparent='00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' */

2. Đối chiếu trong Jaeger

Khi bạn phát hiện một truy vấn chậm trong log, hãy sao chép ID traceparent. Dán ID đó vào thanh tìm kiếm của Jaeger. Bạn sẽ ngay lập tức thấy toàn bộ vòng đời của request cụ thể đó. Bạn sẽ thấy người dùng nào đã truy cập endpoint, service nào đã gọi database và chính xác bao nhiêu mili giây đã được dành cho việc thực thi SQL đó.

Điều này đặc biệt hiệu quả để bắt các lỗi “N+1”. Nếu bạn thấy 100 truy vấn nhỏ trong Jaeger, các SQL comment trong database log sẽ xác nhận rằng tất cả chúng đều bắt nguồn từ cùng một vòng lặp trong code của bạn.

Lưu ý khi triển khai Production

Thiết lập này rất mạnh mẽ, nhưng bạn nên triển khai nó một cách cẩn trọng để tránh ảnh hưởng đến hiệu suất hoặc dung lượng lưu trữ:

  • Dung lượng Log: Việc thêm comment vào mọi truy vấn sẽ làm tăng kích thước file log. Đối với các ứng dụng có lưu lượng truy cập cao, hãy cân nhắc chỉ ghi log các truy vấn vượt quá một ngưỡng cụ thể, chẳng hạn như 100ms.
  • Bảo mật: Không bao giờ bao gồm PII (Thông tin nhận dạng cá nhân) trong SQL comment. Chỉ nên dùng ID, tên route và trace context.
  • Sampling (Lấy mẫu): Sử dụng tính năng sampling của OpenTelemetry. Bạn không cần phải trace 100% request để tìm ra các quy luật có ý nghĩa. Tỷ lệ sample 5% thường là đủ để xác định các nút thắt cổ chai lớn nhất.

Granular observability (khả năng quan sát chi tiết) giúp loại bỏ việc đoán mò khi tối ưu hóa database. Thay vì cảm giác mơ hồ rằng “database đang chậm”, bạn có thể xác định chính xác dòng code nào gây ra sự chậm trễ. Nó chuyển đổi mối quan hệ giữa DevOps và Developer từ việc đổ lỗi sang cùng nhau giải quyết vấn đề thực sự.

Share: