Tổng Quan Về Thiết Kế Skynet
Bản tóm tắt thiết kế Skynet
Sau một tháng phát triển, tôi cơ bản đã hoàn thành bản viết bằng C của Skynet. Trong quá trình này, vài module đã được tái cấu trúc nhiều lần nhưng lượng mã thực tế được tinh giản không nhiều: chỉ khoảng hơn 6.000 dòng mã C và hơn 1.000 dòng mã Lua. Dù một số phần mã được viết khá gấp gáp, nhưng tôi vẫn tin tưởng rằng chất lượng tổng thể đáp ứng được các tiêu chuẩn cá nhân. Những lỗi phát sinh là không thể tránh khỏi, tuy nhiên với quy mô dự án nhỏ như vậy, việc kiểm soát và sửa lỗi sẽ thuận lợi hơn nhiều nhờ tính minh bạch của mã nguồn.
Thời gian thực tế dành cho dự án mở trên Github này ngắn hơn một tháng. Phần lớn thời gian còn lại được đầu tư vào việc tương thích ngược với framework Erlang từ 6 tháng trước, di chuyển các đoạn mã không tương thích và viết lại các module dịch vụ từng xây dựng bằng Erlang. Những phần việc này có liên hệ mật thiết với dự án game thực tế của chúng tôi nên không thể công khai mã nguồn. Ngoài ra, việc công bố hàng chục nghìn dòng mã liên quan đi kèm sẽ không mang lại nhiều giá trị tích cực cho dự án mã nguồn mở này. Các nhà phát triển quan tâm dễ bị lạc lối trong những thiết kế phụ thuộc vào lịch sử với giao diện kém tối ưu.
Sau khi tích hợp thành công toàn bộ mã nguồn legacy của dự án nội bộ, tôi tiếp tục cải tiến thiết kế底层 của Skynet. Dù đảm bảo tính an toàn trong quá trình di chuyển hệ thống, tôi vẫn tối ưu hóa kiến trúc để giảm thiểu gánh nặng lịch sử. Những thay đổi này không đơn giản nhưng mang lại nhiều giá trị thiết thực, là kết tinh của quá trình suy nghĩ kỹ lưỡng trong thời gian gần đây. Bài viết hôm nay sẽ ghi lại chi tiết các quyết định thiết kế chính của phiên bản cuối cùng để tham khảo trong tương lai.
Skynet giải quyết vấn đề cốt lõi nào? Tôi mong muốn máy chủ game của chúng tôi (mặc dù Skynet không giới hạn chỉ dùng cho máy chủ game) có thể tận dụng tối đa lợi thế đa nhân, phân chia các nghiệp vụ độc lập vào các môi trường xử lý riêng biệt song song làm việc. Ban đầu tôi dự định sử dụng tiến trình hệ điều hành, nhưng sau đó nhận ra rằng khi đã chọn ngôn ngữ nhúng như Lua thì tính độc lập của tiến trình hệ điều hành trở nên không cần thiết. Lua State đã tạo ra môi trường sandbox hoàn hảo, cách ly các môi trường thực thi khác nhau. Mô hình đa luồng cho phép chia sẻ trạng thái và trao đổi dữ liệu hiệu quả hơn, đồng thời những nhược điểm như deadlock luồng, vấn đề lập lịch luồng… có thể được hạn chế qua việc tinh giản thiết kế底层. Điều này lý giải tại sao Skynet chỉ cần dưới 3.000 dòng mã C cho lớp nhân cốt lõi - bất kỳ lập trình viên C nào có kinh nghiệm đều có thể nắm bắt và bảo trì trong thời gian ngắn.
Về cơ bản, Skynet giải quyết duy nhất một vấn đề: Khởi động một module C tuân thủ chuẩn từ thư viện động (tệp .so), gán cho nó một định danh số duy nhất (dù module có dừng lại cũng không bị tái sử dụng). Module này được gọi là dịch vụ (Service), có thể tự do gửi tin nhắn qua lại. Mỗi module đăng ký một hàm callback với framework Skynet để nhận tin nhắn. Mỗi dịch vụ hoạt động theo cơ chế driven by message packets - khi không có gói tin đến sẽ tự động treo để tiết kiệm tài nguyên CPU. Nếu cần logic tự chủ, có thể sử dụng cơ chế timeout message của Skynet để định kỳ kích hoạt.
Skynet cung cấp dịch vụ đặt tên, cho phép gán tên dễ đọc cho các dịch vụ thay vì dùng ID. ID liên quan đến thời điểm chạy, không đảm bảo tính nhất quán khi khởi động lại dịch vụ, nhưng tên gọi thì có thể.
Skynet không giải quyết gì? Tin nhắn trong Skynet là đơn hướng, được truyền dưới dạng gói dữ liệu rời rạc. Nó không định nghĩa khái niệm kết nối TCP, cũng không quy định giao thức RPC hay phương thức mã hóa dữ liệu. Không tồn tại API chuẩn hóa cấu trúc dữ liệu phức tạp.
Về nguyên tắc, Skynet chủ trương mọi dịch vụ hợp tác trong cùng một tiến trình hệ điều hành. Do đó lớp nhân không hỗ trợ cơ chế giao tiếp giữa các máy tính, cũng không có cơ chế phục hồi khi dịch vụ gặp sự cố. Giống như chương trình đơn luồng thông thường, lập trình viên phải chịu trách nhiệm với lỗi trong mã của mình. Khi chương trình lỗi, bạn không nên che giấu vấn đề. Đây không phải là việc của lớp nhân - khác với hệ điều hành nơi lỗi tiến trình người dùng không ảnh hưởng tiến trình khác. Trong Skynet, tất cả dịch vụ cùng hướng đến mục tiêu phục vụ khách hàng game, một lỗi ở bất kỳ khâu nào cũng có thể gây thảm họa nên không cần cách ly vấn đề.
Tuy nhiên, điều này không có nghĩa hệ thống xây dựng trên Skynet thiếu tính ổn định. Những vấn đề này có thể giải quyết ở tầng cao hơn. Ví dụ, sử dụng sandbox Lua có thể cách ly phần lớn lỗi logic tầng trên.
Tóm lại, Skynet chỉ đảm nhận việc gửi một gói tin từ dịch vụ này sang dịch vụ khác cùng tiến trình, kích hoạt hàm callback tương ứng. Nó đảm bảo tính thread-safe cho quá trình khởi tạo module và mỗi lần gọi callback độc lập. Nhà phát triển dịch vụ không cần quan tâm đến môi trường đa luồng, chỉ tập trung xử lý các gói tin nhận được.
Với những ai quen với Erlang sẽ nhận ra đây chính là mô hình Actor. Chỉ khác là tôi chọn Lua - ngôn ngữ quen thuộc hơn. Qua kiểm tra mã nguồn Skynet có thể thấy Lua không phải bắt buộc - bạn hoàn toàn có thể viết module bằng C hoặc thay thế bằng Python và các ngôn ngữ động khác. Việc tích hợp song song Lua và Python cũng khả thi, tận dụng được các dịch vụ nền tảng đã xây dựng.
Tại sao chọn Lua? Lý do hàng đầu là sở thích cá nhân. Lua là một trong những ngôn ngữ tôi thành thạo và đánh giá cao. Nó dễ dàng tích hợp với C, có hiệu năng chạy tốt, thậm chí có thể tăng tốc bằng LuaJIT khi cần thiết. Thư viện phụ trợ tối giản giúp hệ thống với hàng trăm sandbox độc lập vẫn nhẹ nhàng, khởi động/ hủy bỏ dịch vụ nhanh chóng.
Để tối ưu hiệu quả giao tiếp giữa dịch vụ, Skynet áp dụng những thiết kế đặc biệt mang lại hiệu suất vượt trội so với phương án đa tiến trình. Gói tin thường được tạo trong một dịch vụ, Skynet không quan