Täielik I/O reaktor puhtal C-s

Täielik I/O reaktor puhtal C-s

Sissejuhatus

I/O reaktor (ühe lõimega sündmuste tsükkel) — see on musternäidis, mis on mõeldud kõrgkoormusega tarkvara kirjutamiseks ja seda kasutatakse paljude populaarsete lahenduste puhul:

Selles artiklis vaatame I/O reaatori tagamaid ja selle tööpõhimõtteid, kirjutame rakenduse vähem kui 200 koodireaga ja paneme lihtsa HTTP-serveri töötlema üle 40 miljoni päringu minutis.

Eessõna

  • Artikkel on kirjutatud selleks, et aidata mõista I/O reaktori toimimist ja seega teadvustada riske selle kasutamisel.
  • Artikli mõistmiseks on vajalik teadmised keelest C ja vähene kogemus võrgurakenduste arendamisel.
  • Kogu kood on kirjutatud keeles C rangelt vastavalt (hoiatavalt: pikk PDF) standardile C11 Linuxis ja on saadaval GitHub.

Miks on see vajalik?

Interneti populaarsuse kasvades on veebiserveritel muutunud vajalikuks töödelda samal ajal suures mahus ühendusi, mille tõttu on proovitud kahte lähenemist: blokeerivat I/O suure hulga opsüsteemi lõimede abil ja mitteblokeerivat I/O koos sündmustest teavitamise süsteemiga, mida nimetatakse ka «süsteemi valijaks» (epoll/kqueue/IOCP/etc).

Esimene lähenemine hõlmab uue operatsioonisüsteemi lõime loomist iga sissetuleva ühenduse jaoks. Selle puuduseks on halb skaleeritavus: operatsioonisüsteem peab tegema arvukaid konteksti vahetusi ja süsteemikõnesid. Need on kulukad operatsioonid ja võivad põhjustada olulise ühenduste arvu korral vaba RAM-i puudujääki.

Muudetud versioon eraldab fikseeritud arvu lõime (thread pool), takistades seeläbi süsteemi äkilist töö lõpetamist, kuid samas toob see esile uue probleemi: kui lõimede puhvrid on lukustatud pikaajaliste lugemistegevuste tõttu, ei saa teised sokid, mis on juba valmis andmeid vastu võtma, seda teha.

Teine lähenemine kasutab sündmustest teavitamise süsteemi (süsteemide selektor), mida operatsioonisüsteem pakub. Käesolevas artiklis käsitletakse kõige sagedasemat tüüpi süsteemide selektorit, mis põhineb I/O operatsioonide valmidusest teavitamise (sündmuste, teavituste) alusel, mitte nende lõpetamise teavitustel. Lihtsustatud näidet selle kasutamisest võib esitada järgmise plokiahelana:

Täielik I/O reaktor puhtal C-s

Needused meetodite vahel on erinevus järgmine:

  • Tõkestavad I/O operatsioonid peatavad kasutaja voolu kuni, kuni OS on korralikult defragmentinud aktiivsed IP paketid baiti voos (TCP, andmete vastuvõtt) või kui sisemistes kirjutusbufferites on piisavalt vaba ruumi edasise saatmise jaoks NIC (andmete saatmine).
  • Süsteemisüsteem mõne aja pärast teavitab programmi, et OS juba on defragmentinud IP paketid (TCP, andmete vastuvõtt) või piisavalt ruumi sisemistes kirjutusbufferites juba on saadaval (andmete saatmine).

Kokkuvõtteks võib öelda, et OS voolu reserveerimine igas I/O puhul on lihtsalt arvutusvõime raiskamine, kuna voolud ei tegele tegelikult kasuliku tööga (just sealt tuleneb mõisted „programmi katkestus“). Süsteemisüsteem lahendab selle probleemi, võimaldades kasutaja programmidel CPU ressursse oluliselt efektiivsemalt kasutada.

I/O reaktsiooni mudel

I/O reaktsioon toimib kui kiht süsteemisüsteemi ja kasutajakoodi vahel. Selle tööpõhimõte on kirjeldatud järgmise vooskeemiga:

Täielik I/O reaktor puhtal C-s

  • Tulet meelde, et sündmus on teade selle kohta, et kindel sokkel on võimeline sooritama mittesünkroonset I/O operatsiooni.
  • Sündmuste töötleja on funktsioon, mida kutsub I/O reaktor üles, kui sündmus on saadud, ning see teostab seejärel mittesünkroonse I/O operatsiooni.

Oluline on märkida, et I/O reaktor on määratletud üheksatoonilisena, kuid ei ole midagi, mis takistaks kontseptsiooni kasutamist mitme niidi keskkonnas 1 niit: 1 reaktor, seega kasutatakse kõiki CPU tuumasid.

Rakendus

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

Kuva deklareerimised failis reactor.h

typedef struct reactor Reactor;

/*
 * Näitaja funktsioonile, mida I/O reaktor kutsub, kui
 * süsteemi selektorilt saadakse sündmus.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Tagastab `NULL` vea korral, mitte-`NULL` näitaja `Reactor`-le
 * muul juhul.
 */
Reactor *reactor_new(void);

/*
 * Vabastab süsteemi selektori, kõik registreeritud socketid antud
 * hetkel ja I/O reaktori ise.
 *
 * Järgmised funktsioonid tagastavad -1 vea korral, 0 eduka korral.
 */
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 ajapiiranguga `timeout`.
 *
 * See funktsioon edastab kontrolli kutsuvatele koodidele, kui eraldatud aeg on möödas
 * või/ja kui registreeritud socketid puuduvad.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

I/O reaktori struktuur koosneb failide descriptorist selektorist epoll ja räsi-tabelist GHashTable, mis seob iga socketi CallbackData (struktuuri sündmustöötlusest ja selle kasutaja argumendist).

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 immutamatute tüüpide käsitsemise funktsionaalsust puutumatut tüüpi näidiku kaudu. Esmalt reactor.h deklareerime struktuuri reactor, ja seejärel määratleme selle, takistades seeläbi kasutajal selgelt selle väljaid muuta. See on üks andmete peitmise mustritest reactor.c me määratleme, takistades seeläbi kasutajal selgelt selle väljade muutmist. See on üks mustritest , mis sobib hästi C semantikasse., mis sobib kokku C-i semantikaga.

Funktsioonid reactor_register, reactor_deregister ja reactor_reregister uuendavad huvipakkuvate soklite ja vastavate sündmuste töötlejate nimekirja süsteemi selektoris ja hash-tabelis.

Näita registreerimisfunktsioone

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

Pärast seda, kui I/O reaktor on haaranud sündmuse deskriptoriga fd, kutsub see vastava sündmuste töötlejat, edastades fd, bitimaski loodud sündmustest ja kasutaja viite void.

Näita funktsiooni 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) {
        // Fehler
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Zeit abgelaufen
        case 0:
            result = 0;
            goto cleanup;
        // Erfolgreiche Operation
        default:
            // Invoke event handlers
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

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

Täielik I/O reaktor puhtal C-s

Ühe protsessori server

I/O reaktori testimiseks suure koormuse korral kirjutame lihtsa HTTP veebiserveri, mis vastab igale päringule pildi abil.

Lühike ülevaade HTTP protokollist

HTTP — on protokoll rakendustasandil, mida kasutatakse peamiselt serveri ja brauseri suhtlemiseks.

HTTP-d on lihtne kasutada üle transportprotokolli protokoll TCP, saates ja vastu võttes määratletud formaadi sõnumeid spetsiifikatsiooniga.

Päringu formaat

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — on kahe sümboli järjestus: r ja n, mis eraldab päringu esimest rida, pealkirjad ja andmed.
  • <КОМАНДА> — üks järgmistest CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Brauser saadab meie serverile käsu GET, mis tähendab „Saada mulle faili sisu“.
  • <URI>ühtne ressursi identifikaator. Näiteks, kui URI = /index.html, siis klient küsib saidi peamist lehte.
  • <ВЕРСИЯ HTTP> — HTTP protokolli versioon formaadis HTTP/X.Y. Praegu enimkasutatav versioon on HTTP/1.1.
  • <ЗАГОЛОВОК N> — on võti-väärtus paar formaadis :, saadetav serverile edasiseks analüüsimiseks.
  • <ДАННЫЕ> — andmed, mida server vajab toimingu täitmiseks. Tihti on need lihtsalt JSON või mõnes muus formaadis.

Vastuse formaat

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — see number, which represents the result of the operation. Our server will always return a status 200 (successful operation).
  • <ОПИСАНИЕ СТАТУСА> — string representation of the status code. For status code 200 — this is OK.
  • <ЗАГОЛОВОК N> — a header in the same format as in the request. We will return the headers Content-Length (size of the file) and Content-Type: text/html (type of returned data).
  • <ДАННЫЕ> — data requested by the user. In our case, this is the path to the image in HTML.

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

Show 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 fail(). The macro compares the expression value to 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)

Function fail() prints the passed 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);
}

Function new_server() returns the file descriptor of the 'server' socket, created by system calls socket(), bind() ja listen() ja suudab vastu võtta sisendeid blokkeerimata režiimis.

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

  • Pange tähele, et socket luuakse algselt blokkeerimata režiimis lipu kaudu SOCK_NONBLOCK, et süsteemikõne on_accept() ei blokeeriks lõime täitmist. accept() Kui
  • Kui reuse_port on väärt true, siis konfigureerib see funktsioon socket'i valiku SO_REUSEPORT kaudu setsockopt(), et kasutada sama porti mitme lõime keskkonnas (vaata jaotist „Mitu lõime server”).

Ürituste käitleja on_accept() kutsutakse välja pärast seda, kui operatsioonisüsteem genereerib sündmuse EPOLLIN, antud juhul tähendab see, et uus ühendus võib olla vastu võetud. on_accept() võtab uue ühenduse, lülitab selle mittesulgurrežiimi ja registreerib sündmuste käitlejale on_recv() I/O reaktoris.

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

Ürituste käitleja on_recv() kutsutakse välja pärast seda, kui operatsioonisüsteem genereerib sündmuse EPOLLIN, antud juhul tähendades, et registreeritud ühendus on_accept(), on andmete vastuvõtmiseks valmis.

on_recv() loeb andmeid ühendusest kuni HTTP päring on täielikult saadud, seejärel registreerib ta töötleja on_send() HTTP vastuse saatmiseks. Kui klient katkestas ühenduse, deregistreeritakse ja suletakse sokkel close().

Näita funktsiooni on_recv()

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

    // Vastuvõtame sisendi, 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 pole blokaadi tõttu
    // voos
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Täielik HTTP päring on saadud 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);
    }
}

Ürituste käitleja on_send() kutsutakse välja pärast seda, kui operatsioonisüsteem genereerib sündmuse EPOLLOUT, mis tähendab, et registreeritud ühendus on_recv(), on andmete saatmiseks valmis. See funktsioon saadab HTTP vastuse, mis sisaldab HTML-i koos pildiga, kliendile ja seejärel muudab sündmuste töötlejat taas on_recv().

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

Ja lõpuks, failis http_server.c, funktsioonis main() loome I/O reaktsiooni meetodi reactor_new(), loome serveri sokli ja registreerime selle, käivitame reaktsiooni kasutades reactor_run() täpselt ühe minuti, seejärel vabastame ressursid ja väljastame programmi.

Näita 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 projekti juures) ja käivitame isetehtud serveri, avame http://127.0.0.1:18470 brauseris ja näeme seda, mida ootasime:

Täielik I/O reaktor puhtal C-s

Jõudluse mõõtmine

Näita minu masina omadusi

$ 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 Teema: Mint-Y-Dark (Mint-Y)
     NMm  dMM`  ..`   ...   ydm. dMM   GTK Teema: Mint-Y [GTK2/3]
     hMM- +MMd/-------...-:sdds  dMM   Ikoni teema: Mint-Y
     -NMm- :hNMNNNmdddddddddy/`  dMM   Fond: 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 ühesektorilise serveri jõudlust. Avame kaks terminali: ühes käivitame ./http_server, teises — wrk. Minuti pärast kuvatakse teises terminalis järgmised statistikad:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: et-EE,et;q=0.5" -H "Connection: keep-alive"
Käivitame 1m testi @ http://127.0.0.1:18470
  8 lõime ja 100 ühendust
  Lõime statistika   Keskmine   Std. kõrvalekalle   Max   +/- Std. kõrvalekalle
    Latentsus   493.52us   76.70us  17.31ms   89.57%
    Päringud/sec    24.37k     1.81k   29.34k    68.13%
  11657769 päringut 1.00m jooksul, 1.60GB loetud
Päringud/sec: 193974.70
Ülekanded/sec:     27.19MB

Meie ühesuunaline server suutis töödelda üle 11 miljoni päringu minutis, mis tulenes 100 ühendusest. Hea tulemus, aga kas seda saab paremaks muuta?

Mitu lõime sisaldav server

Nagu eelnevalt mainitud, saab I/O reaktorit luua eraldi lõimedes, kasutades ära kõiki CPU tuumasid. Rakendame seda lähenemist praktikas:

Kuva 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 omab iga lõim oma reaktorit:

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

Pange tähele, et funktsiooni argumendina new_server() tuleb true. See tähendab, et me määrame serveri soketile valiku SO_REUSEPORT, et kasutada seda mitme lõime keskkonnas. Rohkem teavet leiate siin.

Teine katse

Nüüd mõõdame mitme lõime 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"
Ajal 1m test @ http://127.0.0.1:18470
  8 lõime ja 100 ühendust
  Lõime statistika   Keskmine   Std. kõrvalekalle   Max   +/– Std. kõrvalekalle
    Latentsus     1.14ms    2.53ms  40.73ms   89.98%
    Req/sec    79.98k    18.07k  154.64k    78.65%
  38208400 päringut 1.00m jooksul, 5.23GB loetud
Päringud/sec: 635876.41
Ülekandega/sec:     89.14MB

1 minuti jooksul töödeldud päringute arv kasvas ~3.28 korda! Kuid täieliku numbri saavutamiseks jäi puudu umbes ~ kaks miljonit, proovime seda parandada.

Esialgu vaatame statistikat, mis on genereeritud perf:

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

      Jõudlusloendi statistika '. /http_server_multithreaded':

      242446,314933      ülesande kella (msec)         #    4,000 CPU-d on rakendatud          
        1 813 074      konteksti vahetusi          #    0,007 M/sec                  
            4 689      CPU-migratsioonid            #    0,019 K/sec                  
              254      lehe vead               #    0,001 K/sec                  
   895 324 830 170      tsüklid                    #    3,693 GHz                    
   621 378 066 808      juhised              #    0,69  insn per tsükkel         
   119 926 709 370      harud                  #  494,653 M/sec                  
     3 227 095 669      haru vead             #    2,69% kõikidest harudest        
           808 664      vahemälu vead                                                

      60,604330670 sekundit aega möödus

CPU kohandamise kasutamine, kompileerimine koos -march=native, PGO, sisestuste arvu suurendamine vahemälu, suurenemine MAX_EVENTS ja kasutamine EPOLLET ei toonud märkimisväärset jõudluse kasvu. Aga mis saab, kui suurendada samade ühenduste arvu?

Statistika 352 samal ajal ü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"
Töötamine 1m test @ http://127.0.0.1:18470
  8 lõime ja 352 ühendust
  Lõime Statistika   Keskmine      Std. dev.     Max   +/- Std. dev.
    Latentsus     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 päringut 1,00m, 5.48GB lugemiseks
Päringud/sek: 665789.26
Ülekanded/sek:     93.34MB

Soovitud tulemus saavutatud, koos sellega huvitav graafik, mis näitab ühe minuti jooksul töödeldud päringute arvu sõltuvust ühenduste arvust:

Täielik I/O reaktor puhtal C-s

Näeme, et pärast paarisaja ühenduse saavutamist tõuseb töödeldud päringute arv mõlemal serveril järsult (mitme lõime variandi puhul on see paremini märgatav). Kas see on seotud Linuxi TCP/IP stack'i rakendusega? Ootame teie arvamusi selle graafiku käitumise ja mitme lõime ning ühe lõime variandi optimeerimiste kohta kommentaarides.

Kuidas märkisite kommentaarides, see jõudlustest ei peegelda I/O reaktori käitumist reaalses koormuses, kuna server suhtleb peaaegu alati andmebaasiga, genereerib logisid, kasutab krüptograafiat TLS jne, mille tõttu koormus muutub ebaühtlaseks (dünaamiliseks). Testid koos kolmandate osapoolte komponentidega viiakse läbi I/O reaktori artiklis.

I/O reaktori puudused

Oluline on mõista, et I/O reaktoril on puudusi, sealhulgas:

  • I/O reaktori kasutamine mitme lõime keskkonnas on keerulisem, kuna tuleb käsitsi vooge hallata.
  • Praktika näitab, et enamasti on koormus ebaühtlane, mis võib viia olukorrani, kus üks voog töötleb, samas kui teine on tööga hõivatud.
  • Kui üks sündmuse töötleja blokeerib voolu, blokeerib ka süsteemi selektor, mis võib tekitada raskesti tuvastatavaid vigu.

Need probleemid lahendab I/O proaktor, mis sageli sisaldab ajakava, mis jaotab koormuse ühtlaselt lõimede puuli, ning millel on ka mugavam API. Räägime sellest hiljem teises artiklis.

Kokkuvõte

Selle lõppes meie teekond teooriast profiler'i väljastusse.

Ärge sellega piirduda, sest on veel palju teisi, mitte vähem huvitavaid lähenemisviise võrgu tarkvara kirjutamisele, erineva mugavuse ja kiirusetase. Huvi äratavad lingid on toodud allpool.

Kuni kohtumiseni!

Huvitavad projektid

Mida veel lugeda?

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster