Ghi Chú Phát Triển (16) : Timer Và Sự Kiện Không Đồng Bộ
Vài ngày qua, mình giao cho bạn Vương - thành viên mới gia nhập nhóm - phụ trách công việc lưu trữ dữ liệu. Ban đầu bạn ấy dùng cách serialize dữ liệu từ sharedb thành chuỗi văn bản để lưu trữ. Sau khi hoàn thành bước này, công việc tiếp theo là triển khai Redis để quản lý dữ liệu. Vì hệ thống chính của chúng ta xây dựng bằng Lua nên cần một thư viện Redis cho Lua. Mình đã thử dùng thư viện tìm được trên Google nhưng bạn Vương không hài lòng. Trong tích tắc, bạn ấy đã tự viết lại một thư viện mới với dung lượng mã nguồn chỉ bằng 1/3 phiên bản gốc (hiện đang trong quá trình chuẩn bị open source, mình đang đốc thúc việc này).
Vấn đề duy nhất phát sinh là nếu tận dụng trực tiếp thư viện socket hệ thống thì khó tích hợp mượt mà vào toàn bộ khung truyền thông hiện có. Toàn bộ skynet của chúng ta đều tự điều phối qua cơ chế IO không đồng bộ, nếu dịch vụ dữ liệu này tự ý block tiến trình sẽ khiến các dịch vụ khác không nhận được thời gian CPU.
Bạn Tùng (nickname) đang dự định cải tiến skynet để bổ sung hỗ trợ IO không đồng bộ. Khi xem xét các API hiện có hôm nay, mình nhận thấy cả interface timer truyền thống và interface IO không đồng bộ mới đề xuất thực chất đều thuộc cùng một loại cơ chế - đó là các sự kiện không đồng bộ. Người dùng sẽ chuẩn bị yêu cầu, đính kèm một session id. Khi sự kiện xảy ra, skynet sẽ trả về session id để thông báo sự kiện đã hoàn thành.
Trong luồng thực thi logic của người dùng, IO không đồng bộ và RPC có điểm tương đồng: Mặc dù phía底层 dùng cơ chế callback thông qua message loop, nhưng logic tổng thể vẫn giữ được tính liên tục.
Do ảnh hưởng từ kinh nghiệm trước đây, khi đóng gói Timer ban đầu, mình đã áp dụng cách gọi lại (callback) đơn giản trong Lua. Khi suy nghĩ kỹ hơn hôm nay, mình nhận ra hệ thống không nên tồn tại cơ chế callback tường minh. Interface đúng đắn cần thống nhất với IO không đồng bộ:
Mỗi dịch vụ độc lập đều có bộ phân phối gói tin. Khi nhận message từ bên ngoài, chúng sẽ được xử lý song song - mỗi message là một luồng thực thi độc lập, không cản trở lẫn nhau. Đồng thời, dịch vụ có một luồng chính bắt đầu ngay khi khởi động (có thể xem như phản ứng với tin nhắn khởi động). Nhân tiện nói thêm, tin nhắn khởi động cũng đóng vai trò như tín hiệu restart khi hot update.
Cả luồng chính và các hàm phản ứng message đều có thể gọi hàm sleep. Bản chất đây chính là cơ chế timer底层. Khi gọi, luồng thực thi hiện tại sẽ bị scheduler tạm dừng cho đến khi skynet đếm đủ khoảng thời gian chỉ định, sau đó tiếp tục từ điểm dừng.
Giải pháp này che giấu chi tiết callback của timer, ẩn đi tính không đồng bộ. Đồng thời khó xảy ra tình trạng người dùng chủ ý phát sinh nhiều luồng song song, giúp giảm độ phức tạp hệ thống.
Vậy hot update nên thực hiện thế nào?
Mình kỳ vọng khi cập nhật nóng, khi nhận được tín hiệu, hệ thống chỉ thiết lập cờ hiệu. Sau đó tại vị trí thích hợp trong luồng chính, kiểm tra cờ hiệu này để kết thúc luồng một cách lịch sự. Cách này tương tự kỹ thuật xử lý tín hiệu ngắt trong lập trình C, tốt hơn nhiều so với phương pháp thô bạo là kill callback timer đang đăng ký.
P/s: Hai ngày trước gặp phải vấn đề phụ thuộc chéo giữa các dịch vụ. Cần có cơ chế chủ động cảnh báo thứ tự khởi động trong quy trình boot. Có thể thiết kế một service quản lý khởi động hệ thống, nhận tín hiệu khởi động dịch vụ và cung cấp RPC để sau khi một service hoàn tất khởi động, trả về tín hiệu thành công.