Sửa Đổi Nhỏ Trên Mô-Đun Cổng Của Skynet - nói dối e blog

Sửa Đổi Nhỏ Trên Mô-Đun Cổng Của Skynet

Module cổng giao tiếp của skynet được cập nhật nhỏ

Trong hệ thống skynet tồn tại một module có tên là gate, chuyên xử lý việc đọc dữ liệu từ các kết nối mạng bên ngoài. Module này ban đầu được phát triển dựa trên đoạn mã mà tôi vô tình viết ra để minh họa cho ví dụ về ringbuffer.

Lúc đầu tôi cho rằng việc sử dụng epoll để xử lý sự kiện đọc dữ liệu là hoàn toàn đủ, còn việc ghi dữ liệu thì cứ để ở chế độ đồng bộ (blocking) trực tiếp xuống socket. Lý do là bởi skynet có khả năng phân bổ các tác vụ vào nhiều luồng xử lý khác nhau, nên việc một vài kết nối bị nghẽn cũng sẽ không làm toàn bộ hệ thống dừng hoạt động. Có thể hình dung đơn giản đây là mô hình “độc đọc đa luồng, ghi dữ liệu đơn luồng”.

Tuy nhiên khi áp dụng vào dự án game MMORPG của chúng tôi, nhược điểm của phương pháp này dần lộ rõ. Trong các tình huống có đông người chơi tập trung, việc phát bản tin (broadcast) cùng lúc cho nhiều kết nối có thể làm nghẽn toàn bộ các luồng xử lý. Khi đó dù số lượng luồng làm việc là cố định, nhưng hiện tượng nghẽn cổ chai vẫn xảy ra do hoạt động ghi dữ liệu cùng lúc.

Vấn đề này đã được phát hiện từ khá lâu. Lúc đó, bạn Tiểu Ngưu (Snail) đã tạm thời khắc phục bằng cách chuyển các kết nối mạng bên ngoài sang chế độ không đồng bộ (non-blocking), đồng thời mở rộng kích thước bộ đệm. Nếu phát hiện đầy bộ đệm, hệ thống sẽ tự động đóng kết nối. Tuy nhiên cách xử lý này chưa triệt để và việc không thông báo cho module agent về việc ngắt kết nối đã gây ra lỗi logic nghiệp vụ.

Đầu tuần trước, sau khi phân tích kỹ lưỡng, tôi quyết định sửa chữa dứt điểm phần mã nguồn còn tồn đọng này. Giải pháp đầu tiên là tích hợp thêm vùng đệm ghi (write buffer) vào module gửi dữ liệu. Khi gặp hiện tượng đầy bộ đệm hệ thống, dữ liệu sẽ được lưu trữ tạm thời trong bộ đệm tầng ứng dụng. Đến lần yêu cầu ghi tiếp theo, hệ thống sẽ tiếp tục gửi nốt phần dữ liệu còn lại. Với đặc thù ứng dụng game mạng, giải pháp này cơ bản đã đáp ứng được yêu cầu. Tuy nhiên vẫn tồn tại rủi ro nhỏ là phần dữ liệu cuối cùng có thể không được gửi đi.

Để khắc phục lỗ hổng này, tôi thiết lập cơ chế hẹn giờ (timer) cho mỗi vùng đệm ứng dụng. Khi phát hiện vùng đệm còn dữ liệu tồn đọng, timer sẽ kích hoạt quá trình xử lý sau một khoảng thời gian chờ nhất định.

Giải pháp này lại làm bạn Tiểu Tĩnh (Xiaojing) không hài lòng. Anh ấy cho rằng việc không sử dụng epoll đồng bộ để xử lý các sự kiện ghi mà lại dùng timer là không thể chấp nhận được. Cá nhân tôi cũng đồng cảm với quan điểm này, nhưng thật lòng mà nói, tôi vẫn muốn trì hoãn việc sửa đổi những đoạn mã vốn đang chạy ổn định.

Tuy nhiên cuối tuần qua, tôi vẫn quyết định hành động. Ban đầu tôi dự định viết một module độc lập để quản lý toàn bộ sự kiện ghi. Nhưng việc tách biệt module đọc và ghi sẽ dẫn đến bài toán khó là khi người dùng muốn đóng socket sẽ không thể biết liệu trong vùng đệm ghi còn tồn tại dữ liệu chưa gửi hết hay không. Đối với ứng dụng game thì đây không phải vấn đề nghiêm trọng, nhưng với tư cách là một hệ thống nền tảng như skynet, hành động “cắt đứt” như vậy là hơi thô bạo.

Tôi cũng cân nhắc một phương án khác: xây dựng module mới sử dụng epoll để giám sát cả sự kiện đọc và ghi, sau đó chuyển các sự kiện này đến các dịch vụ cần xử lý, trong khi bản thân module không trực tiếp thao tác đọc/ghi dữ liệu. Cách này sẽ giúp chuyển đổi giao tiếp socket hệ thống thành API thân thiện với skynet một cách hoàn hảo. Tuy nhiên nhược điểm là tốn nhiều bộ nhớ hơn (do mỗi kết nối cần có vùng đệm độc lập), làm thay đổi nhiều mã nguồn hiện có, đồng thời thêm một tầng gián tiếp có thể ảnh hưởng đến hiệu năng.

Mặc dù nhận thấy đây là giải pháp tối ưu nhất (và còn có thể tích hợp cả các thao tác IO khác của skynet vào cùng), cuối cùng tôi vẫn quyết định từ bỏ phương án này.

Đến hôm qua, tôi chọn phương án tập trung toàn bộ thao tác ghi socket về module gate xử lý, đồng thời giữ nguyên cách xử lý phân tán cũ để đảm bảo tính tương thích (sẽ loại bỏ sau khi phiên bản mới vận hành ổn định). Các module gửi dữ liệu truyền thống vẫn được giữ lại, mỗi kết nối sẽ có một dịch vụ riêng đảm nhận việc đóng gói dữ liệu, nhưng thay vì ghi trực tiếp xuống socket, chúng sẽ chuyển dữ liệu qua gate để xử lý tập trung.

Dù giải pháp mới làm tăng thêm một bước sao chép dữ liệu, nhưng xét đến việc sau này chúng ta còn cần thực hiện mã hóa và nén dữ liệu, bước trung gian này lại trở thành cần thiết chứ không thừa thãi.

Hiện tại mã nguồn mới đã được cập nhật lên GitHub. Phần hỗ trợ kqueue vẫn chưa có môi trường kiểm thử, rất mong các bạn có môi trường phù hợp có thể hỗ trợ kiểm tra giúp.

0%