Reaktor I/O me funksione të plota

Reaktor I/O me funksione të plota

Paraqitje

Reaktor I/O (me një fije cikli i ngjarjes) është një model për të shkruar softuer me ngarkesë të lartë, i përdorur në shumë zgjidhje të njohura:

Në këtë artikull, ne do të shikojmë të dhënat e një reaktori I/O dhe mënyrën se si funksionon, do të shkruajmë një zbatim në më pak se 200 rreshta kodi dhe do të bëjmë një proces të thjeshtë të serverit HTTP mbi 40 milionë kërkesa/min.

Parathënie libri

  • Artikulli u shkrua për të ndihmuar në kuptimin e funksionimit të reaktorit I/O, dhe për këtë arsye për të kuptuar rreziqet gjatë përdorimit të tij.
  • Për të kuptuar artikullin kërkohet njohja e bazave. gjuha C dhe disa përvojë në zhvillimin e aplikacioneve të rrjetit.
  • I gjithë kodi është shkruar në gjuhën C në mënyrë rigoroze sipas (kujdes: PDF e gjatë) sipas standardit C11 për Linux dhe në dispozicion në GitHub.

Pse bëhet kjo?

Me rritjen e popullaritetit të internetit, serverët e uebit filluan të kenë nevojë të trajtojnë një numër të madh lidhjesh njëkohësisht, dhe për këtë arsye u provuan dy qasje: bllokimi i hyrjes/daljes në një numër të madh temash të sistemit operativ dhe mosbllokimi i hyrjes/daljes në kombinim me një sistem njoftimi për ngjarje, i quajtur gjithashtu "zgjedhësi i sistemit" (epoll/në radhë/IOCP/etj).

Qasja e parë përfshinte krijimin e një filli të ri OS për çdo lidhje hyrëse. Disavantazhi i tij është shkallëzueshmëria e dobët: sistemi operativ do të duhet të zbatojë shumë tranzicionet e kontekstit и thirrjet e sistemit. Ato janë operacione të shtrenjta dhe mund të çojnë në mungesë të RAM-it të lirë me një numër mbresëlënës lidhjesh.

Versioni i modifikuar thekson numër fiks i fijeve (peshinë thread), duke parandaluar kështu sistemin nga ndërprerja e ekzekutimit, por në të njëjtën kohë duke paraqitur një problem të ri: nëse një grup thread është aktualisht i bllokuar nga operacionet e leximit të gjatë, atëherë prizat e tjera që tashmë janë në gjendje të marrin të dhëna nuk do të jenë në gjendje të bej keshtu.

Qasja e dytë përdor sistemi i njoftimit të ngjarjeve (zgjedhësi i sistemit) i siguruar nga OS. Ky artikull diskuton llojin më të zakonshëm të përzgjedhësit të sistemit, bazuar në sinjalizimet (ngjarjet, njoftimet) në lidhje me gatishmërinë për operacionet I/O, dhe jo në njoftimet për përfundimin e tyre. Një shembull i thjeshtuar i përdorimit të tij mund të përfaqësohet nga bllok diagrami i mëposhtëm:

Reaktor I/O me funksione të plota

Dallimi midis këtyre qasjeve është si më poshtë:

  • Bllokimi i operacioneve I/O pezullojë fluksi i përdoruesit deri saderisa OS të jetë në rregull defragmentet hyrëse paketat IP në rrjedhën e bajtit (TCP, duke marrë të dhëna) ose nuk do të ketë hapësirë ​​të mjaftueshme në buferët e brendshëm të shkrimit për dërgimin e mëvonshëm nëpërmjet NIC (dërgimi i të dhënave).
  • Zgjedhësi i sistemit me kalimin e kohës njofton programin se OS tashmë paketa IP të defragmentuara (TCP, marrja e të dhënave) ose hapësirë ​​e mjaftueshme në buferët e brendshëm të shkrimit tashmë në dispozicion (dërgimi i të dhënave).

Për ta përmbledhur, rezervimi i një thread OS për çdo I/O është humbje e fuqisë kompjuterike, sepse në realitet, thread-ët nuk po bëjnë punë të dobishme (prandaj termi "ndërprerje e softuerit"). Përzgjedhësi i sistemit e zgjidh këtë problem, duke i lejuar programit të përdoruesit të përdorë burimet e CPU-së shumë më ekonomikisht.

Modeli i reaktorit I/O

Reaktori I/O vepron si një shtresë ndërmjet përzgjedhësit të sistemit dhe kodit të përdoruesit. Parimi i funksionimit të tij përshkruhet nga bllok diagrami i mëposhtëm:

Reaktor I/O me funksione të plota

  • Më lejoni t'ju kujtoj se një ngjarje është një njoftim që një prizë e caktuar është në gjendje të kryejë një operacion I/O jo-bllokues.
  • Një mbajtës i ngjarjeve është një funksion i thirrur nga reaktori I/O kur merret një ngjarje, i cili më pas kryen një operacion I/O jo-bllokues.

Është e rëndësishme të theksohet se reaktori I/O është sipas përkufizimit me një filetim të vetëm, por nuk ka asgjë që e ndalon konceptin të përdoret në një mjedis me shumë fije në një raport prej 1 fije: 1 reaktor, duke ricikluar kështu të gjitha bërthamat e CPU.

Zbatimi

Ne do të vendosim ndërfaqen publike në një skedar reactor.h, dhe zbatimi - në reactor.c. reactor.h do të përbëhet nga njoftimet e mëposhtme:

Trego deklaratat në reaktor.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);

Struktura e reaktorit I/O përbëhet nga përshkruesi i skedarit përzgjedhës epoll и tabela hash GHashTable, e cila e lidh çdo fole në CallbackData (struktura e një mbajtësi të ngjarjeve dhe një argument i përdoruesit për të).

Shfaq të dhënat e reaktorit dhe të thirrjes

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

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

Ju lutemi vini re se ne kemi aktivizuar aftësinë për të trajtuar lloj i paplotë sipas indeksit. NË reactor.h ne deklarojmë strukturën reactordhe në reactor.c ne e përcaktojmë atë, duke parandaluar kështu përdoruesin të ndryshojë në mënyrë eksplicite fushat e tij. Ky është një nga modelet fshehja e të dhënave, e cila përshtatet në mënyrë të përmbledhur në semantikën C.

Funksionet reactor_register, reactor_deregister и reactor_reregister përditësoni listën e prizave me interes dhe mbajtësit përkatës të ngjarjeve në përzgjedhësin e sistemit dhe tabelën hash.

Shfaq funksionet e regjistrimit

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

Pasi reaktori I/O ka përgjuar ngjarjen me përshkruesin fd, ai thërret mbajtësin përkatës të ngjarjeve, tek i cili kalon fd, maskë bit ngjarjet e krijuara dhe një tregues përdoruesi për void.

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

Për ta përmbledhur, zinxhiri i thirrjeve të funksionit në kodin e përdoruesit do të marrë formën e mëposhtme:

Reaktor I/O me funksione të plota

Server me një filetim të vetëm

Për të testuar reaktorin I/O nën ngarkesë të lartë, ne do të shkruajmë një server të thjeshtë në internet HTTP që i përgjigjet çdo kërkese me një imazh.

Një referencë e shpejtë për protokollin HTTP

HTTP - ky është protokolli niveli i aplikimit, përdoret kryesisht për ndërveprimin server-shfletues.

HTTP mund të përdoret lehtësisht transporti protokoll TCP, dërgimi dhe marrja e mesazheve në një format të specifikuar Specifikim.

Formati i Kërkesës

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

  • CRLF është një sekuencë prej dy karakteresh: r и n, duke ndarë rreshtin e parë të kërkesës, titujt dhe të dhënat.
  • <КОМАНДА> - nje nga CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Shfletuesi do të dërgojë një komandë në serverin tonë GET, që do të thotë "Më dërgoni përmbajtjen e skedarit."
  • <URI> - identifikues uniform i burimit. Për shembull, nëse URI = /index.html, atëherë klienti kërkon faqen kryesore të faqes.
  • <ВЕРСИЯ HTTP> — versioni i protokollit HTTP në format HTTP/X.Y. Versioni më i përdorur sot është HTTP/1.1.
  • <ЗАГОЛОВОК N> është një çift çelës-vlerë në format <КЛЮЧ>: <ЗНАЧЕНИЕ>, dërguar në server për analiza të mëtejshme.
  • <ДАННЫЕ> — të dhënat e kërkuara nga serveri për të kryer operacionin. Shpesh është e thjeshtë JSON ose ndonjë format tjetër.

Formati i përgjigjes

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

  • <КОД СТАТУСА> është një numër që përfaqëson rezultatin e operacionit. Serveri ynë gjithmonë do të kthejë statusin 200 (operim i suksesshëm).
  • <ОПИСАНИЕ СТАТУСА> — paraqitja e vargut të kodit të statusit. Për kodin e statusit 200 kjo është OK.
  • <ЗАГОЛОВОК N> - titulli i të njëjtit format si në kërkesë. Ne do t'i kthejmë titujt Content-Length (madhësia e skedarit) dhe Content-Type: text/html (kthimi i llojit të të dhënave).
  • <ДАННЫЕ> — të dhënat e kërkuara nga përdoruesi. Në rastin tonë, kjo është rruga drejt imazhit në HTML.

skedar http_server.c (server me një filetim të vetëm) përfshin skedarin common.h, i cili përmban prototipet e mëposhtme të funksionit:

Trego prototipet e funksioneve të përbashkëta.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);

Makro funksionale është përshkruar gjithashtu SAFE_CALL() dhe funksioni është i përcaktuar fail(). Makroja krahason vlerën e shprehjes me gabimin, dhe nëse kushti është i vërtetë, thërret funksionin fail():

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

Funksion fail() printon argumentet e kaluara në terminal (si printf()) dhe përfundon programin me kodin 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);
}

Funksion new_server() kthen përshkruesin e skedarit të prizës "server" të krijuar nga thirrjet e sistemit socket(), bind() и listen() dhe të aftë për të pranuar lidhjet hyrëse në një modalitet jo-bllokues.

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

  • Vini re se priza fillimisht krijohet në modalitetin jo-bllokues duke përdorur flamurin SOCK_NONBLOCKnë mënyrë që në funksion on_accept() (lexo më shumë) thirrje sistemi accept() nuk e ndaloi ekzekutimin e fillit.
  • Nëse reuse_port është true, atëherë ky funksion do të konfigurojë prizën me opsionin SO_REUSEPORT përmes setsockopt()për të përdorur të njëjtën portë në një mjedis me shumë fije (shih seksionin "Serveri me shumë fije").

Mbajtës i ngjarjeve on_accept() thirret pasi OS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se lidhja e re mund të pranohet. on_accept() pranon një lidhje të re, e kalon atë në modalitetin pa bllokim dhe regjistrohet me një mbajtës të ngjarjeve on_recv() në një reaktor I/O.

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

Mbajtës i ngjarjeve on_recv() thirret pasi OS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se lidhja është regjistruar on_accept(), gati për të marrë të dhëna.

on_recv() lexon të dhënat nga lidhja derisa kërkesa HTTP të merret plotësisht, pastaj regjistron një mbajtës on_send() për të dërguar një përgjigje HTTP. Nëse klienti prish lidhjen, priza çregjistrohet dhe mbyllet duke përdorur close().

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

Mbajtës i ngjarjeve on_send() thirret pasi OS gjeneron një ngjarje EPOLLOUT, që do të thotë se lidhja është regjistruar on_recv(), gati për të dërguar të dhëna. Ky funksion dërgon një përgjigje HTTP që përmban HTML me një imazh te klienti dhe më pas ndryshon trajtuesin e ngjarjeve përsëri në on_recv().

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

Dhe së fundi, në dosje http_server.c, në funksion main() ne krijojmë një reaktor I/O duke përdorur reactor_new(), krijoni një prizë serveri dhe regjistrojeni atë, filloni duke përdorur reaktorin reactor_run() për saktësisht një minutë, dhe më pas lëshojmë burime dhe dalim nga programi.

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

Le të kontrollojmë që gjithçka po funksionon siç pritej. Përpilimi (chmod a+x compile.sh && ./compile.sh në rrënjën e projektit) dhe hapni serverin e vetë-shkruar http://127.0.0.1:18470 në shfletues dhe shikoni se çfarë prisnim:

Reaktor I/O me funksione të plota

Matja e performancës

Trego specifikimet e makinës sime

$ 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

Le të matim performancën e një serveri me një filetim të vetëm. Le të hapim dy terminale: në një do të vrapojmë ./http_server, në një tjetër - dërrmues. Pas një minutë, statistikat e mëposhtme do të shfaqen në terminalin e dytë:

$ 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

Serveri ynë me një fije të vetme ishte në gjendje të përpunonte mbi 11 milionë kërkesa në minutë, me origjinë nga 100 lidhje. Nuk është një rezultat i keq, por a mund të përmirësohet?

Server me shumë fije

Siç u përmend më lart, reaktori I/O mund të krijohet në fije të veçanta, duke shfrytëzuar kështu të gjitha bërthamat e CPU. Le ta zbatojmë këtë qasje në praktikë:

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

Tani çdo fije zotëron të tijën reaktor:

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

Ju lutemi vini re se argumenti i funksionit new_server() aktet true. Kjo do të thotë që ne ia caktojmë opsionin prizës së serverit SO_REUSEPORTpër ta përdorur atë në një mjedis me shumë fije. Mund të lexoni më shumë detaje këtu.

Vrapimi i dytë

Tani le të matim performancën e një serveri me shumë fije:

$ 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

Numri i kërkesave të përpunuara në 1 minutë është rritur me ~3.28 herë! Por ne kishim vetëm XNUMX milionë pak nga numri i rrumbullakët, kështu që le të përpiqemi ta rregullojmë atë.

Së pari le të shohim statistikat e krijuara perfekte:

$ 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

Përdorimi i afinitetit të CPU, përmbledhje me -march=native, PGO, një rritje në numrin e goditjeve vend i fshehtë, rrit MAX_EVENTS dhe përdorni EPOLLET nuk dha një rritje të ndjeshme të performancës. Por çfarë ndodh nëse rrit numrin e lidhjeve të njëkohshme?

Statistikat për 352 lidhje të njëkohshme:

$ 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

Rezultati i dëshiruar u mor dhe me të një grafik interesant që tregon varësinë e numrit të kërkesave të përpunuara në 1 minutë nga numri i lidhjeve:

Reaktor I/O me funksione të plota

Ne shohim që pas disa qindra lidhjeve, numri i kërkesave të përpunuara për të dy serverët bie ndjeshëm (në versionin me shumë fije kjo është më e dukshme). A është kjo e lidhur me zbatimin e pirgut të Linux TCP/IP? Mos ngurroni të shkruani supozimet tuaja në lidhje me këtë sjellje të grafikut dhe optimizimet për opsionet me shumë fije dhe me një fije në komente.

Si vuri në dukje në komente, ky test i performancës nuk tregon sjelljen e reaktorit I/O nën ngarkesa reale, sepse pothuajse gjithmonë serveri ndërvepron me bazën e të dhënave, nxjerr regjistrat, përdor kriptografinë me TLS etj., si rezultat i së cilës ngarkesa bëhet jo uniforme (dinamike). Testet së bashku me komponentët e palëve të treta do të kryhen në artikullin për proaktorin I/O.

Disavantazhet e reaktorit I/O

Ju duhet të kuptoni se reaktori I/O nuk është pa të metat e tij, përkatësisht:

  • Përdorimi i një reaktori I/O në një mjedis me shumë fije është disi më i vështirë, sepse do t'ju duhet të menaxhoni manualisht flukset.
  • Praktika tregon se në shumicën e rasteve ngarkesa është jo uniforme, gjë që mund të çojë në prerjen e një fijeje ndërsa një tjetër është e zënë me punë.
  • Nëse një mbajtës ngjarjesh bllokon një thread, vetë zgjedhësi i sistemit do të bllokojë gjithashtu, gjë që mund të çojë në gabime të vështira për t'u gjetur.

Zgjidh këto probleme Proaktor I/O, i cili shpesh ka një planifikues që shpërndan në mënyrë të barabartë ngarkesën në një grup fijesh, dhe gjithashtu ka një API më të përshtatshëm. Ne do të flasim për këtë më vonë, në artikullin tim tjetër.

Përfundim

Këtu ka përfunduar udhëtimi ynë nga teoria drejt e në shter të profilit.

Ju nuk duhet të ndaleni në këtë, sepse ka shumë qasje të tjera po aq interesante për të shkruar softuer rrjeti me nivele të ndryshme komoditeti dhe shpejtësie. Interesante, për mendimin tim, lidhjet janë dhënë më poshtë.

Deri herën tjetër!

Projekte interesante

Çfarë tjetër për të lexuar?

Burimi: www.habr.com

Shto një koment