Tối Ưu Hóa Luồng Mạng Trong Skynet - nói dối e blog

Tối Ưu Hóa Luồng Mạng Trong Skynet

Skynet là một framework được thiết kế để xử lý nhiệm vụ song song hiệu quả, tận dụng tối đa sức mạnh của CPU đa nhân cho các nghiệp vụ tiêu tốn tài nguyên và có khả năng song hành tự nhiên như game online. Trong thiết kế ban đầu, I/O mạng không phải là trọng tâm tối ưu hóa.

Xuất phát từ định hướng này, tầng mạng của skynet được triển khai theo mô hình đơn luồng. Lý do là bởi theo quan điểm của tôi, một chương trình đơn luồng với lượng mã lớn vẫn dễ hiểu và ít bug hơn chương trình đa luồng dù có lượng mã ngắn hơn. Những ứng dụng mạng kinh điển như Redis hay Nginx đã chứng minh rằng xử lý mạng đơn luồng không nhất thiết kém hiệu quả, thậm chí còn tạo được tiếng vang lớn nhờ tính ổn định.

Trong skynet, vòng lặp epoll không hoạt động như cách Erlang xử lý - chỉ quan sát sự kiện đọc/ghi rồi giao tiếp thật sự cho các actor xử lý. Giải pháp này tuy tăng khả năng xử lý mạng nhưng đồng nghĩa phải dùng khóa (lock) để bảo vệ API mạng khi bị truy cập từ nhiều actor trên nhiều luồng làm việc. Thay vào đó, hiện tại skynet áp dụng cơ chế “tuyến tính hóa” toàn bộ yêu cầu mạng: Các yêu cầu được gửi qua pipe vào luồng xử lý mạng, xử lý theo thứ tự, sau đó kết quả được phân phối đến các dịch vụ khác.

Thiết kế này tuy không phải tối ưu tuyệt đối, nhưng hoàn toàn phù hợp với thực tế. Dữ liệu từ các dự án game online cho thấy khi CPU xử lý các nghiệp vụ khác ở mức bão hòa, băng thông mạng hai chiều trên một máy vật lý hiếm khi vượt quá 30MB/s. Với khả năng xử lý hàng chục MB mỗi giây của một lõi CPU hiện đại, thông lượng này hoàn toàn dư thừa.

Tuy nhiên, tôi luôn ấp ủ ý tưởng mở rộng skynet cho các trường hợp nặng về I/O. Trong một lần trao đổi với sinh viên đang phát triển hệ thống phát video qua skynet, tôi nhận thấy ở môi trường sản phẩm có máy được trang bị nhiều card mạng 1Gbps nhưng skynet chưa tận dụng hết tiềm năng phần cứng khi xử lý UDP broadcast. Cách tiếp cận mới trong issue #646 gần đây đã thúc đẩy tôi triển khai ý tưởng đó.

Giải pháp đề xuất tách thao tác ghi mạng khỏi luồng mạng chính. Khi có dữ liệu cần gửi, hệ thống sẽ kiểm tra hàng đợi truyền phát của fd có trống không. Nếu trống, luồng làm việc hiện tại sẽ cố gắng gửi trực tiếp (trường hợp phổ biến nhất). Nếu gửi toàn bộ thành công thì tốt, nếu thất bại hay chỉ gửi một phần, phần chưa gửi sẽ được lưu vào cấu trúc socket và kích hoạt sự kiện EPOLLOUT. Luồng mạng sẽ ưu tiên xử lý phần dữ liệu bị dở dang này trước khi tiếp tục với hàng đợi chờ phát.

Với UDP đơn giản hơn nhiều do không có hiện tượng gửi một phần và không cần đảm bảo thứ tự. Nếu gửi ngay lập tức thất bại, gói tin sẽ được đưa vào cuối hàng đợi để xử lý theo quy trình thông thường.

Để hỗ trợ cơ chế này, mỗi cấu trúc socket cần bổ sung thêm spinlock. Luồng làm việc thực hiện gửi trực tiếp sẽ thử khóa (try lock) thay vì chờ khóa, chỉ duy nhất nơi xử lý hàng đợi trong luồng mạng mới cần khóa thực sự. Vì thời gian giữ khóa cực ngắn nên khả năng tranh giành khóa gần như không xảy ra.

Tuy nhiên đây là mã đa luồng nên tiềm ẩn nguy cơ bug và khó kiểm thử. Hiện tôi đang đặt trên nhánh độc lập, kính mong các bạn quan tâm cùng góp ý đánh giá trước khi quyết định hợp nhất vào nhánh chính.

0%