Plenfunkcia nuda-C I/O-reaktoro

Plenfunkcia nuda-C I/O-reaktoro

Enkonduko

I/O-reaktoro (ununura fadena evento buklo) estas ŝablono por verki altŝarĝan programaron, uzata en multaj popularaj solvoj:

En ĉi tiu artikolo, ni rigardos la enojn de I/O-reaktoro kaj kiel ĝi funkcias, skribos efektivigon en malpli ol 200 linioj de kodo, kaj faros simplan HTTP-servilan procezon pli ol 40 milionoj da petoj/min.

Antaŭparolo

  • La artikolo estis skribita por helpi kompreni la funkciadon de la I/O-reaktoro, kaj tial kompreni la riskojn kiam oni uzas ĝin.
  • Por kompreni la artikolon necesas kono de la bazaĵoj. C lingvo kaj iom da sperto en reto-aplika disvolviĝo.
  • Ĉiu kodo estas skribita en C lingvo strikte laŭ (singardo: longa PDF) al C11-normo por Linukso kaj havebla sur GitHub.

Kial tio estas necesa?

Kun la kreskanta populareco de la Interreto, retserviloj komencis bezoni pritrakti grandan nombron da konektoj samtempe, kaj tial du aliroj estis provitaj: blokado de I/O sur granda nombro da OS-fadenoj kaj ne-blokanta I/O en kombinaĵo kun eventa sciiga sistemo, ankaŭ nomita "sistemelektilo" (epollo/kqueue/IOCP/ktp).

La unua aliro implikis krei novan OS-fadenon por ĉiu envenanta konekto. Ĝia malavantaĝo estas malbona skaleblo: la operaciumo devos efektivigi multajn kunteksttransiroj и sistemaj vokoj. Ili estas multekostaj operacioj kaj povas konduki al manko de libera RAM kun impona nombro da konektoj.

La modifita versio elstaras fiksita nombro da fadenoj (fadena aro), tiel malhelpante la sistemon ĉesigi la ekzekuton, sed samtempe enkondukante novan problemon: se fadena aro estas nuntempe blokita de longaj legadoj, tiam aliaj ingoj, kiuj jam kapablas ricevi datumojn, ne povos faru tion.

La dua aliro uzas sistemo de sciigo de evento (sistema elektilo) provizita de la OS. Ĉi tiu artikolo diskutas la plej oftan specon de sistemelektilo, bazita sur atentigoj (okazaĵoj, sciigoj) pri preteco por I/O-operacioj, prefere ol sur sciigoj pri ilia kompletigo. Simpligita ekzemplo de ĝia uzo povas esti reprezentita per la sekva blokdiagramo:

Plenfunkcia nuda-C I/O-reaktoro

La diferenco inter ĉi tiuj aliroj estas kiel sekvas:

  • Blokado de I/O-operacioj interrompi uzantfluo ĝisĝis la OS estas ĝuste defragmentas alvenanta IP-pakoj al bajta fluo (TCP, ricevante datumojn) aŭ ne estos sufiĉe da spaco disponebla en la internaj skribbufroj por posta sendado per NENIO (sendo de datumoj).
  • Sistemelektilo kun la tempo sciigas al la programo ke la OS jam defragmentitaj IP-pakaĵoj (TCP, datumricevo) aŭ sufiĉe da spaco en internaj skribbufroj jam disponebla (senddatenoj).

Resume, rezervi OS-fadenon por ĉiu I/O estas malŝparo de komputika potenco, ĉar fakte, la fadenoj ne faras utilan laboron (tial la esprimo "programa interrompo"). La sistemelektilo solvas ĉi tiun problemon, permesante al la uzantprogramo uzi CPU-resursojn multe pli ekonomie.

I/O reaktormodelo

La I/O-reaktoro funkcias kiel tavolo inter la sistemelektilo kaj la uzantkodo. La principo de ĝia funkciado estas priskribita per la sekva blokdiagramo:

Plenfunkcia nuda-C I/O-reaktoro

  • Mi memorigu vin, ke evento estas sciigo, ke certa ingo kapablas fari ne-blokan I/O-operacion.
  • Okazaĵtraktilo estas funkcio vokita de la I/O-reaktoro kiam okazaĵo estas ricevita, kiu tiam elfaras ne-blokan I/O-operacion.

Gravas noti, ke la I/O-reaktoro estas laŭdifine unufadena, sed nenio malhelpas la koncepton esti uzata en plurfadena medio je proporcio de 1 fadeno: 1 reaktoro, tiel reciklante ĉiujn CPU-kernojn.

Реализация

Ni metos la publikan interfacon en dosieron reactor.h, kaj efektivigo - en reactor.c. reactor.h konsistos el la jenaj anoncoj:

Montru deklarojn en reaktoro.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);

La I/O-reaktorstrukturo konsistas el dosiera priskribilo elektilo epollo и hashtabloj GHashTable, kiu mapas ĉiun ingon al CallbackData (strukturo de okazaĵtraktilo kaj uzantargumento por ĝi).

Montru Reaktoron kaj CallbackData

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

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

Bonvolu noti, ke ni ebligis la kapablon pritrakti nekompleta tipo laŭ la indekso. EN reactor.h ni deklaras la strukturon reactorkaj en reactor.c ni difinas ĝin, tiel malhelpante la uzanton eksplicite ŝanĝi ĝiajn kampojn. Ĉi tiu estas unu el la ŝablonoj kaŝante datumojn, kiu koncize kongruas en C-semantiko.

Funkcioj reactor_register, reactor_deregister и reactor_reregister ĝisdatigi la liston de interesaj ingoj kaj respondaj evento-traktiloj en la sistema elektilo kaj hashtabelo.

Montru registrajn funkciojn

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

Post kiam la I/O-reaktoro kaptis la okazaĵon kun la priskribilo fd, ĝi vokas la respondan okazaĵtraktilon, al kiu ĝi pasas fd, bit masko generitaj eventoj kaj uzanta montrilo al void.

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

Por resumi, la ĉeno de funkciovokoj en uzantkodo prenos la sekvan formon:

Plenfunkcia nuda-C I/O-reaktoro

Unufadena servilo

Por testi la I/O-reaktoron sub alta ŝarĝo, ni skribos simplan HTTP-retservilon, kiu respondas al ajna peto per bildo.

Rapida referenco al la HTTP-protokolo

HTTP - jen la protokolo aplika nivelo, ĉefe uzata por interago de servilo-retumilo.

HTTP povas esti facile uzata transporto protokolo TCP, sendante kaj ricevante mesaĝojn en specifita formato specifo.

Formato de Peto

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

  • CRLF estas sinsekvo de du signoj: r и n, apartigante la unuan linion de la peto, kapliniojn kaj datumojn.
  • <КОМАНДА> - unu el CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. La retumilo sendos komandon al nia servilo GET, signifante "Sendu al mi la enhavon de la dosiero."
  • <URI> - unuforma rimeda identigilo. Ekzemple, se URI = /index.html, tiam la kliento petas la ĉefpaĝon de la retejo.
  • <ВЕРСИЯ HTTP> — versio de la HTTP-protokolo en la formato HTTP/X.Y. La plej ofte uzata versio hodiaŭ estas HTTP/1.1.
  • <ЗАГОЛОВОК N> estas ŝlosil-valora paro en la formato <КЛЮЧ>: <ЗНАЧЕНИЕ>, sendita al la servilo por plia analizo.
  • <ДАННЫЕ> — datumoj postulataj de la servilo por plenumi la operacion. Ofte ĝi estas simpla JSON aŭ ajna alia formato.

Responda Formato

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

  • <КОД СТАТУСА> estas nombro reprezentanta la rezulton de la operacio. Nia servilo ĉiam redonos statuson 200 (sukcesa operacio).
  • <ОПИСАНИЕ СТАТУСА> — ĉenprezento de la statuskodo. Por statuskodo 200 ĉi tio estas OK.
  • <ЗАГОЛОВОК N> — kaplinio de la sama formato kiel en la peto. Ni resendos la titolojn Content-Length (dosiergrandeco) kaj Content-Type: text/html (revena datumtipo).
  • <ДАННЫЕ> - datumoj petitaj de la uzanto. En nia kazo, ĉi tiu estas la vojo al la bildo en HTML.

dosiero http_server.c (ununura fadenigita servilo) inkluzivas dosieron common.h, kiu enhavas la sekvajn funkcioprototipojn:

Montri funkcioprototipojn komune.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);

La funkcia makroo ankaŭ estas priskribita SAFE_CALL() kaj la funkcio estas difinita fail(). La makroo komparas la valoron de la esprimo kun la eraro, kaj se la kondiĉo estas vera, vokas la funkcion fail():

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

funkcio fail() presas la pasigitajn argumentojn al la terminalo (kiel printf()) kaj finas la programon per la kodo 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);
}

funkcio new_server() resendas la dosierpriskribilon de la "servilo" ingo kreita per sistemaj vokoj socket(), bind() и listen() kaj kapabla akcepti alvenantajn ligojn en ne-bloka reĝimo.

Montru new_server() funkcion

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

  • Notu ke la ingo estas komence kreita en ne-bloka reĝimo uzante la flagon SOCK_NONBLOCKtiel ke en la funkcio on_accept() (legu pli) sistemvoko accept() ne haltigis la fadenekzekuton.
  • se reuse_port estas egala true, tiam ĉi tiu funkcio agordos la ingon kun la opcio SO_REUSEPORT tra setsockopt()uzi la saman havenon en multfadena medio (vidu sekcion "Multfadena servilo").

Eventa Prizorganto on_accept() vokita post kiam la OS generas eventon EPOLLIN, ĉi-kaze signifante ke la nova konekto povas esti akceptita. on_accept() akceptas novan konekton, ŝanĝas ĝin al ne-bloka reĝimo kaj registras kun evento-traktilo on_recv() en I/O-reaktoro.

Montru on_accept() funkcion

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

Eventa Prizorganto on_recv() vokita post kiam la OS generas eventon EPOLLIN, en ĉi tiu kazo signifante ke la konekto registrita on_accept(), preta ricevi datumojn.

on_recv() legas datumojn de la konekto ĝis la HTTP-peto estas tute ricevita, tiam ĝi registras pritraktilon on_send() por sendi HTTP-respondon. Se la kliento rompas la konekton, la ingo estas deregistrita kaj fermita uzante close().

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

Eventa Prizorganto on_send() vokita post kiam la OS generas eventon EPOLLOUT, signifante ke la konekto registrita on_recv(), preta sendi datumojn. Ĉi tiu funkcio sendas HTTP-respondon enhavantan HTML kun bildo al la kliento kaj poste ŝanĝas la okazaĵan pritraktilon reen al on_recv().

Montru on_send() funkcion

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

Kaj fine, en la dosiero http_server.c, en funkcio main() ni kreas I/O-reaktoron uzante reactor_new(), kreu servila ingo kaj registri ĝin, eku la reaktoron reactor_run() dum ekzakte unu minuto, kaj poste ni liberigas rimedojn kaj eliras la programon.

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

Ni kontrolu, ke ĉio funkcias kiel atendite. Kompilado (chmod a+x compile.sh && ./compile.sh en la projekta radiko) kaj lanĉu la memskribitan servilon, malfermu http://127.0.0.1:18470 en la retumilo kaj vidu, kion ni atendis:

Plenfunkcia nuda-C I/O-reaktoro

Mezurado de rendimento

Montru miajn aŭtospecifojn

$ 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

Ni mezuru la rendimenton de unu-fadena servilo. Ni malfermu du terminalojn: en unu ni kuros ./http_server, en malsama - verko. Post minuto, la sekvaj statistikoj estos montrataj en la dua terminalo:

$ 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

Nia unufadena servilo povis procesi pli ol 11 milionojn da petoj je minuto devenantaj de 100 konektoj. Ne malbona rezulto, sed ĉu ĝi povas esti plibonigita?

Plurfadena servilo

Kiel menciite supre, la I/O-reaktoro povas esti kreita en apartaj fadenoj, tiel utiligante ĉiujn CPU-kernojn. Ni praktiku ĉi tiun aliron:

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

Nun ĉiu fadeno posedas sian propran reaktoro:

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

Bonvolu noti, ke la funkcio argumento new_server() favoroj true. Ĉi tio signifas, ke ni atribuas la opcion al la servila ingo SO_REUSEPORTuzi ĝin en multfadena medio. Vi povas legi pliajn detalojn tie.

Dua kuro

Nun ni mezuru la rendimenton de plurfadena servilo:

$ 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

La nombro da petoj procesitaj en 1 minuto pliiĝis je ~3.28 fojojn! Sed al ni nur ~XNUMX milionoj mankis la ronda nombro, do ni provu ripari tion.

Unue ni rigardu la statistikojn generitajn perfekta:

$ 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

Uzante CPU-Afinecon, kompilo kun -march=native, OGP, pliiĝo en la nombro da sukcesoj kaŝaĵo, pliigas MAX_EVENTS kaj uzo EPOLLET ne donis signifan pliiĝon en rendimento. Sed kio okazas se vi pliigas la nombron da samtempaj konektoj?

Statistiko por 352 samtempaj konektoj:

$ 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

La dezirata rezulto estis akirita, kaj kun ĝi interesa grafikaĵo montranta la dependecon de la nombro da procesitaj petoj en 1 minuto de la nombro da konektoj:

Plenfunkcia nuda-C I/O-reaktoro

Ni vidas, ke post kelkaj cent konektoj, la nombro da prilaboritaj petoj por ambaŭ serviloj akre malpliiĝas (en la multfadena versio tio estas pli rimarkebla). Ĉu ĉi tio rilatas al la Linukso TCP/IP-staka efektivigo? Bonvolu skribi viajn supozojn pri ĉi tiu konduto de la grafikaĵo kaj optimumigoj por multfadenaj kaj unufadenaj opcioj en la komentoj.

Kiel notis en la komentoj, ĉi tiu agado-testo ne montras la konduton de la I/O-reaktoro sub realaj ŝarĝoj, ĉar preskaŭ ĉiam la servilo interagas kun la datumbazo, eligas protokolojn, uzas kriptografion kun TLS ktp., sekve de kio la ŝarĝo fariĝas neunuforma (dinamika). Testoj kune kun triaj komponantoj estos faritaj en la artikolo pri la I/O-proactor.

Malavantaĝoj de I/O-reaktoro

Vi devas kompreni, ke la I/O-reaktoro ne estas sen siaj malavantaĝoj, nome:

  • Uzi I/O-reaktoron en multfadena medio estas iom pli malfacila, ĉar vi devos mane administri la fluojn.
  • Praktiko montras, ke plejofte la ŝarĝo estas ne-unuforma, kio povas konduki al unu fadendehakado dum alia estas okupata de laboro.
  • Se unu okazaĵa prizorganto blokas fadenon, tiam la sistemelektilo mem ankaŭ blokos, kio povas konduki al malfacile troveblaj cimoj.

Solvas ĉi tiujn problemojn I/O-proactor, kiu ofte havas planilon, kiu egale distribuas la ŝarĝon al aro da fadenoj, kaj ankaŭ havas pli oportunan API. Pri tio ni parolos poste, en mia alia artikolo.

konkludo

Ĉi tie finiĝis nia vojaĝo de teorio rekte al la profilila ellasilo.

Vi ne devus deteni ĉi tion, ĉar ekzistas multaj aliaj same interesaj aliroj por verki retprogramaron kun malsamaj niveloj de oportuno kaj rapideco. Interesaj, laŭ mi, ligiloj estas donitaj sube.

Ĝis revido!

Interesaj projektoj

Kion alian mi legu?

fonto: www.habr.com

Aldoni komenton