Thiết Kế Và Giao Thức Mã Hóa Mô-Đun Cluster Skynet - nói dối e blog

Thiết Kế Và Giao Thức Mã Hóa Mô-Đun Cluster Skynet

Trong thiết kế ban đầu của skynet, mục tiêu là xây dựng một hệ thống phân tán có khả năng xóa nhòa ranh giới giữa các actor chạy trên cùng một máy và các actor ở hai đầu mạng. Để đạt được điều này, mô hình master/slave đã được thiết kế. Bằng cách sử dụng 4 byte để biểu diễn địa chỉ actor, trong đó 8 bit cao là mã nút và 24 bit thấp là ID nội bộ của tiến trình. Nhờ đó, mỗi actor (được gọi là dịch vụ trong skynet) đều có địa chỉ duy nhất trong toàn hệ thống, bất kể nó nằm trong tiến trình nào. Khi gửi tin nhắn, người dùng không cần quan tâm đích đến là trong cùng tiến trình hay phải qua mạng.

Tuy nhiên, sau này tôi nhận ra rằng việc xóa nhòa sự khác biệt giữa mạng và xử lý cục bộ không thực sự khả thi. Để xây dựng một hệ thống phân tán đáng tin cậy như một tiến trình đơn, chắc chắn sẽ không thể đơn giản. Trong khi đó, cốt lõi của skynet luôn hướng đến sự đơn giản và ổn định. Vì vậy, tôi quyết định chuyển hỗ trợ phân tán lên một tầng cao hơn.

Trước tiên, hãy cùng phân tích sự khác biệt cơ bản giữa giao tiếp nội tiến trình và giao tiếp qua mạng:

  1. Chia sẻ bộ nhớ nội bộ: Trong cùng một tiến trình, bộ nhớ được chia sẻ. Mặc dù skynet sử dụng sandbox Lua để cách ly trạng thái dịch vụ, nhưng vẫn có thể vượt qua sandbox thông qua thư viện C để giao tiếp trực tiếp. Khi một dịch vụ tạo ra lượng lớn dữ liệu cần truyền cho dịch vụ khác, trong môi trường nội tiến trình, không cần quá trình tuần tự hóa (serialization) mà chỉ cần truyền con trỏ địa chỉ bộ nhớ. Tối ưu hóa này tạo ra sự khác biệt về hiệu năng từ O(1) đến O(n), một yếu tố không thể bỏ qua.

  2. Tính sống-chết đồng bộ: Về bản chất, các dịch vụ trong cùng tiến trình có số phận gắn bó với nhau. Sandbox Lua đảm bảo lỗi nghiệp vụ có thể bắt được, trong khi các lỗi ngoài tầm kiểm soát như mất điện hay ngắt mạng sẽ không làm một phần hệ thống hoạt động trong khi phần khác sập. Nếu bạn chắc chắn hai actor ở cùng tiến trình, bạn có thể yên tâm rằng nếu actor này hoạt động thì đối tác cũng vậy - giống như việc gọi một hàm trong cùng tiến trình mà không lo hàm đó không tồn tại hay không trả về. Đây là lý do tại sao không cần cơ chế timeout cho RPC nội tiến trình. Việc bỏ qua việc xử lý phản hồi giúp giảm tải nhận thức cho lập trình viên, tương tự như việc các API thư viện thông thường (không phải xử lý IO) không có tham số timeout.

  3. Băng thông chia sẻ: Giao tiếp nội tiến trình được chia sẻ toàn bộ băng thông bus bộ nhớ, một con số cực lớn tương xứng với tốc độ CPU. Điều này gần như loại bỏ khả năng quá tải do xử lý dữ liệu. Tuy nhiên, khi vượt qua giới hạn mạng, không chỉ có sự chênh lệch về tốc độ xử lý mà còn về băng thông truyền tải. Nếu bắt lập trình viên luôn phân biệt dữ liệu gửi nội hay mạng thì sẽ mâu thuẫn với nguyên tắc thiết kế ban đầu.

Tôi cho rằng, trừ khi nghiệp vụ của bạn chủ yếu dựa vào IO và không tận dụng đa nhân CPU, việc xóa nhòa ranh giới nội bộ-mạng là vô nghĩa. Dù phần cứng phát triển đến đâu, băng thông bus trên bo mạch và mạng TCP sẽ mãi chênh lệch về bậc độ lớn do quy luật vật lý quyết định.

Khi nghiệp vụ đòi hỏi tính toán cao, các actor cần được đặt trên cùng máy để phát huy tối đa hiệu năng CPU. Nếu hệ thống cần mở rộng phân tán, đó phải là tập hợp các nghiệp vụ độc lập có thể xử lý song song. Việc này đòi hỏi kiến trúc sư hệ thống phải có kế hoạch triển khai rõ ràng, không thể tùy tiện phân bổ actor.

Đây chính là kịch bản lý tưởng cho skynet - nền tảng lý tưởng cho các dịch vụ game trực tuyến với nhiều cụm máy chủ, nơi các cảnh game tương tác yếu nhưng đòi hỏi tính toán mạnh mẽ nội bộ.

Trên tầng cốt lõi skynet, tôi thiết kế mô-đun cluster. Phần lớn được viết bằng Lua, chỉ có phần xử lý giao thức mạng là dùng C. Việc dùng Lua tăng tính bảo trì hệ thống, trong khi hiệu năng xử lý gói tin của Lua so với C là không đáng kể so với băng thông mạng.

Nguyên lý hoạt động:

  • Mỗi nút skynet (một tiến trình) khởi động dịch vụ clusterd. Mọi tin nhắn cần gửi ra ngoài sẽ qua clusterd để chuyển tiếp mạng.
  • Mỗi nút trong cluster được đặt tên bằng chuỗi, ánh xạ đến IP/cổng qua bảng cấu hình. Một tiến trình có thể lắng nghe nhiều cổng nếu phân biệt tên.

Để phân biệt với tin nhắn nội bộ, cluster cung cấp thư viện riêng với API mới, là lớp bao bọc nông trên clusterd. Ngoài ra, có thể tạo dịch vụ proxy nhận tin nhắn, gắn tên và chuyển tiếp đến clusterd - tương tự mô hình master/slave cũ.

Trong skynet 1.0, không hỗ trợ cluster.send (gửi một chiều). Mọi tin nhắn phải dùng cluster.call (yêu cầu-phản hồi). Lý do:

  1. Gửi một chiều có thể thay thế bằng call bỏ qua phản hồi. Nếu lo call chặn luồng, có thể dùng skynet.fork hoặc tạo dịch vụ proxy.
  2. Tôi muốn người dùng nhận thức rõ về bản chất mạng. Gửi một chiều có thể thất bại (mất kết nối, node sập), cần cơ chế phản hồi lỗi. Dù có thể xây dựng hàng đợi tin cậy, nhưng sẽ làm tăng độ phức tạp và giảm hiệu năng. cluster.call trực tiếp ném error khi thất bại, buộc lập trình viên xử lý lỗi, trong khi cluster.send không thể làm được điều này.

Tuy nhiên, thực tiễn một năm qua cho thấy cả người dùng nội bộ và bên ngoài đều khao khát cluster.send. Tôi đã điều chỉnh, dự kiến đưa vào skynet 1.1.

Phiên bản đầu tiên khá tùy tiện: mở rộng skynet.ret để nhận biết tin nhắn một chiều qua session=0. Mọi tin nhắn đều phải skynet.ret, framework tự quyết có phản hồi hay không. cluster.send dùng cluster.call để gửi, đối phương phản hồi nhưng bên gửi

0%