Debug bài bản: tìm nguyên nhân gốc, hết đoán mò
Bạn sửa một chỗ, lỗi biến mất, hôm sau nó quay lại ở dạng khác. Đó là dấu hiệu bạn đang đoán mò chứ chưa thật sự debug. Bài này chỉ cho bạn một quy trình debug bài bản: tái hiện lỗi ổn định, thu hẹp phạm vi có hệ thống, và xác nhận đúng nguyên nhân gốc trước khi sửa. Làm theo, bạn sẽ mất ít thời gian hơn và ít tạo bug mới hơn.
Vì sao đoán mò lại nguy hiểm
Đoán mò là khi bạn thay đổi code dựa trên cảm giác, chạy lại, thấy hết lỗi rồi commit. Vấn đề là bạn không biết vì sao nó hết. Có thể bạn chỉ che đi triệu chứng, hoặc vô tình đổi một điều kiện timing. Lỗi vẫn còn nguyên nhân gốc và sẽ tái phát trong tình huống khác.
Bản chất của debug không phải là sửa, mà là hiểu. Khi bạn hiểu chính xác dòng dữ liệu nào sai ở bước nào, cách sửa thường trở nên hiển nhiên. Ngược lại, sửa mà không hiểu là gieo nợ kỹ thuật.
Bốn bước của một lần debug tử tế
1. Tái hiện lỗi một cách ổn định
Nếu bạn không tái hiện được lỗi theo ý muốn, mọi thứ sau đó là may rủi. Hãy ghi lại chính xác input, trạng thái, phiên bản, môi trường. Với lỗi chập chờn, tìm điều kiện làm nó xuất hiện thường xuyên hơn: tăng tải, chạy vòng lặp nhiều lần, cố định seed ngẫu nhiên, giảm concurrency xuống 1 để loại yếu tố đua tranh.
2. Thu hẹp phạm vi bằng chia đôi
Đừng đọc từng dòng từ đầu. Hãy hỏi: lỗi nằm ở nửa trước hay nửa sau của luồng? Đặt một điểm kiểm tra ở giữa, xem dữ liệu tại đó đã sai chưa. Nếu đã sai, nguyên nhân nằm phía trước; nếu còn đúng, nằm phía sau. Lặp lại. Cách chia đôi này biến một hàm 500 dòng thành vài bước log là ra.
3. Hình thành và kiểm chứng giả thuyết
Viết ra một câu rõ ràng: “Tôi nghĩ biến X bị null vì hàm Y trả về sớm khi cache miss.” Sau đó thiết kế một phép thử để chứng minh nó đúng hoặc sai, ví dụ log giá trị X ngay trước điểm nghi ngờ. Một giả thuyết tốt là giả thuyết có thể bị bác bỏ.
4. Sửa nguyên nhân gốc rồi xác nhận
Sau khi sửa, hãy tái hiện lại đúng kịch bản ban đầu để chắc lỗi biến mất. Quan trọng không kém: hiểu vì sao bản sửa hiệu quả. Nếu bạn không giải thích được, bạn chưa xong.
Công cụ nào cho tình huống nào
| Tình huống | Công cụ phù hợp |
| Luồng logic phức tạp, cần xem trạng thái từng bước | Debugger đặt breakpoint, xem call stack và biến |
| Lỗi chỉ xảy ra trên production | Log có cấu trúc kèm request id để lần theo |
| Lỗi hiệu năng, chậm bất thường | Profiler đo thời gian và bộ nhớ theo hàm |
| Bug xuất hiện sau một thay đổi nào đó | git bisect để tìm commit gây lỗi |
Ví dụ thực tế
Một API thỉnh thoảng trả về danh sách rỗng dù dữ liệu có thật. Đoán mò sẽ là thêm retry cho xong. Debug bài bản: tái hiện bằng cách gọi liên tục 200 lần, thấy lỗi xuất hiện khoảng 3%. Chia đôi luồng, log ngay sau truy vấn database thì thấy dữ liệu vẫn đủ; log sau bước lọc theo thời gian thì thành rỗng. Giả thuyết: bộ lọc so sánh mốc thời gian theo múi giờ máy chủ, còn dữ liệu lưu theo UTC. Kiểm chứng bằng cách in cả hai mốc, đúng là lệch 7 tiếng vào một số thời điểm trong ngày. Sửa gốc: chuẩn hóa mọi so sánh về UTC. Lỗi biến mất hẳn, không cần retry.
Lỗi thường gặp và cách khắc phục
Sửa nhiều thứ cùng lúc
Khi lỗi hết bạn không biết thay đổi nào có tác dụng. Hãy đổi một biến số mỗi lần.
Bỏ qua thông báo lỗi
Stack trace và message thường chỉ thẳng vào file và dòng. Đọc kỹ từ trên xuống trước khi mở trình duyệt tìm kiếm.
Tin vào giả định thay vì đo
“Chỗ này chắc chắn đúng” là câu nói tốn nhiều giờ nhất. Hãy log ra để xác nhận, đừng tin.
Không lưu lại kịch bản tái hiện
Biến kịch bản tái hiện thành một test tự động. Nó vừa chứng minh bạn sửa đúng, vừa chặn lỗi quay lại.
Checklist debug nhanh
- Tôi tái hiện được lỗi theo ý muốn chưa?
- Tôi đã đọc kỹ thông báo lỗi và stack trace chưa?
- Tôi đang thu hẹp phạm vi bằng chia đôi, không đọc mò?
- Tôi có một giả thuyết cụ thể có thể kiểm chứng?
- Tôi chỉ đổi một biến số mỗi lần thử?
- Tôi giải thích được vì sao bản sửa hiệu quả?
- Tôi đã viết test chặn lỗi tái phát chưa?
Kết luận
Debug bài bản không phải năng khiếu, mà là kỷ luật: tái hiện, thu hẹp, giả thuyết, xác nhận. Lần tới khi gặp bug, hãy dừng phản xạ sửa liều, viết ra một giả thuyết cụ thể trước khi chạm vào code. Đó là bước đầu tiên và nó thay đổi mọi thứ.
Câu hỏi thường gặp
Khi nào nên dùng debugger, khi nào dùng log?
Debugger phù hợp khi bạn tái hiện được lỗi trên máy mình và cần soi trạng thái từng bước. Log phù hợp cho môi trường không gắn debugger được, nhất là production, hoặc lỗi liên quan tới nhiều tiến trình và thời gian.
Gặp bug chập chờn không tái hiện được thì làm sao?
Tăng khả năng xuất hiện bằng cách chạy lặp nhiều lần, tăng tải, cố định yếu tố ngẫu nhiên. Thêm log chi tiết quanh vùng nghi ngờ rồi chờ nó xảy ra tự nhiên và bắt lấy dữ liệu tại thời điểm đó.
git bisect dùng khi nào?
Khi một tính năng trước đây chạy tốt mà giờ hỏng, và bạn có lịch sử commit sạch. git bisect chia đôi lịch sử để tìm ra commit đầu tiên gây lỗi, giúp khoanh vùng thay đổi rất nhanh.
Làm sao biết mình đã tìm đúng nguyên nhân gốc?
Bạn giải thích được toàn bộ chuỗi nhân quả từ nguyên nhân tới triệu chứng, và khi bạn cố ý tạo lại điều kiện đó thì lỗi xuất hiện đúng như dự đoán.
Nguồn tham khảo
- Tài liệu chính thức của Git về lệnh git bisect
- Sách “Debugging” của David J. Agans về nguyên tắc gỡ lỗi