Một Vài Suy Nghĩ Tiếp Nối Về Bộ Lập Lịch Skynet
Bài viết này là phần tiếp nối tự nhiên từ những ý tưởng trước đó của tôi. Gần đây tôi đang ấp ủ ý định xây dựng một bộ lập lịch đa nhiệm N:M mới dựa trên cơ chế ống dẫn thông điệp, dành riêng cho hệ thống client của chúng tôi. Đây là giải pháp được rút ra từ nhiều năm kinh nghiệm vận hành skynet và khắc phục những hạn chế tồn đọng.
Điểm mấu chốt đầu tiên tôi muốn thay đổi nằm ở cơ chế truyền thống của skynet khi các dịch vụ gửi thông điệp trực tiếp vào hàng đợi đích. Cách tiếp cận này khiến dịch vụ gửi đi bị treo chờ trong quá trình truyền tải. Tôi đã thiết kế một mô-đun trung gian gọi là “bộ lập lịch” (scheduler) chuyên trách việc phân phối thông điệp đến các ống dẫn nhận tương ứng.
Trong kiến trúc mới:
- Bộ lập lịch đóng vai trò là người tiêu dùng thông điệp gửi đi, đồng thời là nhà sản xuất duy nhất đối với các ống dẫn nhận
- Mỗi dịch vụ lại là nhà sản xuất duy nhất của thông điệp gửi đi và người tiêu dùng độc quyền của ống dẫn nhận riêng mình
Cơ chế này hoàn toàn loại bỏ cạnh tranh tài nguyên nhờ thiết kế đơn luồng, ngay cả khi sử dụng hàng đợi có độ dài cố định cũng không cần khóa (chỉ cần xử lý trạng thái hàng đợi đầy). Đồng thời vẫn tuân thủ nguyên tắc bất di bất dịch của skynet: thao tác gửi thông điệp không gây ra hiện tượng tái nhập dịch vụ.
Khi một dịch vụ bị treo do gửi thông điệp, bộ lập lịch sẽ ưu tiên xử lý nhanh chóng để tiếp tục luồng thực thi. Trong quá trình này, không có tác vụ nào khác xen ngang cho đến khi nhận được kết quả từ việc gửi thông điệp. Kết quả này có thể là thành công, thất bại do đối phương không tồn tại, hoặc bị chặn do hàng đợi đối phương đầy. Lớp logic ứng dụng sẽ quyết định có nên thử lại hay loại bỏ thông điệp bị chặn.
Điểm then chốt của giải pháp nằm ở chính bộ lập lịch. Tôi quyết định thay đổi cơ chế hiện tại của skynet nơi các luồng làm việc tranh giành nhiệm vụ. Thay vào đó, chỉ duy nhất bộ lập lịch được phân công nhiệm vụ, giúp loại bỏ khóa hàng đợi toàn cục và tối ưu hóa việc giữ dịch vụ làm việc trên cùng một luồng vật lý.
Cảm hứng thiết kế đến từ hàng trăm giờ chơi game “Factorio” (Nhà máy ngoài hành tinh), nơi tôi học được cách tối ưu hóa luồng công việc trong hệ thống phức tạp. Ban đầu tôi dự định thiết kế hàng đợi công việc cho từng luồng, với bộ lập lịch phân bổ nhiệm vụ dựa trên vị trí luồng gốc và cân bằng tải. Tuy nhiên giải pháp này gặp vấn đề khi có nhiệm vụ kéo dài bất thường, gây trễ dây chuyền cho toàn bộ hàng đợi.
Tôi đã tìm ra cách tiếp cận tinh gọn hơn: thay vì hàng đợi, mỗi luồng làm việc chỉ có một “khe nhiệm vụ” (slot) duy nhất - tương đương hàng đợi độ dài 1. Bộ lập lịch trong mỗi chu kỳ chỉ cần điền nhiệm vụ vào các slot trống. Khi luồng làm việc hoàn thành nhiệm vụ, nó sẽ kiểm tra slot của mình và thực thi nếu còn nhiệm vụ.
Một thủ thuật thông minh được áp dụng: không cần dành riêng luồng cho bộ lập lịch. Thay vào đó, bất kỳ luồng làm việc nào cũng có thể đảm nhận vai trò này thông qua cơ chế CAS (Compare-And-Swap). Khi luồng làm việc phát hiện slot của mình trống rỗng, nó sẽ tranh giành quyền điều phối. Nếu giành chiến thắng, luồng đó sẽ ưu tiên phân bổ nhiệm vụ cho chính mình trước, sau đó mới phân phối cho các slot khác. Trong trường hợp khẩn cấp, nó còn có thể “cướp” nhiệm vụ từ slot khác để xử lý.
Giải pháp này đảm bảo: chỉ cần còn tồn tại ít nhất một luồng làm việc chưa nghỉ, sẽ luôn có người xử lý nhiệm vụ. Khi luồng làm việc không tìm được việc và thất bại trong việc giành quyền điều phối, nó sẽ tự động nghỉ ngơi nhưng vẫn theo dõi số lượng luồng đang hoạt động. Nếu phát hiện mình là luồng cuối cùng, nó sẽ đánh thức toàn bộ hệ thống để tránh tình trạng “tất cả ngủ quên” khi vẫn còn nhiệm vụ tồn đọng.
Chỉ khi bộ lập lịch xác nhận không còn nhiệm vụ nào để xử lý, toàn bộ hệ thống mới thực sự nghỉ ngơi, chờ đợi tín hiệu đánh thức từ bên ngoài (thông điệp mạng hoặc tín hiệu thời gian hệ thống).