Täisfunktsionaalne I/O reaktor puhtal C-l

Täisfunktsionaalne I/O reaktor puhtal C-l

Sissejuhatus

I/O reaktor (üheline sündmusetsükkel) — see on musternäidis kõrge koormusega tarkvara kirjutamiseks, mida kasutatakse paljudes tuntud lahendustes:

Käesolevas artiklis vaatleme I/O reaktori aluseid ja tööpõhimõtteid, kirjutame rakenduse vähem kui 200 koodi reaga ja paneme lihtsa HTTP serveri töötlema üle 40 miljoni päringu/min.

Eessõna

  • Artikli eesmärk on aidata mõista I/O reaktori toimimist ja sellest tulenevaid riske selle kasutamisel.
  • Artikli mõistmiseks on vajalik teadmised C keele ja väike kogemus võrgurakenduste arendamisest.
  • Kogu kood on kirjutatud C keeles rangelt (ole ettevaatlik: pikk PDF) C11 standardi järgi Linuxis ja on saadaval aadressil GitHub.

Miks see vajalik on?

Interneti populaarsuse tõusuga on veebiserveritele vaja olnud samaaegselt töödelda suurt hulka ühendusi, millega seoses on katsetatud kaht lähenemist: blokeeriv I/O suurte OS-i lõimede arvuga ja mitteblokeeriv I/O koos sündmuste teavitamis süsteemiga, mida nimetatakse ka „süsteemseks valijaks“ (epoll/kqueue/IOCP/etc).

Esimene lähenemine hõlmas iga sissetuleva ühenduse jaoks uue OS-i lõime loomist. Selle puuduseks on halb skaleeritavus: operatsioonisüsteem peab tegema palju kontekstivahetusi ja süsteemikõnesid. Need on kallid tegevused ja võivad põhjustada vabade RAM-i puudust suure hulga ühenduste korral.Muudetud versioon eraldab

fikseeritud arvu lõime (lõiupool), takistades seeläbi süsteemi juhuslikku töö lõpetamist, kuid samas toob see endaga kaasa uue probleemi: kui hetkel takistab lõimede kogu pikaajalisi lugemistoiminguid, ei saa teised sokid, mis on juba andmeid vastu võtma valmis, seda teha. Teine lähenemine kasutab

sündmuste teavitamissüsteemi (süsteemne valija), mida pakub operatsioonisüsteem. Käesolevas artiklis käsitletakse kõige sagedamini esinevat süsteemset valijat, mis põhineb teavitustel (sündmustel, teavitustel) I/O toimingute valmiduse osas, mitte toimingute lõpetamise teavitustel. Selle kasutamise lihtsustatud näidet on võimalik kujutada järgmise vooskeemiga:Mõlema lähenemise vahel on erinevus järgmine:

Täisfunktsionaalne I/O reaktor puhtal C-l

Blokeerivad I/O toimingud

  • peatavad kasutaja lõime пользовательский поток kuni AWS teeb sama asja ja ehitab selle ümber eduka äri, ja kui kliendid küsivad seda sõna otseses mõttes. Siiski tuleb teha teatavaid jõupingutusi, et sundida Google'it midagi toetama., kuni operatsioonisüsteem defragmenteerib saabuvad IP paketid bittide voolus (TCP, andmete vastuvõtt) või kui kirjutamise sisemistes puhvrites ei ole piisavalt ruumi edasise saatmise jaoks NIC (andmete saatmine).
  • Süsteemi valija mõne aja pärast teavitab programmi, et operatsioonisüsteem juba defragmenteeris IP pakette (TCP, andmete vastuvõtt) või et kirjutamise sisemistes puhvrites on piisavalt ruumi juba kergesti kergesti (andmete saatmine).

Kokkuvõtteks, operatsioonisüsteemi voogude reserveerimine iga I/O jaoks on arvutusvõimsuse raiskamine, kuna vood ei tegele tõeliselt kasuliku tööga (sealt tuleneb mõisted «programmiline katkestus»). Süsteemi valija lahendab selle probleemi, võimaldades kasutaja programmile kasutada CPU ressursse oluliselt efektiivsemalt.

I/O reaktori mudel

I/O reaktor toimib kihina süsteemi valija ja kasutaja koodi vahel. Selle tööprintsiipi kirjeldab järgmine vooskeem:

Täisfunktsionaalne I/O reaktor puhtal C-l

  • Kordan, et sündmus on teade, et teatud sokkel on valmis teostama mitteblokeerivat I/O operatsiooni.
  • Sündmuste töötleja on funktsioon, mille I/O reaktor kutsub esile sündmuse saamisel, mis seejärel sooritab mitteblokeeriva I/O operatsiooni.

Oluline on märkida, et I/O reaktor on määratletud ühe lõngana, kuid miski ei takista kasutamast kontsepti mitme lõnga keskkonnas seoses 1 lõng: 1 reaktor, kasutades seeläbi ära kõiki CPU südamikke.

Rakendamine

Avalik liides paigutame faili reactor.h, ja rakenduse reactor.c. reactor.h koosneb järgmistest deklareerimistest:

Kuva deklareerimised failis reactor.h

typedef struct reactor Reactor;

/*
 * Funktsioonipointer, mis kutsub esile I/O reaktori, kui SYS selektoril
 * on sündmus.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Tagastab `NULL`, kui viga, mitte-`NULL` pointer `Reactor`-ile
 * vastasel juhul.
 */
Reactor *reactor_new(void);

/*
 * Vabastab süsteemi selektori, kõik registreeritud soketid antud
 * hetkel ja I/O reaktori end.
 *
 * Järgmised funktsioonid tagastavad -1, kui viga, 0, kui õnnestus.
 */
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);

/*
 * Käivitab sündmuste tsükli ajavahemiku `timeout`-iga.
 *
 * See funktsioon edastab kontrolli kutsuvasse koodi, kui
 * ettenähtud aeg on lõppenud või kui registreeritud sokette ei ole.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

I/O reaktori struktuur koosneb faili deskriptori valijast epoll ja häshtabelist GHashTable, mis seondab iga soketi CallbackData (struktuur sündmuse töötleja ja selle jaoks kasutatava argumendi jaoks).

Kuva Reactor ja CallbackData

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

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

Pange tähele, et oleme kasutanud * osaliselt täidetud tüübi käitlemist * pointeriga. Me * deklareerime struktuuri * reactor, määratledes selle, takistades seeläbi kasutajat * seda otse muutmast. See on üks mustreid * andmete varjamise * , mis sobib hästi C semantikasse. reactor_register reactor_deregister reactor.h reactor_reregister uuendavad huvipakkuvate sokettide nimekirja ja vastavaid sündmuste töötlejat * süsteemi selektoris ja häshtabelis.toob kaasa selle, et "saadetamise" hetkel server ei ole veel käivitatud ja kõik läheb katki. reactor.c Kuva registreerimisfunktsioonid Pärast seda, kui I/O reaktor on sündmuse väljastanud deskriptorilt * , kutsub ta vastava sündmuse töötleja esile, edastades *bitimaski * genereeritud sündmustest ja kasutaja pointeri *

Funktsioonid Kuva reactor_run() funktsioon, Pange tähele, et oleme kasutanud mitme tasandi ja Pange tähele, et oleme kasutanud mitme tasandi Pange tähele, et oleme kasutanud mitme tasandi

Pange tähele, et oleme kasutanud mitme tasandi

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

Pange tähele, et oleme kasutanud mitme tasandi fdPange tähele, et oleme kasutanud mitme tasandi fd, Pange tähele, et oleme kasutanud mitme tasandi Pange tähele, et oleme kasutanud mitme tasandi void.

Pange tähele, et oleme kasutanud mitme tasandi

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) {
        // Error
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Timeout
        case 0:
            result = 0;
            goto cleanup;
        // Successful operation
        default:
            // Invoke the event handlers
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Kokkuvõtteks, kasutajakoodi funktsioonide ahel näeb välja järgmine:

Täisfunktsionaalne I/O reaktor puhtal C-l

Üksikprotsessoriga server

Selleks, et testida I/O reactorit suure koormuse korral, kirjutame lihtsa HTTP veebiserveri, mis vastab igale päringule pildiga.

Lühike ülevaade HTTP protokollist

HTTP — see on protokoll rakenduslikul tasemel, mida kasutatakse peamiselt serveri ja brauseri vaheliseks suhtlemiseks.

HTTP-d on lihtne kasutada transpordikihi peal , edastades ja vastu võttes sõnumeid, mille formaat on määratud protokollist TCPPäringu formaat spetsifikatsioonile.

CRLF CRLF CRLF CRLF CRLF

CRLF

  • — on kahe tähe jadad: , mis eraldab päringu esimese rea, pealkirjad ja andmed. r ja n, разделяющая первую строку запроса, заголовки и данные.
  • — on üks järgmistest CONNECT OPTIONS, DELETE, GET, HEAD, TRACE., PATCH, POST, PUT, Brauser saadab meie serverile käsu,, mis tähendab 'Saada mulle failisisu'. GET, означающую «Отправь мне содержимое файла».
  • ühtne ressurssi identifitseerija.Näiteks, kui URI =, siis klient küsib saidi peamist lehte. /index.html, то клиент запрашивает главную страницу сайта.
  • — HTTP protokolli versioon formaadis HTTP/X.Y. Tänapäeval on kõige sagedamini kasutatav versioon —HTTP/1.1 HTTP/1.1.
  • — on paar võti-väärtus formaadis : , saadetud serverile edasiseks analüüsiks., отправляемая серверу для дальнейшего анализа.
  • — andmed, mida server vajab operatsiooni täitmiseks. Tihti on see lihtsalt või mõni muu formaat. JSON Vastuse formaat

CRLF CRLF CRLF CRLF CRLF

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

  • <КОД СТАТУСА> — see number, which represents the result of the operation. Our server will always return a status 200 (successful operation).
  • <ОПИСАНИЕ СТАТУСА> — a string representation of the status code. For status code 200, it is OK.
  • — on paar võti-väärtus formaadis — a header in the same format as in the request. We will return headers Content-Length (file size) and Content-Type: text/html (type of returned data).
  • — andmed, mida server vajab operatsiooni täitmiseks. Tihti on see lihtsalt — the data requested by the user. In our case, this is the path to the image in HTML.

Fail http_server.c (single-threaded server) includes the file common.h, which contains the following function prototypes:

Show the function prototypes in common.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);

Also described is the functional macro SAFE_CALL() and the function defined fail(). The macro compares the expression value with an error, and if the condition is met, it calls the function fail():

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

Funktsioon fail() prints the given arguments to the terminal (like printf()) and terminates the program with 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);
}

Funktsioon new_server() returns a file descriptor for the 'server' socket, created by system calls socket(), bind() ja listen() and able to accept incoming connections in non-blocking mode.

Show the function 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;
}

  • Note that the socket is initially created in non-blocking mode with the flag SOCK_NONBLOCK, so in the function on_accept() (read more) the system call accept() does not stop the execution of the thread.
  • Kui reuse_port on true, this function will configure the socket with the option SO_REUSEPORT through setsockopt(), to use the same port in a multithreaded environment (see the 'Multithreaded server' section).

The event handler on_accept() is called after the OS generates the event EPOLLIN, in this case meaning that a new connection can be accepted. on_accept() accepts a new connection, switches it to non-blocking mode, and registers it with the event handler on_recv() in the I/O reactor.

Show the function 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);
}

The event handler on_recv() is called after the OS generates the event EPOLLIN, mis tähendab, et ühendus, mis on registreeritud on_accept(), on valmis andmeid vastu võtma.

on_recv() loeb andmeid ühendusest, kuni HTTP päring on täielikult saadud, seejärel registreerib see töötleja on_send() HTTP vastuse saatmiseks. Kui klient katkestab ühenduse, eemaldatakse sokket registreerimisest ja suletakse läbi close().

Kuva funktsioon on_recv()

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

    // Vastuvõtame sissetulevaid andmeid seni, kuni recv tagastab 0 või vea
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Klient katkestas ühenduse
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read tagastas vea, mis ei olnud see viga, mis blokeeriks
    // voogu
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Saadi täielik HTTP päring kliendilt. Nüüd registreerime sündmuste
    // töötleja andmete saatmiseks
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

The event handler on_send() is called after the OS generates the event EPOLLOUT, mis tähendab, et ühendus, mis on registreeritud on_recv(), on valmis andmeid saatma. See funktsioon saadab HTTP vastuse, mis sisaldab HTML-i koos pildiga kliendile, ja seejärel muudab sündmuste töötlejat taas on_recv().

Kuva funktsioon on_send()

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

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

Ja lõpuks, failis http_server.c, funktsioonis main() loome I/O reaktori läbi reactor_new(), loome serveri soketi ja registreerime selle, käivitame reaktori läbi reactor_run() täpselt üheks minutiks, seejärel vabastame ressursid ja lahkume programmist.

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

Kontrollime, et kõik töötab nagu peab. Kompileerime (chmod a+x compile.sh && ./compile.sh projektijuurest) ja käivitame isetehtud serveri, avame http://127.0.0.1:18470 brauseris ja jälgime seda, mida ootasime:

Täisfunktsionaalne I/O reaktor puhtal C-l

Jõudluse mõõtmine

Kuva minu masina omadused

$ 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

Mõõdame ühekiiruselise serveri jõudlust. Avame kaks terminali: ühes käivitame ./http_server, teises — wrk. Ühe minuti pärast kuvatakse teises terminalis järgmine statistika:

$ 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

Meie ühekiiruseline server suutis töödelda rohkem kui 11 miljonit päringut minutis, mis tulenevad 100 ühendusest. Tulemused on head, aga kas neid on võimalik parandada?

Mitme lõimega server

Kuidas mainitud, saab I/O reaktorit luua eraldi lõimedes, mistõttu kasutatakse kõiki CPU'sid. Rakendame seda lähenemist praktikas:

Näita 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);
    }
}

Nüüd on igal lõimel oma reaktor:

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

Pange tähele, et funktsiooni argumendiks new_server() seisab true. See tähendab, et määrame serveri soketile sätte SO_REUSEPORT, et kasutada seda mitme lõimega keskkonnas. Lisainfot leiate siit.

Teine katse

Nüüd mõõdame mitme lõimega serveri jõudlust:

$ 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

Töödeldud päringute arv 1 minuti jooksul suurenes ~3,28 korda! Kuid ringisummani jäi puudu vaid ~kaks miljonit, proovime seda parandada.

Esmalt vaatame statistikat, mille genereeris 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

CPU afinnity kasutamine, kompileerimine lippudega -march=native, PGO, tabamuste arvu suurendamine vahemälus, suurendamine MAX_EVENTS ja kasutamine EPOLLET ei andnud märkimisväärset kasu jõudluses. Aga mis juhtub, kui suurendada samaaegsete ühenduste arvu?

Statistika 352 samaaegse ühenduse korral:

$ 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

Soovitud tulemus on saavutatud ning koos sellega huvitav graafik, mis näitab, kuidas ühe minuti jooksul töödeldud päringute arv sõltub ühenduste arvust:

Täisfunktsionaalne I/O reaktor puhtal C-l

Nähes, et paari saja ühenduse järel langeb töödeldud päringute arv mõlema serveri puhul järsult (mitme sõlme variandi puhul on see rohkem märgatav). Kas see on seotud Linuxi TCP\/IP steki rakendusega? Ootame teie arvamusi selle graafiku käitumise ja mitme- ning ühetuus variationide optimeerimise kohta kommentaarides.

Kuidas märkisid kommentaarides, see jõudlustest ei näita I\/O reaktori käitumist reaalsetes koormustes, kuna server suhtleb peaaegu alati andmebaasiga, genereerib logisid, kasutab krüptograafiat TLS jne., mistõttu koormus muutub ebaühtlaseks (dünaamiliseks). Testid koos väliste komponentidega viiakse läbi I\/O reaktori artiklis.

I\/O reaktori puudused

Peab mõistma, et I\/O reaktoril on puudusi, nimelt:

  • I\/O reaktori kasutamine mitme niidi keskkonnas on veidi keerulisem, kuna tuleb käsitsi hallata niite.
  • Praktika näitab, et enamikul juhtudel on koormus ebamugav, mis võib viia olukorrani, kus üks voog töötab, samas kui teine on tööga üle koormatud.
  • Kui üks sündmuse töötleja lukustab voolu, siis lukustub ka süsteemi valija, mis võib viia raskesti leidlike vigadeni.

Need probleemid lahendab I/O proaktor, millel on sageli ajakava, mis jaotab koormuse ühtlaselt lõime basseini, ja millel on ka mugavam API. Räägime sellest hiljem, minu teises artiklis.

Kokkuvõte

Selles seoses on meie teekond teooriast otse profilerist lõppenud.

Ei tasu sellega piirduda, sest on palju teisi sama huvitavaid lähenemisviise võrgutarkvara kirjutamiseks, millel on erinevad mugavuse ja kiirusetase. Minu arvates on allpool huvitavad lingid.

Kohtumiseni!

Huvitavad projektid

Mida veel lugeda?

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster