Full-feature na bare-C I/O reactor

Full-feature na bare-C I/O reactor

Pagpapakilala

I/O reactor (iisang sinulid loop ng kaganapan) ay isang pattern para sa pagsulat ng high-load na software, na ginagamit sa maraming sikat na solusyon:

Sa artikulong ito, titingnan natin ang mga ins at out ng isang I/O reactor at kung paano ito gumagana, magsulat ng pagpapatupad sa mas mababa sa 200 linya ng code, at gagawa ng isang simpleng proseso ng HTTP server na higit sa 40 milyong kahilingan/min.

paunang salita

  • Ang artikulo ay isinulat upang makatulong na maunawaan ang paggana ng I/O reactor, at samakatuwid ay maunawaan ang mga panganib kapag ginagamit ito.
  • Ang kaalaman sa mga pangunahing kaalaman ay kinakailangan upang maunawaan ang artikulo. C wika at ilang karanasan sa pagbuo ng aplikasyon sa network.
  • Ang lahat ng code ay nakasulat sa wikang C nang mahigpit ayon sa (pag-iingat: mahabang PDF) sa pamantayan ng C11 para sa Linux at magagamit sa GitHub.

Bakit ito gawin?

Sa lumalagong katanyagan ng Internet, nagsimula ang mga web server na hawakan ang isang malaking bilang ng mga koneksyon nang sabay-sabay, at samakatuwid ay sinubukan ang dalawang diskarte: pagharang sa I/O sa isang malaking bilang ng mga OS thread at hindi pagharang sa I/O kasama ng isang system ng notification ng kaganapan, na tinatawag ding “system selector” (epoll/kqueue/IOCP/etc).

Kasama sa unang diskarte ang paglikha ng bagong OS thread para sa bawat papasok na koneksyon. Ang kawalan nito ay mahinang scalability: ang operating system ay kailangang ipatupad ang marami mga pagbabago sa konteksto и mga tawag sa system. Ang mga ito ay mamahaling operasyon at maaaring humantong sa kakulangan ng libreng RAM na may kahanga-hangang bilang ng mga koneksyon.

Ang binagong bersyon ay nagha-highlight nakapirming bilang ng mga thread (thread pool), sa gayon ay pinipigilan ang system na i-abort ang pagpapatupad, ngunit sa parehong oras ay nagpapakilala ng isang bagong problema: kung ang isang thread pool ay kasalukuyang na-block ng matagal na mga operasyon sa pagbabasa, kung gayon ang ibang mga socket na nakakatanggap na ng data ay hindi magagawang gawin mo.

Ginagamit ng pangalawang diskarte sistema ng abiso ng kaganapan (system selector) na ibinigay ng OS. Tinatalakay ng artikulong ito ang pinakakaraniwang uri ng tagapili ng system, batay sa mga alerto (mga kaganapan, notification) tungkol sa kahandaan para sa mga operasyon ng I/O, sa halip na sa mga abiso tungkol sa kanilang pagkumpleto. Ang isang pinasimpleng halimbawa ng paggamit nito ay maaaring katawanin ng sumusunod na block diagram:

Full-feature na bare-C I/O reactor

Ang pagkakaiba sa pagitan ng mga pamamaraang ito ay ang mga sumusunod:

  • Pag-block sa mga operasyon ng I/O suspindihin daloy ng gumagamit hangganghanggang sa maayos ang OS mga defragment papasok Mga IP packet sa byte stream (TCP, pagtanggap ng data) o walang sapat na espasyong magagamit sa mga panloob na write buffer para sa kasunod na pagpapadala sa pamamagitan ng NIC (pagpapadala ng data).
  • Tagapili ng system sa paglipas ng panahon Inaabisuhan ang program na ang OS na mga defragmented na IP packet (TCP, pagtanggap ng data) o sapat na espasyo sa mga internal na write buffer na magagamit (nagpapadala ng data).

Sa kabuuan, ang pagreserba ng OS thread para sa bawat I/O ay isang pag-aaksaya ng kapangyarihan sa pag-compute, dahil sa katotohanan, ang mga thread ay hindi gumagawa ng kapaki-pakinabang na gawain (kaya ang termino "naantala ang software"). Niresolba ng system selector ang problemang ito, na nagpapahintulot sa user program na gumamit ng CPU resources nang mas matipid.

I/O reactor model

Ang I/O reactor ay gumaganap bilang isang layer sa pagitan ng system selector at ng user code. Ang prinsipyo ng pagpapatakbo nito ay inilarawan ng sumusunod na block diagram:

Full-feature na bare-C I/O reactor

  • Hayaan mong ipaalala ko sa iyo na ang isang kaganapan ay isang abiso na ang isang partikular na socket ay makakagawa ng isang hindi nakaharang na operasyon ng I/O.
  • Ang event handler ay isang function na tinatawag ng I/O reactor kapag natanggap ang isang event, na pagkatapos ay nagsasagawa ng non-blocking I/O operation.

Mahalagang tandaan na ang I/O reactor ay sa pamamagitan ng kahulugan na single-threaded, ngunit walang makakapigil sa konsepto na magamit sa isang multi-threaded na kapaligiran sa ratio na 1 thread: 1 reactor, sa gayon ay nire-recycle ang lahat ng CPU core.

Pagpapatupad

Ilalagay namin ang pampublikong interface sa isang file reactor.h, at pagpapatupad - sa reactor.c. reactor.h ay binubuo ng mga sumusunod na anunsyo:

Ipakita ang mga deklarasyon sa 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);

Ang istraktura ng I/O reactor ay binubuo ng deskriptor ng file tagapili epoll и mga talahanayan ng hash GHashTable, na nagmamapa sa bawat socket CallbackData (istraktura ng isang tagapangasiwa ng kaganapan at isang argumento ng gumagamit para dito).

Ipakita ang Reactor at CallbackData

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

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

Pakitandaan na pinagana namin ang kakayahang pangasiwaan hindi kumpletong uri ayon sa index. SA reactor.h ipinapahayag namin ang istraktura reactorat sa reactor.c tinutukoy namin ito, sa gayon ay pinipigilan ang user na tahasang baguhin ang mga field nito. Ito ay isa sa mga pattern pagtatago ng data, na akma sa C semantics.

Pag-andar reactor_register, reactor_deregister и reactor_reregister i-update ang listahan ng mga socket ng interes at kaukulang mga tagapangasiwa ng kaganapan sa tagapili ng system at talahanayan ng hash.

Ipakita ang mga function ng pagpaparehistro

#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;
}

Matapos ma-intercept ng I/O reactor ang kaganapan gamit ang descriptor fd, tinatawag nito ang kaukulang tagapangasiwa ng kaganapan, kung saan ito pumasa fd, medyo mask nabuong mga kaganapan at isang pointer ng user sa void.

Ipakita ang reactor_run() function

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;
}

Upang buod, ang chain ng mga function na tawag sa user code ay kukuha ng sumusunod na anyo:

Full-feature na bare-C I/O reactor

Single threaded server

Upang masubukan ang I/O reactor sa ilalim ng mataas na load, magsusulat kami ng isang simpleng HTTP web server na tumutugon sa anumang kahilingan gamit ang isang imahe.

Isang mabilis na sanggunian sa HTTP protocol

HTTP - ito ang protocol antas ng aplikasyon, pangunahing ginagamit para sa pakikipag-ugnayan ng server-browser.

Madaling magamit ang HTTP transportasyon protocol TCP, pagpapadala at pagtanggap ng mga mensahe sa isang tinukoy na format pagtutukoy.

Format ng Kahilingan

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

  • CRLF ay isang sequence ng dalawang character: r и n, na naghihiwalay sa unang linya ng kahilingan, mga header at data.
  • <КОМАНДА> - isa sa CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Magpapadala ang browser ng command sa aming server GET, ibig sabihin ay "Ipadala sa akin ang mga nilalaman ng file."
  • <URI> - pare-parehong tagatukoy ng mapagkukunan. Halimbawa, kung URI = /index.html, pagkatapos ay hihilingin ng kliyente ang pangunahing pahina ng site.
  • <ВЕРСИЯ HTTP> — bersyon ng HTTP protocol sa format HTTP/X.Y. Ang pinakakaraniwang ginagamit na bersyon ngayon ay HTTP/1.1.
  • <ЗАГОЛОВОК N> ay isang key-value pair sa format <КЛЮЧ>: <ЗНАЧЕНИЕ>, ipinadala sa server para sa karagdagang pagsusuri.
  • <ДАННЫЕ> — data na kinakailangan ng server upang maisagawa ang operasyon. Kadalasan ito ay simple JSON o anumang iba pang format.

Format ng Tugon

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

  • <КОД СТАТУСА> ay isang numero na kumakatawan sa resulta ng operasyon. Palaging ibabalik ng aming server ang status 200 (matagumpay na operasyon).
  • <ОПИСАНИЕ СТАТУСА> — string na representasyon ng status code. Para sa status code 200 ito ay OK.
  • <ЗАГОЛОВОК N> — header ng parehong format tulad ng sa kahilingan. Ibabalik namin ang mga pamagat Content-Length (laki ng file) at Content-Type: text/html (ibalik ang uri ng data).
  • <ДАННЫЕ> — data na hiniling ng user. Sa aming kaso, ito ang landas patungo sa larawan sa HTML.

talaksan http_server.c (iisang sinulid na server) ay may kasamang file common.h, na naglalaman ng mga sumusunod na prototype ng function:

Ipakita ang mga prototype ng function na magkakatulad.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);

Inilarawan din ang functional macro SAFE_CALL() at ang pag-andar ay tinukoy fail(). Inihahambing ng macro ang halaga ng expression sa error, at kung totoo ang kundisyon, tatawagin ang function fail():

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

Tungkulin fail() nagpi-print ng mga naipasa na argumento sa terminal (tulad ng printf()) at tinatapos ang programa gamit ang code 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);
}

Tungkulin new_server() ibinabalik ang file descriptor ng "server" socket na ginawa ng mga system call socket(), bind() и listen() at may kakayahang tumanggap ng mga papasok na koneksyon sa isang non-blocking mode.

Ipakita ang new_server() function

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;
}

  • Tandaan na ang socket ay unang ginawa sa non-blocking mode gamit ang flag SOCK_NONBLOCKupang sa pag-andar on_accept() (magbasa pa) system call accept() hindi napigilan ang pag-execute ng thread.
  • Kung reuse_port ay pantay sa true, pagkatapos ay iko-configure ng function na ito ang socket na may opsyon SO_REUSEPORT sa pamamagitan ng setsockopt()upang gamitin ang parehong port sa isang multi-threaded na kapaligiran (tingnan ang seksyong "Multi-threaded server").

Tagapangasiwa ng Kaganapan on_accept() tinatawag pagkatapos bumuo ng isang kaganapan ang OS EPOLLIN, sa kasong ito ay nangangahulugan na ang bagong koneksyon ay maaaring tanggapin. on_accept() tumatanggap ng bagong koneksyon, inililipat ito sa non-blocking mode at nagrerehistro sa isang event handler on_recv() sa isang I/O reactor.

Ipakita ang on_accept() function

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);
}

Tagapangasiwa ng Kaganapan on_recv() tinatawag pagkatapos bumuo ng isang kaganapan ang OS EPOLLIN, sa kasong ito ay nangangahulugan na ang koneksyon ay nakarehistro on_accept(), handang tumanggap ng data.

on_recv() nagbabasa ng data mula sa koneksyon hanggang sa ganap na natanggap ang kahilingan sa HTTP, pagkatapos ay nagrerehistro ito ng isang handler on_send() para magpadala ng HTTP na tugon. Kung masira ng kliyente ang koneksyon, ang socket ay aalisin sa pagkakarehistro at sarado gamit close().

Ipakita ang function na 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);
    }
}

Tagapangasiwa ng Kaganapan on_send() tinatawag pagkatapos bumuo ng isang kaganapan ang OS EPOLLOUT, ibig sabihin ay nakarehistro ang koneksyon on_recv(), handang magpadala ng data. Ang function na ito ay nagpapadala ng HTTP na tugon na naglalaman ng HTML na may larawan sa client at pagkatapos ay binago ang event handler pabalik sa on_recv().

Ipakita ang on_send() function

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);
}

At sa wakas, sa file http_server.c, sa paggana main() gumagawa kami ng I/O reactor gamit reactor_new(), lumikha ng socket ng server at irehistro ito, simulan ang paggamit ng reaktor reactor_run() para sa eksaktong isang minuto, at pagkatapos ay naglalabas kami ng mga mapagkukunan at lumabas sa programa.

Ipakita ang 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);
}

Suriin natin na gumagana ang lahat gaya ng inaasahan. Pinagsasama-sama (chmod a+x compile.sh && ./compile.sh sa ugat ng proyekto) at ilunsad ang self-written server, buksan http://127.0.0.1:18470 sa browser at tingnan kung ano ang inaasahan namin:

Full-feature na bare-C I/O reactor

Pagsusukat ng pagganap

Ipakita ang mga detalye ng aking sasakyan

$ 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

Sukatin natin ang pagganap ng isang single-threaded server. Buksan natin ang dalawang terminal: sa isa ay tatakbo tayo ./http_server, sa ibang- wrk. Pagkatapos ng isang minuto, ang mga sumusunod na istatistika ay ipapakita sa pangalawang terminal:

$ 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

Ang aming single-threaded server ay nakapagproseso ng mahigit 11 milyong kahilingan kada minuto na nagmula sa 100 koneksyon. Hindi masamang resulta, ngunit maaari ba itong mapabuti?

Multithreaded server

Tulad ng nabanggit sa itaas, ang I/O reactor ay maaaring gawin sa magkahiwalay na mga thread, sa gayon ay ginagamit ang lahat ng CPU core. Isagawa natin ang diskarteng ito:

Ipakita ang 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);
    }
}

Ngayon bawat thread nagmamay-ari ng kanyang sarili reaktor:

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

Pakitandaan na ang function argument new_server() tagapagtaguyod true. Nangangahulugan ito na itinalaga namin ang opsyon sa socket ng server SO_REUSEPORTupang gamitin ito sa isang multi-threaded na kapaligiran. Maaari mong basahin ang higit pang mga detalye dito.

Pangalawang pagtakbo

Ngayon, sukatin natin ang pagganap ng isang multi-threaded server:

$ 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

Ang bilang ng mga kahilingang naproseso sa loob ng 1 minuto ay tumaas ng ~3.28 beses! Ngunit ~XNUMX milyon lang ang kulang sa round number, kaya subukan nating ayusin iyon.

Tingnan muna natin ang nabuong mga istatistika perpekto:

$ 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

Gamit ang CPU Affinity, compilation with -march=native, PGO, isang pagtaas sa bilang ng mga hit cache, pagtaas MAX_EVENTS at gamitin EPOLLET hindi nagbigay ng makabuluhang pagtaas sa pagganap. Ngunit ano ang mangyayari kung dagdagan mo ang bilang ng mga sabay-sabay na koneksyon?

Mga istatistika para sa 352 sabay-sabay na koneksyon:

$ 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

Nakuha ang nais na resulta, at kasama nito ang isang kawili-wiling graph na nagpapakita ng pagtitiwala sa bilang ng mga naprosesong kahilingan sa loob ng 1 minuto sa bilang ng mga koneksyon:

Full-feature na bare-C I/O reactor

Nakikita namin na pagkatapos ng ilang daang mga koneksyon, ang bilang ng mga naprosesong kahilingan para sa parehong mga server ay bumaba nang husto (sa multi-threaded na bersyon ito ay mas kapansin-pansin). May kaugnayan ba ito sa pagpapatupad ng stack ng Linux TCP/IP? Huwag mag-atubiling isulat ang iyong mga pagpapalagay tungkol sa pag-uugaling ito ng graph at mga pag-optimize para sa multi-threaded at single-threaded na mga opsyon sa mga komento.

Bilang nabanggit sa mga komento, ang pagsubok sa pagganap na ito ay hindi nagpapakita ng pag-uugali ng I/O reactor sa ilalim ng mga totoong pagkarga, dahil halos palaging nakikipag-ugnayan ang server sa database, naglalabas ng mga log, gumagamit ng cryptography na may TLS atbp., bilang isang resulta kung saan ang pagkarga ay nagiging hindi pare-pareho (dynamic). Ang mga pagsubok kasama ang mga third-party na bahagi ay isasagawa sa artikulo tungkol sa I/O proactor.

Mga disadvantages ng I/O reactor

Kailangan mong maunawaan na ang I/O reactor ay walang mga kakulangan nito, lalo na:

  • Ang paggamit ng I/O reactor sa isang multi-threaded na kapaligiran ay medyo mas mahirap, dahil kakailanganin mong manu-manong pamahalaan ang mga daloy.
  • Ipinapakita ng pagsasanay na sa karamihan ng mga kaso ang load ay hindi pare-pareho, na maaaring humantong sa isang thread logging habang ang isa ay abala sa trabaho.
  • Kung na-block ng isang event handler ang isang thread, ang system selector mismo ay haharang din, na maaaring humantong sa mga bug na mahirap hanapin.

Lutasin ang mga problemang ito I/O proactor, na kadalasang may scheduler na pantay na namamahagi ng load sa isang pool ng mga thread, at mayroon ding mas maginhawang API. Pag-uusapan natin ito mamaya, sa aking iba pang artikulo.

Konklusyon

Dito natapos ang aming paglalakbay mula sa teorya diretso sa profiler exhaust.

Hindi mo dapat pag-isipan ito, dahil maraming iba pang kawili-wiling mga diskarte sa pagsulat ng software ng network na may iba't ibang antas ng kaginhawahan at bilis. Kawili-wili, sa aking opinyon, ang mga link ay ibinigay sa ibaba.

Nakikita mo ulit!

Mga kawili-wiling proyekto

Ano pa ba ang dapat kong basahin?

Pinagmulan: www.habr.com

Magdagdag ng komento