Skynet Mở Mã Nguồn
Dự án Skynet mã nguồn mở
Hai ngày gần đây là điểm kiểm tra đầu tiên của mốc thứ hai trong dự án của chúng tôi. Hệ thống máy chủ của chúng tôi đang gặp một số vấn đề hiệu năng khi trải qua kiểm tra tải trọng. Rất nhiều khía cạnh vẫn còn dư địa tối ưu hóa đáng kể. Chúng tôi dự định sẽ hoàn thiện toàn bộ chức năng trước, sau đó mới dành thời gian tái cấu trúc những module độc lập có tiềm năng nâng cao hiệu suất.
Trong hai tuần qua, tôi không có nhiệm vụ phát triển trực tuyến nào liên quan đến tiến độ dự án. Điều này cho phép tôi tập trung nghiên cứu các vấn đề hiệu năng. Vài ngày trước, tôi đã viết lại nhiều module nghi ngờ có vấn đề. Những cập nhật này đều được ghi chép chi tiết trong các bài blog gần đây.
Dù chưa có bằng chứng cụ thể, nhưng cảm nhận cá nhân cho thấy khung nền底层 Skynet của chúng tôi đang gây ra chi phí hệ thống đáng kể. Hệ thống này được phát triển bằng Erlang - ngôn ngữ mà tôi chưa có nhiều kinh nghiệm phân tích hiệu năng. Hơn nữa, cơ sở mã nguồn Erlang tương đối đồ sộ khiến tôi khó nắm bắt rõ các điểm hiệu suất then chốt.
Về bản chất, khung nền底层 cần giải quyết vấn đề cơ bản: truyền tải có trật tự các thông điệp từ điểm này đến điểm khác. Mỗi điểm là một tiến trình dịch vụ khái niệm, có thể được đặt tên hoặc do hệ thống tự động phân bổ tên duy nhất. Về mặt chức năng, nó cung cấp một hàng đợi tin nhắn. Chính vì vậy ban đầu tôi từng dự định sử dụng ZeroMQ để phát triển.
Khi nhìn lại, dù sử dụng Erlang hay ZeroMQ đều có phần cồng kềnh. Thực tế, chỉ cần khoảng 2000 dòng mã C là có thể triển khai hiệu quả chức năng cốt lõi này. Trong hai ngày qua, tôi đã hoàn thành lại toàn bộ hệ thống bằng C với hơn 1000 dòng mã. Dù phiên bản này chưa đạt đến mức độ hoàn thiện của khung nền hiện tại, nhưng đã giúp tôi xác định rõ các điểm tiêu hao hiệu năng. Ngay cả khi không sử dụng phiên bản C này trong tương lai, việc dành nửa tuần để phát triển công cụ đối chiếu hiệu năng cũng rất đáng giá.
Tôi đã công khai mã nguồn này trên GitHub với hy vọng mang lại lợi ích cho nhiều người. Từ góc độ cá nhân, nếu có bạn nào muốn sử dụng hệ thống này để phát triển, việc đó sẽ giúp tôi phát hiện lỗi nhanh hơn. Những ai quan tâm có thể theo dõi tiến độ phát triển của tôi tại đây.
Về giao diện API, tôi đã liệt kê trong bài blog trước đó. Trong quá trình triển khai lại, tôi nhận thấy một số chi tiết chưa hợp lý, nhưng khó sửa đổi nên tạm coi đó là di sản lịch sử.
Phiên bản hiện tại chưa hỗ trợ giao tiếp giữa các máy tính, và tôi cũng không dự định tích hợp chức năng này vào tầng cốt lõi. Thay vào đó, tôi sẽ phát triển nó như một dịch vụ bổ trợ trong tương lai.
Hệ thống này sử dụng mô hình đơn tiến trình đa luồng. Mỗi dịch vụ nội bộ được đặt trong thư viện động riêng, với ba giao diện xuất ra là create, init, release để tạo thể hiện dịch vụ. Hàm init nhận chuỗi tham số để khởi tạo thể hiện. Ví dụ, dịch vụ viết bằng Lua (gọi là snlua) có thể nhận tên tệp Lua khởi động khi khởi tạo.
Mỗi dịch vụ đều hoàn toàn bị động, hoạt động theo cơ chế tin nhắn thông qua hàm callback thống nhất được đăng ký với khung nền. Khung nền lấy tin nhắn từ hàng đợi, xác định dịch vụ nhận, tìm hàm callback tương ứng và gọi thực thi. Khi không được xử lý, dịch vụ không tiêu hao tài nguyên CPU nào. Khung nền đảm bảo hai nguyên tắc quan trọng:
Thứ nhất, hàm callback của mỗi dịch vụ không bao giờ bị gọi đồng thời. Thứ hai, thứ tự tin nhắn gửi từ dịch vụ này sang dịch vụ khác luôn được bảo toàn.
Tôi triển khai điều này bằng mô hình đa luồng. Dưới底层, hệ thống có hàng đợi tin nhắn gồm ba phần: địa chỉ nguồn, địa chỉ đích và khối dữ liệu. Khung nền khởi động nhiều luồng cố định, mỗi luồng liên tục lấy tin nhắn từ hàng đợi. Khi xác định được dịch vụ đích, nếu dịch vụ đang hoạt động (bị khóa), tin nhắn sẽ được đưa vào hàng đợi riêng của dịch vụ đó. Ngược lại, hàm callback sẽ được gọi. Sau khi callback hoàn tất, hệ thống kiểm tra và xử lý toàn bộ hàng đợi riêng trước khi giải phóng khóa.
Số luồng nên nhiều hơn một chút so với số nhân CPU để tránh tình trạng đói tài nguyên. (Miễn là dịch vụ không tự gửi tin nhắn liên tục cho chính mình, sẽ không có dịch vụ nào bị đói)
Vì hệ thống hoạt động trong cùng một tiến trình, tôi đã tối ưu hóa việc truyền tin nhắn. Với tin nhắn điểm-điểm hiện tại, yêu cầu người gửi gọi malloc cấp phát bộ nhớ cho dữ liệu tin nhắn, và người nhận (thông qua khung nền) sẽ gọi free để giải phóng sau khi xử lý xong. Điều này giúp tránh việc sao chép dữ liệu không cần thiết.
Ngoài chức năng cốt lõi, chúng tôi còn cần cung cấp một số tiện ích nền tảng:
- Dịch vụ “lỗ đen” (blackhole): Nhận và dọn dẹp bộ nhớ khi không có người nhận tin nhắn.
- Dịch vụ ghi log lỗi: Thay vì dùng printf đơn giản (gây lộn xộn trong mô hình đa luồng), tôi thiết kế dịch vụ độc lập để tuần tự hóa thông tin log, có thể xử lý và phân tích dữ liệu nếu cần.
Khởi động và dừng dịch vụ được tích hợp vào khung nền dưới dạng lệnh skynet. Ban đầu dự định dùng dịch vụ quản lý riêng, nhưng tôi nhận thấy cần biết địa chỉ dịch vụ vừa khởi tạo để tiện thao tác tiếp theo. Dùng quản lý dịch vụ sẽ yêu cầu xây dựng giao thức RPC, điều mà tôi không muốn quy định ở tầng cốt lõi.
Dịch vụ hẹn giờ (timer) cũng được tích hợp vào khung nền. Đặc biệt với timeout=0, hệ thống sẽ bỏ qua hàng đợi timer và đưa trực tiếp vào hàng đợi tin nhắn. Tôi thiết lập độ chính xác 1/1000 giây cho thời gian hệ thống và 1/100 giây cho callback timeout - đủ đáp ứng nhu cầu game dịch vụ.
Đối với nhu cầu MMO, tôi phát triển dịch vụ cổng riêng để xử lý kết nối bên ngoài. Dịch vụ này được triển khai bằng epoll cách đây không lâu, sau khi sửa đổi nhỏ đã đưa vào sử dụng.
Hiện tại mới chỉ hoàn thiện phần đọc kết nối bên ngoài, phần gửi vẫn chưa triển khai. Khi khởi động dịch vụ cổng, hệ thống sẽ lắng nghe cổng theo tham số cấu hình, gán ID duy nhất cho mỗi kết nối mới. ID này được thiết kế tăng dần trong suốt vòng đời hệ thống để tránh xung đột nguy hiểm khi tái sử dụng ngắn hạn. Tôi dùng bảng băm