Ghi Chú Phát Triển (28): Tái Cấu Trúc Và Tối Ưu Hóa
Như đã ghi chép trong bài trước, cột mốc thứ 2 của chúng ta đã hoàn thành đúng hạn vào ngày 30/9. Tuy nhiên do chạy đua tiến độ, hệ thống tồn đọng nhiều lỗi và hiệu năng gặp vấn đề nghiêm trọng. Toàn bộ nhóm đều nhận thấy cần phải tái cấu trúc nhiều module. Trong những ngày cuối cùng sửa chữa bug, nhiều bản vá chỉ mang tính tạm thời (vì toàn bộ module đều cần viết lại). Vì vậy, chúng tôi dành trọn 1 tháng để tái cấu trúc mã nguồn, sửa lỗi và đánh giá lại kết quả cuối cùng.
Công việc này đã hoàn thành đúng như kế hoạch. Trên bảng trắng, kế hoạch làm việc được viết cách đây hơn nửa tháng vẫn chưa được xóa. Tôi đã liệt kê 12 điểm cần cải tiến hoặc viết lại, sau đó loại bỏ 3 mục do khối lượng công việc quá lớn. Nhờ sự hợp tác chặt chẽ của toàn nhóm, tiến độ diễn ra rất thuận lợi.
Trước đây tôi từng đề cập hệ thống cũ không thể xử lý trận chiến đồng thời của 80 người chơi. Lúc đó CPU server đạt mức 790%. Dù phần cứng server chúng tôi sử dụng đã cũ (2 CPU Intel Xeon E5310 @ 1.60GHz), việc nâng cấp phần cứng có thể cải thiện tình hình. Tuy nhiên kết quả này hoàn toàn không thể chấp nhận được. Từ thời điểm đó, tôi bắt đầu tối ưu hóa từ tầng framework底层.
Kết quả kiểm tra ngày hôm qua khá khả quan: trên cùng máy chủ, trận chiến của 200 robot chỉ tiêu tốn trung bình 130% CPU, độ trễ gói tin từ client chỉ khoảng 1 giây - hoàn toàn có thể đưa vào sử dụng. Dù vẫn chưa đạt mục tiêu thiết kế (500 người chơi đồng thời mượt mà), nhưng nếu nâng cấp lên 2 CPU Intel Xeon E5-2620 @ 2.00GHz mới, hiệu năng dự kiến tăng ít nhất gấp đôi theo chỉ số benchmark.
Lưu ý: Theo báo cáo tham khảo, hệ thống [Dual CPU] Intel Xeon E5-2620 @ 2.00GHz đạt 16,707 điểm benchmark, trong khi hệ thống hiện tại [Dual CPU] Intel Xeon E5310 @ 1.60GHz chỉ đạt 4,160 điểm. Ngay cả khi tính riêng hiệu năng đơn luồng, hệ thống mới cũng vượt trội hơn 2 lần.
Bốn cải tiến trọng điểm trong tháng này: Hiện chưa có đánh giá chính xác về mức đóng góp của từng cải tiến, nhưng bốn điểm chính bao gồm:
- Cải tiến module AOI (Area of Interest) với giao diện và cách sử dụng thay đổi, yêu cầu tích hợp lại toàn bộ.
- Tối ưu hóa chức năng multicast (xem phần “Multicast” trong tổng quan thiết kế Skynet).
- Trừu tượng hóa và viết lại module tính toán công thức bằng C.
- Chuyển toàn bộ tính toán kỹ năng liên quan đến người chơi từ agent sang map service.
Thay đổi thứ tư là công việc tốn nhiều thời gian nhất, mang lại cả ưu và nhược điểm. Ban đầu, chúng tôi phát hiện điểm nghẽn hiệu năng nằm ở việc đọc/ghi thuộc tính người chơi. Không phải do chi phí khóa dữ liệu, mà do Lua phải gọi hàm C bên ngoài để truy xuất userdata, chi phí cao gấp 4 lần so với table nội bộ. Việc tính toán công thức yêu cầu truy xuất dữ liệu này liên tục.
Tôi đã viết lại module tính toán công thức, qua đó loại bỏ nhu cầu lưu trữ thuộc tính chiến đấu trong cấu trúc dữ liệu phức tạp. Điều này dẫn đến việc chuyển toàn bộ tính toán thuộc tính người chơi từ agent sang map. Đây là sự thay đổi ngược với thiết kế ban đầu, vì tôi luôn mong muốn mỗi dữ liệu chỉ có một điểm ghi duy nhất, các nơi khác chỉ đọc - giúp tránh dùng khóa phức tạp và dễ theo dõi luồng dữ liệu.
Lập luận thuyết phục tôi thay đổi là: hệ thống chiến đấu nên tập trung hoàn toàn trong map, xử lý đồng nhất các tình huống (người chơi vs người chơi, người chơi vs NPC, NPC vs NPC). Trong khi NPC không thể tách thành agent độc lập như người chơi, việc xử lý chiến đấu người chơi trong map là hợp lý. Về mặt dữ liệu, thuộc tính chiến đấu của người chơi nên tách biệt với hành trang và các thuộc tính phi chiến đấu (như tên nhân vật).
Tuy nhiên, thay đổi này cũng có nhược điểm: trước đây agent và map chia sẻ công việc tính toán chiến đấu, cho phép xử lý song song. Các agent không liên quan có thể tính toán hoàn toàn độc lập. Nay toàn bộ xử lý tập trung trong map, mất đi lợi thế song song.
Hiệu năng hiện tại: CPU chỉ đạt 130% (8 nhân, mỗi nhân ~20%), nhưng độ trễ tăng rõ rệt khi vượt 240 người chơi. Điều này cho thấy khả năng xử lý của map đã chạm giới hạn. Theo thiết kế hiện tại, map xử lý chiến đấu theo cơ chế vòng lặp, số lượng chiến đấu tăng khiến độ trễ tăng tuyến tính.
Theo đánh giá của Mike, vẫn còn không gian tối ưu hóa mã nguồn. Ngay cả khi không tối ưu thêm, nâng cấp phần cứng cũng có thể dễ dàng tăng gấp đôi hiệu năng. Với phần cứng hiện tại, máy chủ có thể xử lý 1200 người chơi hoạt động đồng thời. Khi ra mắt năm sau với phần cứng mới, mục tiêu 5000 người chơi đồng thời hoàn toàn khả thi.
Vấn đề LuaJIT: Tôi luôn coi LuaJIT như “tấm séc hiệu năng” có thể sử dụng trước, nhưng vẫn tồn tại trở ngại nhỏ.
Vấn đề lớn nhất trước đây là giới hạn bộ nhớ: LuaJIT trên nền 64-bit chỉ sử dụng được 1GB RAM, dù có tạo nhiều lua state. Điều tra mã nguồn cho thấy LuaJIT dùng bộ quản lý bộ nhớ riêng với “con trỏ giả 32-bit”, yêu cầu vùng nhớ phân bổ phải có 32 bit cao bằng 0.
Hiện mỗi agent tiêu tốn gần 10MB RAM (có thể tối ưu xuống 2MB), 1000 agent dễ vượt 1GB. Tôi đã nghĩ ra cách phá bỏ giới hạn này bằng cách cấp phát trước 32MB cho mỗi lua state (giả định đủ dùng), đảm bảo 32 bit cao nhất đồng nhất. Việc sửa đổi mã nguồn LuaJIT chỉ tốn chi phí nhỏ.
Trong quá trình nghiên cứu, tôi phát hiện vấn đề khác: LuaJIT sử dụng kỹ thuật NaN Trick khiến lightuserdata chỉ có 48 bit hiệu lực (xem lj_obj.h
). Điều này xung đột với cách chúng tôi dùng số nguyên 64-bit làm khóa bảng trong Lua. Việc dùng cách “boxed cdata