Thiết Kế Lại Mô-Đun Harbor Của Skynet - nói dối e blog

Thiết Kế Lại Mô-Đun Harbor Của Skynet

Skynet hỗ trợ khởi động nhiều nút mạng, trong đó địa chỉ dịch vụ của mỗi nút đều có tính duy nhất toàn cục. Địa chỉ dịch vụ là số nguyên 32bit, với 8bit cao nhất xác định nút mạng chứa dịch vụ đó. Các dịch vụ trong cùng một tiến trình sẽ chia sẻ chung giá trị 8bit này.

Mỗi nút mạng đều có một dịch vụ đặc biệt gọi là harbor (cảng biển). Khi nhận thấy 8bit cao của địa chỉ đích không trùng với nút hiện tại, các gói tin sẽ được chuyển đến harbor. Dịch vụ này sẽ đảm nhận việc truyền tải qua kết nối TCP đến harbor của nút đích.

Vậy các harbor của các nút Skynet khác nhau làm thế nào để thiết lập kết nối mạng với nhau? Vai trò trung tâm trong quá trình này thuộc về dịch vụ master. Dịch vụ master có thể tồn tại như một tiến trình độc lập, hoặc được tích hợp vào một trong các nút Skynet (cấu hình mặc định).

Master sẽ lắng nghe một cổng mạng được chỉ định trong file cấu hình (thông qua tham số standalone). Các nút Skynet sẽ kết nối đến master dựa trên thông tin được cấu hình trong tham số master. Master sau đó sẽ điều phối việc thiết lập kết nối giữa các harbor với nhau.

Trong một mạng Skynet gồm 5 nút, cấu trúc mạng sẽ như sau: !network.png Trong sơ đồ, dịch vụ master (màu xanh) kết nối với tất cả các harbor, đồng thời các harbor cũng kết nối chéo với nhau. Đây chính là giải pháp phân tán ban đầu của Skynet, được ghi lại chi tiết trong một bài blog cách đây 2 năm.

Qua thời gian phát triển, từ giai đoạn hạ tầng còn sơ khai đến nay đã hoàn thiện đáng kể. Phần mã nguồn này đã trải qua nhiều lần cải tiến, tuy nhiên mỗi lần muốn thay đổi lớn đều phải cân nhắc kỹ lưỡng để tránh rủi ro.

Gần đây, một dự án mới của chúng tôi gặp phải vấn đề về kết nối mạng khi triển khai trên máy ảo với chất lượng mạng không ổn định. Sự cố này phơi bày những điểm yếu trong quy trình thiết lập kết nối ban đầu của Skynet. Vì vậy, tôi đã quyết định tái thiết kế hoàn toàn mô-đun này trong cuối tuần vừa qua.

Trước đây để đơn giản hóa thiết kế, mỗi cặp máy tính sẽ thiết lập 2 kết nối TCP song song. Mỗi kết nối chỉ truyền dữ liệu một chiều theo hướng ai khởi tạo kết nối thì sẽ gửi dữ liệu theo hướng đó. Cách tiếp cận này giúp tiết kiệm bước bắt tay nếu các máy đều đáng tin cậy.

Tuy nhiên việc sử dụng 2 kết nối riêng biệt cũng gây ra vấn đề: khi một bên đã gửi dữ liệu thành công, chưa chắc bên kia đã thiết lập được kết nối ngược lại. Điều này đòi hỏi quản lý trạng thái phức tạp hơn. Dù vậy, khi mạng đã thiết lập xong thì hiệu suất không có khác biệt đáng kể.

Ban đầu phần mã này được viết bằng C, trải qua nhiều giai đoạn phát triển từ việc sử dụng zeromq, đến gate service độc lập, rồi chuyển sang socket module hoàn thiện hơn. Trải nghiệm thực tế cho thấy việc thiết lập mạng lưới kết nối giữa nhiều máy là một bài toán phức tạp, dù bản thân việc này không yêu cầu hiệu suất cao bằng việc chuyển tiếp tin nhắn sau khi mạng đã thiết lập.

Vì vậy, việc tách riêng phần logic thiết lập mạng và triển khai bằng Lua là một quyết định hợp lý. Trong phiên bản mới, tôi đã thiết kế lại hoàn toàn mô-đun này với những cải tiến sau:

  1. Giảm số lượng kết nối: Thay vì 2 kết nối song song, mỗi cặp nút chỉ cần 1 kết nối TCP song công. Khi một nút mới gia nhập mạng (thông qua master), master sẽ thông báo số lượng nút hiện có, và nút mới sẽ chờ kết nối từ tất cả các nút cũ. Sau khi thiết lập xong, cổng lắng nghe sẽ được đóng lại.

  2. Tự động kết nối ngược: Khi có nút mới gia nhập, các nút đã hoạt động sẽ chủ động kết nối đến nút mới, thay vì chờ nút mới kết nối đến. Điều này giúp các nút cũ không cần mở cổng chờ kết nối.

Giải pháp này được triển khai trong 2 file cmaster.luacslave.lua, thay thế cho service_master.cservice_harbor.c trước đây. Việc sử dụng Lua mang lại tính linh hoạt cao hơn, đặc biệt trong việc đồng bộ tên dịch vụ toàn cục - chức năng vốn rất phức tạp khi triển khai bằng C.

Mô-đun proxy tin nhắn từ xa đã được tách riêng và đặt trong service_harbor.c phiên bản mới. Mã nguồn này giờ đây gọn nhẹ hơn nhiều, chỉ tập trung quản lý file descriptor TCP mà không cần quan tâm đến quy trình thiết lập kết nối. Nhờ đó, không còn phụ thuộc vào gate service như trước.

Phiên bản gate bằng C đã được tách khỏi lõi hệ thống. Trong các ứng dụng thông thường, phiên bản gate bằng Lua mới viết đã đủ đáp ứng nhu cầu.

Đối với các cụm máy tính phân tán lỏng lẻo, tôi khuyến nghị sử dụng chế độ nút đơn của Skynet, kết nối các nút bằng TCP ở tầng trên và chỉ dùng giao thức RPC đơn giản. Skynet hiện tại đã tích hợp sẵn module cluster để hỗ trợ việc này.

Giải pháp này yêu cầu phân biệt rõ ràng giữa gọi dịch vụ cục bộ và từ xa. Dù hiệu suất gọi từ xa có thể thấp hơn, nhưng nhờ tính minh bạch nên ít gặp lỗi hơn. Việc sử dụng socketchannel giúp đảm bảo độ tin cậy: khi kết nối bị ngắt, bên gọi RPC sẽ nhận được ngoại lệ và có thể thử lại (tự động kết nối lại).

Ngược lại, harbor ở tầng thấp giả định kết nối giữa các máy luôn ổn định. Khi mạng nội bộ gặp sự cố, toàn bộ hệ thống có thể bị ảnh hưởng. Thiết kế này không nhằm mục đích mở rộng đàn máy linh hoạt, mà chủ yếu để vượt qua giới hạn hiệu năng của một máy đơn.

Hai giải pháp truyền thông này đều có ưu nhược điểm riêng, cần cân nhắc kỹ khi thiết kế hệ thống. Tùy trường hợp cụ thể,

0%