Hành Trình Debug Đầy Gian Nan - nói dối e blog

Hành Trình Debug Đầy Gian Nan

Vào những ngày cuối cùng trước kỳ nghỉ lễ 1/5, tôi đã đối mặt với một lỗi nghiêm trọng kéo dài suốt hai ngày trời. Đây là một trong ba lỗi khó nhằn nhất trong sự nghiệp của tôi, đáng để ghi lại như một bài học kinh nghiệm.

Vấn đề được phát hiện vào hai ngày trước lễ, khi nhiều đồng nghiệp đã nghỉ phép. Tôi cũng dự định tận hưởng kỳ nghỉ bên gia đình và con cái. Trong lúc cập nhật kho mã nguồn của dự án game như thường lệ, tôi bất ngờ gặp phải lỗi sụp đổ chương trình. Ban đầu tôi không quá lo lắng vì dự án game được phát triển chủ yếu bằng Lua - ngôn ngữ không yêu cầu biên dịch lại sau mỗi lần cập nhật. Thông thường tôi chỉ build dự án một lần mỗi tuần. Có lẽ đây chỉ là vấn đề do tôi chưa build trong thời gian dài, nên tôi tiến hành build lại. Tuy nhiên lỗi vẫn tồn tại, dù xác suất xảy ra không cao - khoảng 1/3 lần khởi động.

Điều bất thường là lỗi segmentation fault này đã rất lâu rồi tôi chưa gặp. Hệ thống engine của chúng tôi được thiết kế đặc biệt cho thiết bị di động. Để tiện theo dõi hiệu ứng trên điện thoại trong quá trình phát triển, chúng tôi không chạy phiên bản Windows/Mac như các engine khác, mà thay vào đó xây dựng một ứng dụng iOS với nhân engine viết bằng C/C++/Obj-C. Hệ thống tích hợp hệ thống tập tin ảo (virtual file system), trong khi máy phát triển chạy tiến trình fileserver để ánh xạ thư mục cục bộ vào hệ thống tập tin ảo. Nhờ đó mọi thay đổi trên máy phát triển sẽ phản ánh ngay lập tức trên ứng dụng di động.

Lần này tiến trình fileserver trên máy tôi gặp sự cố. Dù tiến trình này không xử lý logic game, chỉ cung cấp mã Lua và tài nguyên thông qua giao thức đơn giản, nhưng cũng có các chức năng phức tạp hơn như gỡ lỗi Lua trên thiết bị di động, biên dịch shader, xử lý tài nguyên hình ảnh, mô hình, hoạt ảnh… Tuy nhiên lỗi xảy ra ngay giai đoạn khởi động, chưa chạm đến các chức năng này.

Điều kỳ lạ là chỉ có máy tôi gặp vấn đề trong khi cả nhóm phát triển không ai gặp phải. Ban đầu tôi cho rằng do tôi dùng mingw64 trong khi các đồng nghiệp khác dùng Visual C++, nhưng khi một đồng nghiệp thử build bằng mingw64 vẫn không tái hiện được lỗi. Dù đã cập nhật hệ điều hành và khởi động lại, vấn đề vẫn tồn tại. Tôi loại bỏ khả năng môi trường phát triển bị lỗi sau khi kiểm chứng qua nhiều commit với git.

Việc thêm các dòng log đơn giản vào mã Lua cũng khiến lỗi biến mất, thậm chí chỉ cần thêm một dòng “local x = 1” không ảnh hưởng gì cũng đủ loại bỏ lỗi. Điều này khiến tôi nhận ra vấn đề liên quan đến thời điểm garbage collection (gc) của Lua, đặc biệt khi chương trình sử dụng nhiều máy ảo Lua chạy song song thông qua thư viện đa luồng ltask.

Sau nhiều phương pháp kiểm tra, tôi phát hiện ra rằng khi giảm số luồng làm việc của fileserver từ 8 xuống còn 1, lỗi sẽ biến mất. Điều này giải thích tại sao đồng nghiệp không gặp vấn đề - máy họ đang chạy một chương trình giám sát hệ thống (cloud shell) chiếm dụng một nhân CPU, làm giảm mức độ song song thực tế.

Một phát hiện quan trọng khác là việc thay đổi cơ chế khóa từ CRITICAL SECTION sang SRWLOCK cũng ảnh hưởng đến lỗi. Khi quay lại phiên bản dùng CRITICAL SECTION, lỗi không còn xuất hiện. Điều này khẳng định đây là lỗi liên quan đến tính đồng thời (concurrency).

Trong những ngày cuối cùng trước kỳ nghỉ, tôi quyết định thay đổi chiến lược tiếp cận. Thay vì dùng các công cụ debug truyền thống, tôi xây dựng một cơ chế “mật ong” (honeypot) để phát hiện vi phạm bộ nhớ. Bằng cách tùy chỉnh hàm cấp phát bộ nhớ của Lua để sử dụng heap độc lập, tôi phát hiện một byte dữ liệu bị thay đổi bất thường trong quá trình thoát chương trình.

Quá trình phân tích kỹ lưỡng dẫn tôi đến một đoạn mã xử lý giao tiếp giữa các máy ảo Lua thông qua std::binary_semaphore. Vấn đề nằm ở việc đồng bộ hóa không đầy đủ giữa tiến trình gửi và nhận dữ liệu, khiến bộ nhớ có thể bị giải phóng quá sớm khi garbage collector của Lua hoạt động. Đặc biệt với cơ chế garbage collection thế hệ mới trong Lua 5.4, các đối tượng userdata có thể bị thu hồi ngay lập tức sau khi ra khỏi phạm vi sử dụng, tạo ra điều kiện cho race condition xảy ra.

Phát hiện này không chỉ giải thích vì sao lỗi bị ảnh hưởng bởi garbage collection, mà còn làm rõ lý do nó chỉ xuất hiện trong điều kiện tải cao. Sau khi chia sẻ phát hiện với đồng nghiệp qua钉钉 (DingTalk), tôi nhận được phản hồi tích cực và có thể yên tâm tận hưởng kỳ nghỉ bên gia đình.

Đêm đó, khi thấy tin nhắn đã được đọc và nhận được một nút “thích”, tôi cảm thấy hài lòng với hành trình debug đầy thử thách nhưng cũng rất bổ ích này.

0%