Thiết Kế Máy Chủ MMO Dựa Trên Skynet
Gần đây, đối tác Momo đã mang theo một nhà phát triển game đến công ty chúng tôi để tham vấn về các vấn đề gặp phải khi triển khai dự án game MMO sử dụng framework Skynet. Trò chơi MMO của họ đang trong giai đoạn chuẩn bị ra mắt nhưng đã bộc lộ nhiều vấn đề nghiêm trọng trong quá trình thử nghiệm tải. Mặc dù qua phân tích, chúng tôi phát hiện phần mềm thử nghiệm tải có nhiều lỗi lập trình, nhưng đồng thời cũng phơi bày những điểm yếu trong thiết kế kiến trúc máy chủ.
Vấn đề cốt lõi nằm ở cách họ triển khai máy chủ MMO: dù sử dụng Skynet nhưng lại nhồi nhét toàn bộ logic nghiệp vụ vào cùng một dịch vụ Lua duy nhất, tức là mọi thứ đều chạy trong cùng một Lua State. Điều này khiến họ không thể tận dụng được các lợi thế cốt lõi mà Skynet mang lại, dẫn đến áp lực tải hệ thống không thể tránh khỏi.
Trong buổi thảo luận kéo dài một buổi chiều, tôi đã trình bày chi tiết cách thiết kế máy chủ MMO hiệu quả. Dưới đây là ghi chép từ buổi trao đổi:
Nguyên tắc phân tách nghiệp vụ
Tôi cho rằng tiêu chí phân tách nghiệp vụ nên dựa trên tần suất tương tác dữ liệu. Những module có dữ liệu độc lập nên được tách thành các dịch vụ riêng biệt. Ngược lại, các chức năng có tương tác dữ liệu mật thiết thì không nên chia tách. Mục tiêu là tránh tập trung xử lý dữ liệu nóng (hot data) tại một dịch vụ duy nhất, trong khi các dữ liệu lạnh (cold data) hoặc không yêu cầu phản hồi tức thì có thể được xử lý tập trung.
Ví dụ cụ thể:
- Quản lý hành trang người chơi: Vì chỉ liên quan trực tiếp đến nhân vật, logic xử lý hành trang nên được tích hợp vào dịch vụ Agent của người chơi. Ngay cả khi thao tác hành trang ảnh hưởng đến người chơi khác hoặc môi trường, Agent cũng nên đảm nhiệm vai trò trung gian giao tiếp với các dịch vụ bên ngoài.
- Hệ thống giao dịch: Các chức năng như chợ đấu giá hay quầy hàng người chơi nên được tách thành các dịch vụ giao dịch độc lập, không nên tích hợp chung với dịch vụ cảnh (scene). Dù giao diện người dùng có thể được truy cập qua cảnh, nhưng về bản chất đây là hệ thống riêng biệt. Các lệnh giao dịch từ client nên được Agent chuyển tiếp đến dịch vụ chuyên dụng.
- Chat hệ thống: Dù chức năng chat có thể ảnh hưởng đến trạng thái người chơi (như tiêu hao năng lượng), nhưng hoàn toàn có thể tách riêng thành dịch vụ độc lập. Hệ thống đội nhóm (party) cũng có thể được thiết kế như một kênh chat đặc biệt, giúp tách biệt logic tổ đội khỏi dịch vụ cảnh.
- Nhiệm vụ & Sự kiện: Đây là những hệ thống cần được tách biệt rõ ràng do tính biến động cao trong quá trình vận hành. Việc tách riêng không chỉ giúp quản lý dữ liệu hiệu quả (chiếm tỷ trọng lớn trong dữ liệu cá nhân người chơi) mà còn giảm tải cho các dịch vụ khác.
Thiết kế cảnh (scene) và bản sao (instance)
Thông thường, mỗi cảnh nên được xử lý bởi một dịch vụ độc lập. Tuy nhiên, nhóm phát triển đến tham vấn đã mắc sai lầm nghiêm trọng khi tích hợp toàn bộ cảnh vào cùng một dịch vụ. Tôi đề xuất:
- Dữ liệu tương tác giữa người chơi (vị trí, HP, buff…) nên được xử lý tại dịch vụ cảnh để tránh giao tiếp giữa các dịch vụ khi tính toán chiến đấu.
- Thông tin cá nhân như hành trang, nhiệm vụ, sự kiện không nên tải lên dịch vụ cảnh để giảm tải dữ liệu.
Xác thực đăng nhập - Vấn đề thường bị xem nhẹ
Mặc dù có thể thiết kế module xác thực đơn giản bằng cách tích hợp vào mỗi Agent, nhưng cách này tiềm ẩn rủi ro khi có lượng lớn người chơi đăng nhập đồng loạt. Giải pháp tối ưu:
- Tạo dịch vụ xác thực không trạng thái (stateless) để có thể mở nhiều instance song song, tận dụng tối đa CPU.
- Thiết lập “bể Agent” (agent pool) với hàng trăm đến hàng nghìn Agent được tạo sẵn, tránh việc tạo mới theo yêu cầu gây nghẽn cổ chai.
Đồng bộ trạng thái - Thách thức trên mạng di động
Đối với game theo lượt, việc đồng bộ trạng thái không cần độ trễ thấp tuyệt đối. Một số nguyên tắc cần tuân thủ:
- Giới hạn số lượng người chơi được đồng bộ trạng thái (ví dụ: 20/50/100 người) để tối ưu băng thông.
- Cho phép client chủ động yêu cầu cập nhật trạng thái. Nếu không có thay đổi, server không phản hồi để tránh truyền dữ liệu thừa. Cơ chế này tự động điều chỉnh tần suất đồng bộ theo khả năng xử lý của client và mạng.
Thiết kế dịch vụ dữ liệu
Dịch vụ dữ liệu nên được tách biệt để giao tiếp với cơ sở dữ liệu. Tuy nhiên:
- Tránh tải toàn bộ dữ liệu người chơi (trang bị, thuộc tính, nhiệm vụ…) cùng lúc. Thay vào đó, chia dữ liệu thành các phần nhỏ, mỗi dịch vụ chỉ yêu cầu phần dữ liệu liên quan.
- Giải pháp này giúp giảm đáng kể lượng dữ liệu trao đổi giữa các dịch vụ.
Công cụ chia sẻ dữ liệu giữa Lua State
Một vấn đề quan trọng trong Skynet là trao đổi dữ liệu giữa các Lua State. Cách thông thường là serialize/unserialize bảng dữ liệu, nhưng tôi đã phát triển một thư viện tối ưu hóa quá trình này:
Thư viện TablePointer ():
- Cho phép đọc trực tiếp bảng dữ liệu từ Lua State khác thông qua con trỏ C (lightuserdata).
- Hàm
tablepointer.topointer
lấy con trỏ bảng, sau đó có thể truyền cho dịch vụ khác. - Hàm
tablepointer.pairs
duyệt bảng qua con trỏ với cơ chế tối ưu hóa, trả về 3 giá trị (số nguyên chỉ vị trí mảng/dictionary). - Không hỗ trợ
__pairs
hoặc kiểu dữ liệu function/userdata, nhưng hoàn toàn phù hợp với các cấu trúc dữ liệu đơn giản.
Ứng dụng thực tế:
- Trong dịch vụ cảnh, có thể định kỳ gửi dữ liệu người chơi đến dịch vụ dữ liệu thông qua con trỏ bảng, tránh serialize tốn kém.
- Kết hợp với kỹ thuật “commit” (tương tự sharemap), sử dụng metatable để ghi nhận thay đổi, sau đó hợp nhất dữ liệu khi cần gửi.
Kết luận
Thiết kế hệ thống MMO hiệu quả trên Skynet đòi hỏi sự cân bằng giữa phân tách dịch vụ hợp lý và tối ưu hóa giao tiếp giữa các thành phần. Việc tận dụng các công cụ như TablePointer không chỉ giúp giảm độ trễ mà còn mở ra