Lò phản ứng I/O trần C đầy đủ tính năng

Lò phản ứng I/O trần C đầy đủ tính năng

Giới thiệu

Lò phản ứng I/O (luồng đơn vòng lặp sự kiện) là mẫu viết phần mềm tải cao, được sử dụng trong nhiều giải pháp phổ biến:

Trong bài viết này, chúng ta sẽ tìm hiểu chi tiết về bộ phản ứng I/O cũng như cách hoạt động của nó, viết phần triển khai trong chưa đầy 200 dòng mã và tạo một quy trình máy chủ HTTP đơn giản với hơn 40 triệu yêu cầu/phút.

lời tựa

  • Bài viết được viết nhằm giúp hiểu rõ hoạt động của lò phản ứng I/O và từ đó hiểu được những rủi ro khi sử dụng nó.
  • Cần phải có kiến ​​thức cơ bản để hiểu được bài viết. ngôn ngữ C và một số kinh nghiệm trong phát triển ứng dụng mạng.
  • Tất cả mã được viết bằng ngôn ngữ C theo đúng (thận trọng: PDF dài) đạt tiêu chuẩn C11 dành cho Linux và có sẵn trên GitHub.

Tại sao làm điều đó?

Với sự phổ biến ngày càng tăng của Internet, các máy chủ web bắt đầu cần xử lý đồng thời một số lượng lớn kết nối và do đó, hai phương pháp đã được thử: chặn I/O trên một số lượng lớn các luồng hệ điều hành và không chặn I/O kết hợp với một hệ thống thông báo sự kiện, còn được gọi là "bộ chọn hệ thống" (kỷ nguyên/xếp hàng/IOCP/vân vân).

Cách tiếp cận đầu tiên liên quan đến việc tạo một luồng hệ điều hành mới cho mỗi kết nối đến. Nhược điểm của nó là khả năng mở rộng kém: hệ điều hành sẽ phải triển khai nhiều chuyển tiếp bối cảnh и cuộc gọi hệ thống. Chúng là những hoạt động tốn kém và có thể dẫn đến thiếu RAM trống với số lượng kết nối ấn tượng.

Điểm nổi bật của phiên bản sửa đổi số lượng chủ đề cố định (nhóm luồng), do đó ngăn hệ thống hủy bỏ việc thực thi, nhưng đồng thời gây ra một vấn đề mới: nếu nhóm luồng hiện bị chặn bởi các hoạt động đọc dài, thì các ổ cắm khác đã có thể nhận dữ liệu sẽ không thể làm như vậy.

Cách tiếp cận thứ hai sử dụng hệ thống thông báo sự kiện (bộ chọn hệ thống) do HĐH cung cấp. Bài viết này thảo luận về loại bộ chọn hệ thống phổ biến nhất, dựa trên các cảnh báo (sự kiện, thông báo) về mức độ sẵn sàng cho các hoạt động I/O, thay vì dựa trên thông báo về việc hoàn thành của họ. Một ví dụ đơn giản về việc sử dụng nó có thể được biểu diễn bằng sơ đồ khối sau:

Lò phản ứng I/O trần C đầy đủ tính năng

Sự khác biệt giữa các phương pháp này như sau:

  • Chặn hoạt động I/O đình chỉ luồng người dùng cho đến khicho đến khi hệ điều hành hoạt động bình thường chống phân mảnh mới đến gói IP sang luồng byte (TCP, nhận dữ liệu) hoặc sẽ không có đủ dung lượng trong bộ đệm ghi bên trong để gửi tiếp theo qua NIC (gửi dữ liệu).
  • Bộ chọn hệ thống tăng ca thông báo cho chương trình rằng hệ điều hành đã gói IP được chống phân mảnh (TCP, nhận dữ liệu) hoặc có đủ dung lượng trong bộ đệm ghi bên trong đã có sẵn (gửi dữ liệu).

Tóm lại, việc dành một luồng hệ điều hành cho mỗi I/O là một sự lãng phí sức mạnh tính toán, bởi vì trên thực tế, các luồng không thực hiện công việc hữu ích (đây là nguồn gốc của thuật ngữ này). "phần mềm bị gián đoạn"). Bộ chọn hệ thống giải quyết vấn đề này, cho phép chương trình người dùng sử dụng tài nguyên CPU một cách tiết kiệm hơn nhiều.

Mô hình lò phản ứng I/O

Bộ phản hồi I/O hoạt động như một lớp giữa bộ chọn hệ thống và mã người dùng. Nguyên lý hoạt động của nó được mô tả bằng sơ đồ khối sau:

Lò phản ứng I/O trần C đầy đủ tính năng

  • Hãy để tôi nhắc bạn rằng một sự kiện là một thông báo rằng một ổ cắm nhất định có thể thực hiện thao tác I/O không chặn.
  • Trình xử lý sự kiện là một chức năng được bộ phản hồi I/O gọi khi nhận được một sự kiện, sau đó thực hiện thao tác I/O không chặn.

Điều quan trọng cần lưu ý là bộ phản ứng I/O theo định nghĩa là đơn luồng, nhưng không có gì ngăn cản khái niệm này được sử dụng trong môi trường đa luồng với tỷ lệ 1 luồng: 1 bộ phản ứng, nhờ đó tái chế tất cả các lõi CPU.

Thực hiện

Chúng tôi sẽ đặt giao diện công cộng trong một tập tin reactor.hvà thực hiện - trong reactor.c. reactor.h sẽ bao gồm các thông báo sau:

Hiển thị các khai báo trong Reactor.h

typedef struct reactor Reactor;

/*
 * Указатель на функцию, которая будет вызываться I/O реактором при поступлении
 * события от системного селектора.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Возвращает `NULL` в случае ошибки, не-`NULL` указатель на `Reactor` в
 * противном случае.
 */
Reactor *reactor_new(void);

/*
 * Освобождает системный селектор, все зарегистрированные сокеты в данный момент
 * времени и сам I/O реактор.
 *
 * Следующие функции возвращают -1 в случае ошибки, 0 в случае успеха.
 */
int reactor_destroy(Reactor *reactor);

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg);

/*
 * Запускает цикл событий с тайм-аутом `timeout`.
 *
 * Эта функция передаст управление вызывающему коду если отведённое время вышло
 * или/и при отсутствии зарегистрированных сокетов.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Cấu trúc lò phản ứng I/O bao gồm mô tả tập tin bộ chọn kỷ nguyên и bảng băm GHashTable, ánh xạ từng ổ cắm tới CallbackData (cấu trúc của trình xử lý sự kiện và đối số người dùng cho nó).

Hiển thị Reactor và CallbackData

struct reactor {
    int epoll_fd;
    GHashTable *table; // (int, CallbackData)
};

typedef struct {
    Callback callback;
    void *arg;
} CallbackData;

Xin lưu ý rằng chúng tôi đã kích hoạt khả năng xử lý loại không đầy đủ theo chỉ số. TRONG reactor.h chúng tôi khai báo cấu trúc reactorvà trong reactor.c chúng tôi xác định nó, do đó ngăn người dùng thay đổi rõ ràng các trường của nó. Đây là một trong những mẫu ẩn dữ liệu, phù hợp một cách ngắn gọn với ngữ nghĩa C.

Chức năng reactor_register, reactor_deregister и reactor_reregister cập nhật danh sách các ổ cắm quan tâm và trình xử lý sự kiện tương ứng trong bộ chọn hệ thống và bảng băm.

Hiển thị chức năng đăng ký

#define REACTOR_CTL(reactor, op, fd, interest)                                 
    if (epoll_ctl(reactor->epoll_fd, op, fd,                                   
                  &(struct epoll_event){.events = interest,                    
                                        .data = {.fd = fd}}) == -1) {          
        perror("epoll_ctl");                                                   
        return -1;                                                             
    }

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

int reactor_deregister(const Reactor *reactor, int fd) {
    REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0)
    g_hash_table_remove(reactor->table, &fd);
    return 0;
}

int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

Sau khi bộ phản ứng I/O đã chặn sự kiện bằng bộ mô tả fd, nó gọi trình xử lý sự kiện tương ứng mà nó chuyển tới fd, mặt nạ bit các sự kiện được tạo và một con trỏ người dùng tới void.

Hiển thị hàm Reactor_run()

int reactor_run(const Reactor *reactor, time_t timeout) {
    int result;
    struct epoll_event *events;
    if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL)
        abort();

    time_t start = time(NULL);

    while (true) {
        time_t passed = time(NULL) - start;
        int nfds =
            epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);

        switch (nfds) {
        // Ошибка
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Время вышло
        case 0:
            result = 0;
            goto cleanup;
        // Успешная операция
        default:
            // Вызвать обработчиков событий
            for (int i = 0; i < nfds; i++) {
                int fd = events[i].data.fd;

                CallbackData *callback =
                    g_hash_table_lookup(reactor->table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Tóm lại, chuỗi lệnh gọi hàm trong mã người dùng sẽ có dạng sau:

Lò phản ứng I/O trần C đầy đủ tính năng

Máy chủ luồng đơn

Để kiểm tra lò phản ứng I/O ở mức tải cao, chúng tôi sẽ viết một máy chủ web HTTP đơn giản đáp ứng mọi yêu cầu có hình ảnh.

Tham khảo nhanh về giao thức HTTP

HTTP - đây là giao thức cấp độ ứng dụng, chủ yếu được sử dụng để tương tác giữa máy chủ và trình duyệt.

HTTP có thể được sử dụng dễ dàng vận chuyển giao thức TCP, gửi và nhận tin nhắn theo định dạng được chỉ định sự chỉ rõ.

Định dạng yêu cầu

<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • CRLF là một chuỗi gồm hai ký tự: r и n, tách dòng đầu tiên của yêu cầu, tiêu đề và dữ liệu.
  • <КОМАНДА> - một trong CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Trình duyệt sẽ gửi lệnh đến máy chủ của chúng tôi GET, có nghĩa là "Gửi cho tôi nội dung của tập tin."
  • <URI> - định danh tài nguyên thống nhất. Ví dụ: nếu URI = /index.html, sau đó khách hàng sẽ yêu cầu trang chính của trang web.
  • <ВЕРСИЯ HTTP> - phiên bản của giao thức HTTP ở định dạng HTTP/X.Y. Phiên bản được sử dụng phổ biến nhất hiện nay là HTTP/1.1.
  • <ЗАГОЛОВОК N> là một cặp khóa-giá trị ở định dạng <КЛЮЧ>: <ЗНАЧЕНИЕ>, được gửi đến máy chủ để phân tích thêm.
  • <ДАННЫЕ> — dữ liệu được máy chủ yêu cầu để thực hiện thao tác. Nhiều khi nó đơn giản JSON hoặc bất kỳ định dạng nào khác.

Định dạng phản hồi

<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • <КОД СТАТУСА> là một số đại diện cho kết quả của hoạt động. Máy chủ của chúng tôi sẽ luôn trả về trạng thái 200 (hoạt động thành công).
  • <ОПИСАНИЕ СТАТУСА> — biểu diễn chuỗi mã trạng thái. Đối với mã trạng thái 200, đây là OK.
  • <ЗАГОЛОВОК N> - tiêu đề có cùng định dạng như trong yêu cầu. Chúng tôi sẽ trả lại danh hiệu Content-Length (kích thước tệp) và Content-Type: text/html (trả về kiểu dữ liệu).
  • <ДАННЫЕ> - dữ liệu do người dùng yêu cầu. Trong trường hợp của chúng tôi, đây là đường dẫn đến hình ảnh trong HTML.

hồ sơ http_server.c (máy chủ luồng đơn) bao gồm tệp common.h, chứa các nguyên mẫu hàm sau:

Hiển thị các nguyên mẫu hàm chung.h

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
static void on_accept(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов отправить HTTP ответ.
 */
static void on_send(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять часть HTTP запроса.
 */
static void on_recv(void *arg, int fd, uint32_t events);

/*
 * Переводит входящее соединение в неблокирующий режим.
 */
static void set_nonblocking(int fd);

/*
 * Печатает переданные аргументы в stderr и выходит из процесса с
 * кодом `EXIT_FAILURE`.
 */
static noreturn void fail(const char *format, ...);

/*
 * Возвращает файловый дескриптор сокета, способного принимать новые
 * TCP соединения.
 */
static int new_server(bool reuse_port);

Macro chức năng cũng được mô tả SAFE_CALL() và chức năng được xác định fail(). Macro so sánh giá trị của biểu thức với lỗi và nếu điều kiện đúng thì gọi hàm fail():

#define SAFE_CALL(call, error)                                                 
    do {                                                                       
        if ((call) == error) {                                                   
            fail("%s", #call);                                                 
        }                                                                      
    } while (false)

Chức năng fail() in các đối số được truyền vào thiết bị đầu cuối (như printf()) và kết thúc chương trình với mã EXIT_FAILURE:

static noreturn void fail(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, ": %sn", strerror(errno));
    exit(EXIT_FAILURE);
}

Chức năng new_server() trả về bộ mô tả tệp của ổ cắm "máy chủ" được tạo bởi các lệnh gọi hệ thống socket(), bind() и listen() và có khả năng chấp nhận các kết nối đến ở chế độ không chặn.

Hiển thị hàm new_server()

static int new_server(bool reuse_port) {
    int fd;
    SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)),
              -1);

    if (reuse_port) {
        SAFE_CALL(
            setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)),
            -1);
    }

    struct sockaddr_in addr = {.sin_family = AF_INET,
                               .sin_port = htons(SERVER_PORT),
                               .sin_addr = {.s_addr = inet_addr(SERVER_IPV4)},
                               .sin_zero = {0}};

    SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1);
    SAFE_CALL(listen(fd, SERVER_BACKLOG), -1);
    return fd;
}

  • Lưu ý rằng ổ cắm ban đầu được tạo ở chế độ không chặn bằng cờ SOCK_NONBLOCKdo đó trong hàm on_accept() (đọc thêm) cuộc gọi hệ thống accept() đã không dừng việc thực thi luồng.
  • Nếu reuse_porttrue, thì chức năng này sẽ cấu hình ổ cắm với tùy chọn SO_REUSEPORT xuyên qua setsockopt()để sử dụng cùng một cổng trong môi trường đa luồng (xem phần “Máy chủ đa luồng”).

Xử lý sự kiện on_accept() được gọi sau khi hệ điều hành tạo ra một sự kiện EPOLLIN, trong trường hợp này có nghĩa là kết nối mới có thể được chấp nhận. on_accept() chấp nhận kết nối mới, chuyển nó sang chế độ không chặn và đăng ký với trình xử lý sự kiện on_recv() trong một lò phản ứng I/O.

Hiển thị hàm on_accept()

static void on_accept(void *arg, int fd, uint32_t events) {
    int incoming_conn;
    SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1);
    set_nonblocking(incoming_conn);
    SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv,
                               request_buffer_new()),
              -1);
}

Xử lý sự kiện on_recv() được gọi sau khi hệ điều hành tạo ra một sự kiện EPOLLIN, trong trường hợp này có nghĩa là kết nối đã đăng ký on_accept(), sẵn sàng nhận dữ liệu.

on_recv() đọc dữ liệu từ kết nối cho đến khi nhận được yêu cầu HTTP hoàn toàn, sau đó nó đăng ký một trình xử lý on_send() để gửi phản hồi HTTP. Nếu máy khách ngắt kết nối, ổ cắm sẽ bị hủy đăng ký và đóng bằng cách sử dụng close().

Hiển thị hàm on_recv()

static void on_recv(void *arg, int fd, uint32_t events) {
    RequestBuffer *buffer = arg;

    // Принимаем входные данные до тех пор, что recv возвратит 0 или ошибку
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Клиент оборвал соединение
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read вернул ошибку, отличную от ошибки, при которой вызов заблокирует
    // поток
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Получен полный HTTP запрос от клиента. Теперь регистрируем обработчика
    // событий для отправки данных
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

Xử lý sự kiện on_send() được gọi sau khi hệ điều hành tạo ra một sự kiện EPOLLOUT, nghĩa là kết nối đã được đăng ký on_recv(), sẵn sàng gửi dữ liệu. Hàm này gửi phản hồi HTTP chứa HTML kèm hình ảnh đến máy khách và sau đó thay đổi trình xử lý sự kiện trở lại thành on_recv().

Hiển thị hàm on_send()

static void on_send(void *arg, int fd, uint32_t events) {
    const char *content = "<img "
                          "src="https://habrastorage.org/webt/oh/wl/23/"
                          "ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
    char response[1024];
    sprintf(response,
            "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
            "text/html" DOUBLE_CRLF "%s",
            strlen(content), content);

    SAFE_CALL(send(fd, response, strlen(response), 0), -1);
    SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}

Và cuối cùng, trong tập tin http_server.c, trong chức năng main() chúng tôi tạo ra một lò phản ứng I/O bằng cách sử dụng reactor_new(), tạo một ổ cắm máy chủ và đăng ký nó, khởi động lò phản ứng bằng cách sử dụng reactor_run() trong đúng một phút, sau đó chúng tôi giải phóng tài nguyên và thoát khỏi chương trình.

Hiển thị http_server.c

#include "reactor.h"

static Reactor *reactor;

#include "common.h"

int main(void) {
    SAFE_CALL((reactor = reactor_new()), NULL);
    SAFE_CALL(
        reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL),
        -1);
    SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
    SAFE_CALL(reactor_destroy(reactor), -1);
}

Hãy kiểm tra xem mọi thứ có hoạt động như mong đợi không. Biên dịch (chmod a+x compile.sh && ./compile.sh trong thư mục gốc của dự án) và khởi chạy máy chủ tự viết, mở http://127.0.0.1:18470 trong trình duyệt và xem những gì chúng tôi mong đợi:

Lò phản ứng I/O trần C đầy đủ tính năng

Đo lường hiệu suất

Hiển thị thông số kỹ thuật xe của tôi

$ screenfetch
 MMMMMMMMMMMMMMMMMMMMMMMMMmds+.        OS: Mint 19.1 tessa
 MMm----::-://////////////oymNMd+`     Kernel: x86_64 Linux 4.15.0-20-generic
 MMd      /++                -sNMd:    Uptime: 2h 34m
 MMNso/`  dMM    `.::-. .-::.` .hMN:   Packages: 2217
 ddddMMh  dMM   :hNMNMNhNMNMNh: `NMm   Shell: bash 4.4.20
     NMm  dMM  .NMN/-+MMM+-/NMN` dMM   Resolution: 1920x1080
     NMm  dMM  -MMm  `MMM   dMM. dMM   DE: Cinnamon 4.0.10
     NMm  dMM  -MMm  `MMM   dMM. dMM   WM: Muffin
     NMm  dMM  .mmd  `mmm   yMM. dMM   WM Theme: Mint-Y-Dark (Mint-Y)
     NMm  dMM`  ..`   ...   ydm. dMM   GTK Theme: Mint-Y [GTK2/3]
     hMM- +MMd/-------...-:sdds  dMM   Icon Theme: Mint-Y
     -NMm- :hNMNNNmdddddddddy/`  dMM   Font: Noto Sans 9
      -dMNs-``-::::-------.``    dMM   CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
       `/dMNmy+/:-------------:/yMMM   GPU: NV136
          ./ydNMMMMMMMMMMMMMMMMMMMMM   RAM: 2544MiB / 7926MiB
             .MMMMMMMMMMMMMMMMMMM

Hãy đo hiệu suất của một máy chủ đơn luồng. Hãy mở hai thiết bị đầu cuối: trong một thiết bị đầu cuối chúng tôi sẽ chạy ./http_server, theo một cách khác - công việc. Sau một phút, số liệu thống kê sau sẽ được hiển thị trong thiết bị đầu cuối thứ hai:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   493.52us   76.70us  17.31ms   89.57%
    Req/Sec    24.37k     1.81k   29.34k    68.13%
  11657769 requests in 1.00m, 1.60GB read
Requests/sec: 193974.70
Transfer/sec:     27.19MB

Máy chủ đơn luồng của chúng tôi có thể xử lý hơn 11 triệu yêu cầu mỗi phút bắt nguồn từ 100 kết nối. Kết quả không tệ nhưng liệu nó có thể cải thiện được không?

Máy chủ đa luồng

Như đã đề cập ở trên, bộ phản ứng I/O có thể được tạo trong các luồng riêng biệt, nhờ đó tận dụng được tất cả các lõi CPU. Hãy áp dụng phương pháp này vào thực tế:

Hiển thị http_server_multithreaded.c

#include "reactor.h"

static Reactor *reactor;
#pragma omp threadprivate(reactor)

#include "common.h"

int main(void) {
#pragma omp parallel
    {
        SAFE_CALL((reactor = reactor_new()), NULL);
        SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN,
                                   on_accept, NULL),
                  -1);
        SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
        SAFE_CALL(reactor_destroy(reactor), -1);
    }
}

Bây giờ mỗi chủ đề sở hữu của riêng mình lò phản ứng:

static Reactor *reactor;
#pragma omp threadprivate(reactor)

Xin lưu ý rằng đối số hàm new_server() những người ủng hộ true. Điều này có nghĩa là chúng tôi gán tùy chọn cho ổ cắm máy chủ SO_REUSEPORTđể sử dụng nó trong môi trường đa luồng. Bạn có thể đọc thêm chi tiết đây.

Lần chạy thứ hai

Bây giờ hãy đo hiệu suất của máy chủ đa luồng:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.14ms    2.53ms  40.73ms   89.98%
    Req/Sec    79.98k    18.07k  154.64k    78.65%
  38208400 requests in 1.00m, 5.23GB read
Requests/sec: 635876.41
Transfer/sec:     89.14MB

Số lượng yêu cầu được xử lý trong 1 phút đã tăng ~3.28 lần! Nhưng chúng tôi chỉ còn thiếu ~XNUMX triệu so với con số tròn, vì vậy hãy cố gắng khắc phục điều đó.

Trước tiên hãy nhìn vào số liệu thống kê được tạo ra perf:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         1 813 074      context-switches          #    0,007 M/sec                  
             4 689      cpu-migrations            #    0,019 K/sec                  
               254      page-faults               #    0,001 K/sec                  
   895 324 830 170      cycles                    #    3,693 GHz                    
   621 378 066 808      instructions              #    0,69  insn per cycle         
   119 926 709 370      branches                  #  494,653 M/sec                  
     3 227 095 669      branch-misses             #    2,69% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

Sử dụng mối quan hệ CPU, biên soạn với -march=native, PGO, số lượng truy cập tăng lên bộ nhớ đệm, tăng MAX_EVENTS Và sử dụng EPOLLET không mang lại sự gia tăng đáng kể về hiệu suất. Nhưng điều gì sẽ xảy ra nếu bạn tăng số lượng kết nối đồng thời?

Thống kê cho 352 kết nối đồng thời:

$ wrk -c352 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 352 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec:     93.34MB

Đã thu được kết quả mong muốn và kèm theo đó là một biểu đồ thú vị cho thấy sự phụ thuộc của số lượng yêu cầu được xử lý trong 1 phút vào số lượng kết nối:

Lò phản ứng I/O trần C đầy đủ tính năng

Chúng tôi thấy rằng sau vài trăm kết nối, số lượng yêu cầu được xử lý cho cả hai máy chủ giảm mạnh (trong phiên bản đa luồng, điều này dễ nhận thấy hơn). Điều này có liên quan đến việc triển khai ngăn xếp TCP/IP của Linux không? Vui lòng viết các giả định của bạn về hành vi này của biểu đồ và cách tối ưu hóa cho các tùy chọn đa luồng và đơn luồng trong phần nhận xét.

làm sao lưu ý trong phần nhận xét, bài kiểm tra hiệu năng này không hiển thị hoạt động của bộ phản ứng I/O dưới tải thực, bởi vì hầu như máy chủ luôn tương tác với cơ sở dữ liệu, xuất nhật ký, sử dụng mật mã với TLS v.v., do đó tải trở nên không đồng nhất (động). Các thử nghiệm cùng với các thành phần của bên thứ ba sẽ được thực hiện trong bài viết về thước đo I/O.

Nhược điểm của lò phản ứng I/O

Bạn cần hiểu rằng lò phản ứng I/O không phải không có nhược điểm, cụ thể là:

  • Việc sử dụng bộ phản ứng I/O trong môi trường đa luồng có phần khó khăn hơn, bởi vì bạn sẽ phải quản lý các luồng theo cách thủ công.
  • Thực tế cho thấy rằng trong hầu hết các trường hợp, tải không đồng đều, điều này có thể dẫn đến việc một luồng ghi nhật ký trong khi luồng khác đang bận làm việc.
  • Nếu một trình xử lý sự kiện chặn một luồng, thì chính bộ chọn hệ thống cũng sẽ chặn, điều này có thể dẫn đến các lỗi khó tìm.

Giải quyết những vấn đề này thước đo I/O, thường có bộ lập lịch phân bổ tải đồng đều cho một nhóm luồng và cũng có API thuận tiện hơn. Chúng ta sẽ nói về nó sau, trong bài viết khác của tôi.

Kết luận

Đây là nơi kết thúc hành trình của chúng tôi từ lý thuyết đến ống xả profiler.

Bạn không nên tập trung vào vấn đề này vì có nhiều cách tiếp cận thú vị không kém khác để viết phần mềm mạng với mức độ tiện lợi và tốc độ khác nhau. Thật thú vị, theo ý kiến ​​​​của tôi, các liên kết được đưa ra dưới đây.

Cho đến lần sau!

Dự án thú vị

Tôi nên đọc gì nữa?

Nguồn: www.habr.com

Thêm một lời nhận xét