- Trong các hệ thống phân tán hiện đại, cách làm log truyền thống có những giới hạn mang tính cấu trúc khiến nó không truyền tải được sự thật
- Log vẫn được thiết kế dựa trên môi trường máy chủ đơn kiểu năm 2005, nên làm mất ngữ cảnh của những request đi qua nhiều service, database và cache
- Việc chỉ tìm kiếm chuỗi đơn thuần không hiểu được cấu trúc, quan hệ và tương quan, khiến việc tìm nguyên nhân sự cố trở nên khó khăn
- Giải pháp là với mỗi request, ghi lại một ‘Wide Event’ (hoặc Canonical Log Line) duy nhất chứa toàn bộ ngữ cảnh
- Nhờ đó, log được chuyển từ văn bản đơn thuần thành tài sản dữ liệu có thể phân tích
Vấn đề cốt lõi của logging
- Log truyền thống được tạo ra với giả định của thời đại máy chủ monolithic, nên không phản ánh được kiến trúc dịch vụ phân tán hiện đại
- Một request đi qua nhiều service, DB, cache và queue, nhưng log vẫn được ghi theo tiêu chuẩn của một máy chủ đơn
- Trong ví dụ, mỗi request tạo ra 13 dòng log; với 10.000 người dùng đồng thời sẽ có 130.000 dòng mỗi giây, nhưng phần lớn là thông tin vô nghĩa
- Khi xảy ra sự cố, thứ cần thiết là ngữ cảnh (context), nhưng log hiện tại lại thiếu điều đó
Giới hạn của tìm kiếm theo chuỗi
- Khi người dùng báo “không thanh toán được”, dù tìm log bằng email hay user_id thì cũng khó có được kết quả hữu ích vì không có cấu trúc nhất quán
- Cùng một user ID có thể được ghi dưới hàng chục dạng khác nhau như
user-123, user_id=user-123, {"userId":"user-123"}
- Định dạng log khác nhau giữa các service khiến không thể lần theo các sự kiện liên quan
- Vấn đề cốt lõi là log được thiết kế xoay quanh ghi (write), chứ không được tối ưu cho truy vấn (query)
Định nghĩa các khái niệm cốt lõi
- Structured Logging: cách ghi log dưới dạng key-value (JSON) thay vì chuỗi
- Cardinality: số lượng giá trị duy nhất của một field; ví dụ
user_id có độ cardinality rất cao
- Dimensionality: số lượng field trong một sự kiện log; càng nhiều thì khả năng phân tích càng cao
- Wide Event / Canonical Log Line: một sự kiện log đơn giàu ngữ cảnh cho mỗi request
- Phần lớn hệ thống logging hạn chế dữ liệu có cardinality cao vì chi phí, nhưng trên thực tế đây lại là thứ hữu ích nhất cho việc debug
Giới hạn của OpenTelemetry
- OpenTelemetry (OTel) là một bộ giao thức và SDK, chỉ cung cấp tiêu chuẩn cho việc thu thập và truyền dữ liệu
- Tuy nhiên OTel không làm những việc sau
- Không quyết định cần log cái gì
- Không tự động thêm ngữ cảnh nghiệp vụ (ví dụ: gói thuê bao, giá trị giỏ hàng)
- Không thay đổi tư duy logging của lập trình viên
- Dù dùng cùng một thư viện, trải nghiệm debug giữa instrumentation được thêm ngữ cảnh một cách có chủ đích và instrumentation đơn thuần là khác biệt rất lớn
- OTel chỉ là đường ống truyền dẫn (plumbing), còn truyền cái gì qua đó là do lập trình viên quyết định
Cách tiếp cận Wide Event / Canonical Log Line
- Cần rời bỏ kiểu logging tập trung vào “code đang làm gì” để ghi lại “điều gì đã xảy ra với request”
- Với mỗi request, tạo một sự kiện rộng ở cấp độ service
- Có thể bao gồm hơn 50 field như request, người dùng, thanh toán, lỗi, môi trường...
- JSON ví dụ chứa toàn bộ ngữ cảnh cần cho debug như
user_id, subscription_tier, service_version, error_code
- Nhờ vậy, chỉ với một lần tìm kiếm có thể phân tích ngay lập tức những việc như “nguyên nhân thất bại thanh toán của người dùng premium”
Cách truy vấn Wide Event
- Wide Event không được xử lý bằng tìm kiếm văn bản đơn thuần mà bằng truy vấn dữ liệu có cấu trúc
- Có thể debug ở mức độ phân tích thời gian thực dựa trên dữ liệu có cardinality cao và nhiều chiều
- Ví dụ: có thể chạy ngay truy vấn như “thống kê tỷ lệ thất bại thanh toán của người dùng premium trong 1 giờ qua theo từng mã lỗi”
Mẫu triển khai
- Xây dựng sự kiện xuyên suốt toàn bộ vòng đời request, rồi chỉ ghi ra một lần ở cuối
- Trong middleware, khởi tạo các field cơ bản như
request_id, timestamp, method, path
- Trong handler, bổ sung dần thông tin về người dùng, giỏ hàng, thanh toán và lỗi
- Cuối cùng ghi một sự kiện JSON duy nhất bằng
logger.info(event)
Kiểm soát chi phí bằng sampling
- Ghi hơn 50 field cho mỗi request có thể làm chi phí tăng mạnh, nên cần sampling
- Sampling ngẫu nhiên đơn thuần có nguy cơ bỏ sót lỗi
- Đề xuất chiến lược Tail Sampling
- Luôn lưu lỗi (như 500)
- Luôn lưu các request chậm (từ p99 trở lên)
- Luôn lưu người dùng VIP hoặc session có cờ cụ thể
- Phần còn lại chỉ lấy mẫu ngẫu nhiên 1~5%
- Nhờ vậy có thể giảm chi phí mà vẫn giữ lại các sự kiện quan trọng
Làm rõ những hiểu lầm phổ biến
- Structured Logging ≠ Wide Event: chỉ dùng định dạng JSON là chưa đủ, ngữ cảnh mới là cốt lõi
- Dùng OpenTelemetry ≠ có observability đầy đủ: nó chỉ chuẩn hóa việc thu thập, còn ghi gì vẫn là trách nhiệm của lập trình viên
- Không giống Tracing: tracing thể hiện luồng đi giữa các service, còn Wide Event cung cấp ngữ cảnh bên trong service
- Không cần tách biệt “log dùng để debug, metric dùng cho dashboard” — Wide Event đáp ứng được cả hai mục đích
- Quan niệm “dữ liệu cardinality cao thì đắt” đã lỗi thời; các DB hiện đại như ClickHouse, BigQuery có thể xử lý hiệu quả
Hiệu quả của việc áp dụng Wide Event
- Debug được chuyển từ khai quật (archaeology) sang phân tích (analytics)
- Thay vì grep log của 50 service để tìm “lỗi thanh toán của người dùng”,
giờ đây có thể chuyển sang phân tích dựa trên một truy vấn duy nhất như “xem tỷ lệ thất bại thanh toán của người dùng premium theo từng mã lỗi”
- Kết quả là log được chuyển từ một công cụ từng nói dối thành một tài sản dữ liệu nói lên sự thật
1 bình luận
Ý kiến trên Hacker News
Bài viết khá khó đọc và có cảm giác như được AI hỗ trợ. Dù vậy, thông điệp vẫn có giá trị, và có lẽ sẽ tốt hơn nếu ngắn gọn hơn
Gần đây tôi có vài suy nghĩ như sau.
Với chủ đề này thì không thể không nhắc đến Charity Majors. Cô ấy đã truyền bá các khái niệm “wide events” và “observability” hơn 10 năm qua, và xây dựng Honeycomb.io dựa trên triết lý đó.
Hiện nay có nhiều công cụ khác nhau để hiện thực hóa cách làm này. Điều quan trọng là dùng structured logs hoặc traces để ghi lại wide event, rồi dùng các công cụ có trực quan hóa phong phú như chuỗi thời gian, histogram, v.v.
Tôi đồng ý với một phần lập luận của bài, nhưng cách chỉ để lại một wide event duy nhất có một cái bẫy. Nếu ngoại lệ hoặc timeout xảy ra giữa chừng trong request thì sẽ không còn gì được ghi lại cả.
Bạn cũng sẽ bỏ lỡ framework logging mặc định của ngôn ngữ hoặc log từ dependency.
Vì vậy, tốt hơn là dùng nó như một lớp bổ sung phủ lên trên log hiện có. Chỉ cần có ID theo request/session rồi tổng hợp ở nơi như ClickHouse là được
log.error(data)haywide_event.attach(error, data)về bản chất là như nhauPhần trình bày và ví dụ tương tác rất hay. Nhưng rốt cuộc thì vẫn có thể tóm lại là “hãy thêm các tag có cấu trúc vào log”.
Tôi cảm thấy wide log không mang lại lợi ích đủ lớn so với mức độ phức tạp và giảm khả năng đọc mà nó tạo ra.
Chỉ cần
grep "uid=user-123" application.loglà đủ rồi, liệu có cần phải gắn thêm cả phương thức giao hàng của người dùng không?(Nhân tiện, trên trình duyệt Brave Android thì checkbox không hoạt động)
grep '\"uid\": \"user-123\"'. Cũng có thể dùng tùy chọn--contextđể xem các dòng xung quanhTôi từng làm việc trong môi trường sản xuất bán dẫn, với hệ thống có hàng nghìn thành phần tham gia vào message bus. Log tạo ra 300~400MB mỗi giờ, nhưng vẫn quản lý đủ tốt chỉ với grep và các công cụ CLI.
Log chỉ đơn giản là chuỗi sự kiện theo thời gian, còn phân tích chi tiết thì xử lý bằng truy vấn Oracle. Log là công cụ để nắm được quan hệ nhân quả của sự việc
Log nói cho ta biết “khi nào, điều gì đã xảy ra”, còn “tại sao” thì phải tìm trong tổ hợp của code, dữ liệu và sự kiện
Cá nhân tôi thấy các giao diện như ELK stack không thuận tiện cho việc khám phá trực quan. Điều quan trọng là log phải có thể được đọc và lần theo một cách bản năng
Lời khuyên ở cuối bài rằng “hãy log mọi lỗi, ngoại lệ và request chậm” là một ý tưởng nguy hiểm.
Ví dụ, nếu một dependency trở nên chậm đi, lượng log có thể tăng vọt gấp 100 lần.
Trong tình huống sự cố, dịch vụ nên làm ít việc hơn để dễ phục hồi, nhưng việc log bùng nổ lại dễ gây ra sự cố dây chuyền
Lượng log càng nhiều thì tỷ lệ lấy mẫu càng tự động điều chỉnh để hệ thống không bị quá tải
Trong phần mềm hiện đại, rất khó để một log đơn lẻ giải thích hoàn toàn “điều gì đã xảy ra”.
Vì vậy cần có tương quan theo chiều dọc (Vertical correlation) và tương quan theo chiều ngang (Horizontal correlation).
Giữa các tầng trên dưới trong stack phải chia sẻ cùng một giá trị correlation, còn khi giao tiếp giữa các hệ thống thì cần correlation giữa các peer.
Việc thêm các giá trị này vào API hay protocol là khó, nhưng nếu thiết kế sẵn transaction ID thì có thể lần theo toàn bộ
Tôi nghĩ việc đăng ký hẳn một domain riêng chỉ cho một bài viết thì thiếu tính bền vững.
Vì phải trả phí gia hạn hằng năm, nên dùng blog cá nhân hoặc subdomain sẽ tốt hơn.
Ví dụ dạng logging-sucks.boristane.com sẽ hợp lý hơn
Với nhận định “log là di tích của thời kỳ monolithic”, tôi nghĩ log cục bộ vẫn còn giá trị.
Vai trò vốn có của nó là ghi lại cuộc đối thoại trong tiến trình cục bộ, còn nếu muốn hiểu tình hình ở các server khác thì cần transaction tracing.
Chỉ cần xem log ở đúng chỗ cũng có thể lần ra nguyên nhân gốc rễ
Log giàu ngữ cảnh khi kết hợp với engine phân tích còn có thể dùng để cải thiện sản phẩm
Tôi đồng ý với câu “đừng log những gì code đang làm, hãy log những gì đã xảy ra với request”, nhưng tác giả có vẻ còn thiếu kinh nghiệm.
Tôi gọi điều này là “bug parts logging”, và cho rằng cần đưa vào các tín hiệu báo trước như đường đi xử lý, số lần, thời gian, v.v.
Logging khác với metric hay audit. Nếu logging thất bại thì xử lý vẫn phải tiếp tục, nhưng audit mà thất bại thì là vấn đề nghiêm trọng.
Giống như khái niệm “historian” trong hệ thống SCADA, cần phân biệt observables và evaluatives.
Ví dụ, các sự kiện chi tiết từ cảm biến nhiên liệu hữu ích cho chẩn đoán, nhưng lại không cần thiết cho câu hỏi “có đi tới đích được không”.
Cuối cùng, điều quan trọng là phải xác định rõ cần quan sát gì, và cần đánh giá gì
Dù cách lưu trữ, chuyển đổi, truy vấn có khác nhau, nhưng điểm tiêu thụ và cơ chế có thể được thiết kế thống nhất.
Làm vậy sẽ giúp thiết kế hệ thống đơn giản hơn, và log lưu trữ dài hạn cũng có thể được xử lý lại về sau nếu cần