Một Lỗi Rò Rỉ Bộ Nhớ Khó Nhằn
Câu chuyện bắt đầu từ một vấn đề được phản ánh trên diễn đàn Skynet, đồng thời trong hai ngày qua, dự án đang phát triển cũng gặp hiện tượng nghi ngờ rò rỉ bộ nhớ. Hai sự kiện trùng hợp này khiến tôi quyết định đào sâu điều tra.
Thực tế, việc dò tìm rò rỉ bộ nhớ trong Skynet dễ hơn nhiều so với các dự án thông thường. Nhờ kiến trúc mô-đun phân tán đặc trưng của Skynet, mỗi dịch vụ (service) đều có không gian cấp phát bộ nhớ độc lập, phạm vi chức năng tập trung và vòng đời ngắn hơn cả tiến trình chính. Khi phát hiện vùng nhớ đã cấp phát nhưng không được giải phóng, chúng ta có thể tập trung kiểm tra các mô-đun C - vốn rất ít trong Skynet, giúp thu hẹp phạm vi tìm kiếm đáng kể.
Cốt lõi của công cụ phân tích nằm ở tập tin malloc_hook.c trong thư mục skynet_src. Tôi đã đề xuất đồng nghiệp thực hiện cải tiến: ghi nhật ký (log) cho từng lần cấp phát/giải phóng bộ nhớ, lấy handle của dịch vụ hiện hành làm tên tệp. Dù có tồn tại nguy cơ xung đột đa luồng khi ghi log, nhưng xác suất xảy ra rất thấp. Trong trường hợp đặc biệt khi giải phóng bộ nhớ ở luồng khác, dữ liệu log có thể bị trộn lẫn, nhưng điều này chấp nhận được trong giai đoạn debug tạm thời.
Điểm phức tạp nằm ở vòng lặp vô hạn tiềm ẩn khi mở tệp log lần đầu trong hook - bởi chính thao tác này có thể kích hoạt hàm malloc qua crt. Giải pháp là sử dụng biến TLS (Thread Local Storage) để đánh dấu trạng thái, ngăn chặn việc hook bị gọi lặp lại.
Chỉ cần thêm hơn chục dòng mã, hệ thống ghi log quản lý bộ nhớ đã hoạt động ổn định. Ban đầu tôi cho rằng bộ cấp phát của Lua không có vấn đề, nên đã bypass hàm skynet_lalloc dành riêng cho Lua bằng cách gọi trực tiếp je_malloc. Cách tiếp cận này giúp tập trung vào các mô-đun C, giảm khối lượng dữ liệu phân tích.
Tuy nhiên sau khi phân tích log kỹ lưỡng mà không tìm thấy dấu hiệu rò rỉ, tôi buộc phải nghi ngờ đến các vùng nhớ được cấp phát qua Lua. Hai điểm đáng chú ý: thứ nhất là các hàm cấp phát được lấy bằng lua_getallocf (chỉ phát hiện ở thư viện lpeg, đã xác nhận an toàn), thứ hai chính là các sửa đổi do tôi thực hiện trên Lua.
Để tối ưu hóa việc sử dụng bộ nhớ trong môi trường Skynet với nhiều máy ảo Lua, tôi từng thêm patch cho Lua nhằm cho phép chia sẻ nguyên mẫu hàm (function prototype) giữa các máy ảo. Phiên bản byte code Lua đầu tiên được tải sẽ không bị giải phóng, nhưng các lần tải lại mã giống nhau sẽ tái sử dụng bản sao đã có.
Sau khi rà soát kỹ patch mà không phát hiện lỗi, tôi quyết định mở rộng phạm vi ghi log sang cả bộ cấp phát của Lua. Phân tích dữ liệu mới thu thập được cho thấy tồn tại các khối bộ nhớ 88 byte chưa được giải phóng. Đặc biệt, dù loại dữ liệu của các khối này được ghi nhận là 0 (không xác định), nhưng bản ghi ngay trước đó lại mang loại 9 (LUA_TPROTO), cho thấy rõ ràng có vấn đề với việc giải phóng nguyên mẫu hàm.
Điều bất ngờ là kích thước 88 byte chính xác bằng độ dài của cấu trúc ShareProto mà tôi đã thêm vào patch. Hóa ra lỗi chỉ đơn giản là thiếu một dòng lệnh free. Điều trớ trêu là trên máy tính cá nhân của tôi, dòng mã này vẫn luôn tồn tại! (Patch này được nâng cấp từ phiên bản Lua 5.2 cũ hơn, nơi dòng free đã có sẵn). Cả git diff và git status đều không phát hiện sự khác biệt giữa mã cục bộ và kho lưu trữ.
Sự việc cuối cùng được giải quyết bằng cách clone lại kho lưu trữ và commit dòng mã thiếu. Tuy nhiên nguyên nhân gốc rễ vẫn là ẩn số - có thể do vài tháng trước tôi đã thực hiện lệnh push -f khiến lịch sử commit bị thay đổi.
Bổ sung ngày 21/8:
Khi dọn dẹp kho lưu trữ cục bộ, tôi phát hiện nguyên nhân không phải do Git mà do tôi đang làm việc trên một nhánh chứa quá nhiều nhánh con lộn xộn, khiến dòng mã thiếu này không được đồng bộ lên kho lưu trữ từ rất lâu.