Tam's Blog

<- Quay về trang chủ

Networking (1) I/O multiplexing

Thông tin cập nhật:


Trong 2 tháng gần đây, mỗi ngày mình đều loay hoay với việc benchmark 1 opensource có liên quan tới chủ đề webRTC. Với 1 opensource cũng đã hơn chục năm, rất nhiều sao trên github nhưng kết quả test lại không đạt được như kì vọng về số connections, bitrate lẫn các metrics của server. Vì mình chưa có kinh nghiệm nhiều về mảng này nên chọn cách vào đọc source để hiểu được mô hình test cũng như đảm bảo mình đang viết đúng script test, và sau khi mày mò một thời gian dài, cuối cùng mình cũng thấy được việc mình có vấn đề trong cách tiếp cận một mảng kiến thức mới, sau khi đọc source và tìm hiểu thêm về webRTC, mình mới thực sự hiểu document của các tác giả :”).

Mục tiêu đặt ra là test trên môi trường nội bộ của công ty và và sau đó là trên Oracle với server riêng lẻ và cluster. Tưởng chừng như sau khi có số liệu trên môi trường nội bộ của công ty rồi việc test trên Oracle server cũng suông sẻ nhưng không, nó còn đau khổ hơn xD, lại mất 2 3 tuần, đến lúc tuyệt vọng, anh senior team mình bảo mình yêu cầu team infrastructure cùng tham gia để tìm hiểu vấn đề, và cuối cùng thì thật là abcxyz khi họ nhờ Oracle team và biết vấn đề nằm ở network giữa 2 region của server.

Anyway trước khi tới được kết quả, mình cũng đã có cơ hội tìm hiểu về một vài vấn đề liên quan đến network (chủ yếu là ở linux OS), những thứ mà hồi xưa học môn mạng máy tính ở trường không hiểu gì :))))), nên vì thế, mình dự định viết một vài bài về những thứ mình đã biết trong 2 tháng vừa qua.

Bài đầu tiên trong chuỗi này (và cũng không biết sẽ có bao nhiêu bài), về mô hình I/O multiplexing.

File Descriptor (FD): “In Linux, everything is a file”

Trong linux, FD là khái niệm chỉ sự định danh cho tệp tin, hoặc các tài nguyên I/O khác như socket khi một process thao tác với các tài nguyên này. Kernel lưu trữ thông tin các file được mở của mỗi process và FD là giá trị index trỏ tới các bản ghi này.

Mô hình I/O multiplexing hỗ trợ 1 thread có thể theo dõi vào nhiều FD cùng 1 lúc, bị block, chờ và nhận được thông báo về các FD sẵn sàng cho việc đọc dữ liệu. Ưu điểm của mô hình này so với mô hình truyền thống I/O blocking là giảm context switch giữa các threads vì chúng ta chỉ có 1 main thread để nhận request, xử lý cũng như đọc gói tin, tránh được race condition trong lập trình multithreads, có thể handle được số lượng lớn requests cùng lúc, tuy nhiên, mô hình này lại bị giới hạn bởi 1 CPU.

Các system call giúp bạn gọi Linux theo dõi các FD là poll, select và epoll.

poll/select

int select(int nfds,
           fd_set *restrict readfds,
           fd_set *restrict writefds,
           fd_set *restrict errorfds,
           struct timeval *restrict timeout);

Cách select hoạt động là truyền vào một danh sách các FD cần theo dõi và timeout, kernel sẽ lặp qua lần lượt các FD và kiểm tra xem các FD có thể đọc, ghi hay có khả năng lỗi. Giá trị timeout quyết định thời gian thread bị blocked khi hàm select được gọi.

I/O Multiplexing

Mô hình này có nhiều nhược điểm:

epoll

epoll là một phiên bản cải tiến của poll/select, giải quyết vấn đề hiệu năng và số lượng FD.

int listenfd = socket(AF_INET, SOCK_STREAM, 0);   
bind(listenfd, ...)
listen(listenfd, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd,...);

while(1){
    int n = epoll_wait(...)
    for(data from socket){
        // TODO
    }
}

Đầu tiên tạo một epoll object với hàm epoll_create, cấu trúc của 1 epoll instance:

epoll-mechanism

Ưu điểm của epoll:

Level-Triggered vs Edge-Triggered

Mặc định, epoll sẽ hoạt động với level-triggered, điều này làm nó gần như giống với poll/select nhưng tối ưu hơn về hiệu năng.

Ngoài ra, epoll còn có thể hoạt động với edge-triggered, epoll_wait trả về cho chúng ta có sự thay đổi nào trên FD kể từ lần gọi trước hay không.

Để sử dụng edge-triggered với epoll, chúng ta sử dụng cờ EPOLLETev.events khi gọi hàm epoll_ctl.

Để làm rõ hơn sự khác nhau giữa 2 level, xem ví dụ sau: Giả sử chúng ta muốn theo dõi để đọc dữ liệu một socket, có các bước như sau:

  1. Dữ liệu đến socket.
  2. Thực hiện gọi hàm epoll_wait, kết quả trả về của hàm này là FD đã sẵn sàng cho dù chúng ta đang sử dụng level-triggered hay edged-triggered.
  3. Thực hiện gọi hàm epoll_wait 1 lần nữa.

Kết quả của bước thứ 3 như sau:

Ví dụ

Tổng kết

Qua bài viết này, mình đã tóm gọn lại về tổng quát cách mô hình I/O multiplexing hoạt động với các cơ chế ở đằng sau như poll, select và epoll.

Hiện nay, nhiều công cụ và framework nổi tiếng cũng áp dụng epoll:

Tham khảo