Vấn Đề IO Không Đồng Bộ Trong Quy Trình Khởi Động Skynet - nói dối e blog

Vấn Đề IO Không Đồng Bộ Trong Quy Trình Khởi Động Skynet

Một số bạn đồng nghiệp đã phản ánh với tôi rằng kể từ khi thư viện IO của Skynet được viết lại, hệ thống không thể khởi động được trên nền tảng Mac OSX. Sau khi kiểm tra kỹ lưỡng, nguyên nhân trực tiếp nằm ở phần triển khai kqueue chưa chính xác. Cần lưu ý rằng thiết kế API của kqueue và epoll có những điểm khác biệt quan trọng: epoll cho phép gộp chung sự kiện đọc/ghi trong khi kqueue yêu cầu tách biệt rõ ràng giữa đọc và ghi. Phiên bản mã nguồn này được viết vội mà chưa qua kiểm thử thực tế, nên tồn tại lỗ hổng từ trước, chỉ đến khi gặp vấn đề khác mới bộc lộ rõ.

Phiên bản thư viện IO mới của tôi được xây dựng theo mô hình thao tác không đồng bộ (asynchronous IO), trong đó bao gồm cả các thao tác kết nối mạng và lắng nghe kết nối. Khi khởi động Skynet, trước tiên cần khởi động dịch vụ master, sau đó các node phân tán sẽ khởi động harbor service để liên kết thông qua master thông qua module socket connect.

Tôi kỳ vọng tất cả các kết nối này (ít nhất là các kết nối chủ đạo) phải được thiết lập hoàn tất trước khi quá trình khởi động hoàn tất. Tuy nhiên, với đặc tính không đồng bộ, việc xác định chính xác thời điểm kết nối hoàn tất trước khi thực hiện các thao tác tiếp theo là rất thách thức. Tôi đã áp dụng một giải pháp tạm thời nhưng chưa hoàn hảo: ban đầu chỉ đăng ký callback để lắng nghe sự kiện kết nối thành công, sau đó mới chuyển sang callback đầy đủ. Hạn chế của cách này là nếu có yêu cầu gửi tin nhắn từ xa xảy ra trước khi module phân tán được khởi tạo, hệ thống sẽ không xử lý được những tin nhắn đó.

Ban đầu tôi cho rằng khi module phân tán chưa khởi tạo xong, node sẽ không nhận được bất cứ tin nhắn nào từ bên ngoài, nên đã thêm một lệnh assert trong hàm callback để kiểm soát. Tuy nhiên, với lỗi trong triển khai kqueue, kết nối không thể thiết lập được và vấn đề này bị phơi bày ra. Đối với phần này mà tôi dự định thiết kế lại trong tương lai, tôi không muốn tăng độ phức tạp bằng cách thêm cơ chế hàng đợi tin nhắn đệm để lưu trữ những tin nhắn đến sớm. Một phương án khả thi hơn là sử dụng chức năng forward tích hợp sẵn của Skynet để chuyển tiếp lại những tin nhắn chưa thể xử lý. Khi gặp lại tin nhắn đã được forward trước đó, hệ thống có thể tạm ngủ chờ kết nối hoàn tất.

Tôi đã dành thời gian xem xét kỹ lưỡng phương án này, nhưng việc làm như vậy sẽ dẫn đến mất thứ tự tin nhắn - điều khiến tôi cảm thấy không hài lòng. Cuối cùng, tôi quyết định đơn giản hóa vấn đề bằng cách thêm một giao diện connect đồng bộ (blocking connect).

Tuy nhiên, ngay cả khi sửa như vậy chương trình vẫn tồn tại vấn đề, bởi thao tác listen cũng được xử lý không đồng bộ. Thực tế, listen không nhất thiết phải xử lý theo kiểu không đồng bộ, nhưng do thiết kế ban đầu của tôi muốn thống nhất cách xử lý tất cả các thao tác IO. Để giải quyết vấn đề an toàn luồng khi xử lý song song, tôi đã thiết kế một cấu trúc dữ liệu nội bộ ánh xạ từ file handle đến socket descriptor, đồng thời chuyển toàn bộ thao tác đến một luồng riêng biệt xử lý. Tuy nhiên, trong giai đoạn bootstrap của Skynet khi các luồng làm việc chưa được khởi động, yêu cầu IO sẽ không được xử lý, dẫn đến thất bại khi kết nối đến master service đang chạy trên cùng tiến trình.

Giải pháp tôi áp dụng là xử lý trực tiếp thao tác listen thông qua API hệ thống trước, sau đó mới gửi socket descriptor đến luồng xử lý socket để liên kết với cấu trúc dữ liệu nội bộ và thực hiện các bước đăng ký sự kiện tiếp theo cho epoll/kqueue.

Khi qua đợt bận rộn trong tháng này, tôi sẽ dành thời gian nghiêm túc nghiên cứu cách tối ưu hóa kiến trúc phân tán cho các máy chủ game Skynet. Tôi cho rằng việc che giấu hoàn toàn sự khác biệt giữa các dịch vụ chạy nội bộ tiến trình và các dịch vụ ngoài tiến trình thông qua kết nối socket là không thực tế. Trong các dự án thực tế, các module xử lý công việc không thể phân bố tùy tiện giữa các máy chủ vật lý khác nhau. Hiệu quả và độ tin cậy của việc truyền tin và chia sẻ dữ liệu nội bộ tiến trình luôn vượt trội so với việc truyền thông giữa các node vật lý khác nhau.

0%