Ezaugarri osoko bare-C I/O erreaktorea

Ezaugarri osoko bare-C I/O erreaktorea

Sarrera

I/O erreaktorea (hari bakarra gertaeren begizta) karga handiko softwarea idazteko eredua da, irtenbide ezagun askotan erabiltzen dena:

Artikulu honetan, I/O erreaktore baten nondik norakoak eta nola funtzionatzen duen aztertuko dugu, inplementazio bat idatziko dugu 200 kode lerro baino gutxiagotan eta HTTP zerbitzariaren prozesu sinple bat egingo dugu 40 milioi eskaera/min baino gehiago.

hitzaurrea

  • Artikulua I/O erreaktorearen funtzionamendua ulertzen laguntzeko idatzi zen, eta, beraz, erabiltzean arriskuak ulertzen laguntzeko.
  • Artikulua ulertzeko oinarriak ezagutzea beharrezkoa da. C hizkuntza eta sareko aplikazioen garapenean esperientziaren bat.
  • Kode guztia C hizkuntzan idatzita dago (Kontuz: PDF luzea) C11 estandarrera Linuxerako eta eskuragarri GitHub.

Zergatik da?

Interneten gero eta ospe handiagoarekin, web zerbitzariek konexio ugari kudeatu behar izaten hasi ziren aldi berean, eta, beraz, bi ikuspegi saiatu ziren: I/O blokeatzea OS hari kopuru handi batean eta blokeatzen ez duten I/O-rekin batera. gertaeren jakinarazpen-sistema, "sistema hautatzailea" ere deitua (epoll/kilara/IOCP/etb).

Lehenengo hurbilketa OS hari berri bat sortzea izan zen sarrerako konexio bakoitzeko. Bere desabantaila eskalagarritasun eskasa da: sistema eragileak asko ezarri beharko ditu testuinguruaren trantsizioak и sistema deiak. Eragiketa garestiak dira eta doako RAM eza ekar dezakete konexio kopuru ikaragarri batekin.

Aldatutako bertsioa nabarmentzen da hari kopuru finkoa (hari multzoa), horrela sistemak exekuzioa bertan behera uztea eragotziz, baina, aldi berean, arazo berri bat sartuz: irakurketa-eragiketa luzeek hari multzo bat blokeatuta badago, dagoeneko datuak jasotzeko gai diren beste socketek ezin izango dute hala egin.

Bigarren hurbilketa erabiltzen da gertaerak jakinarazteko sistema (sistema-hautatzailea) sistema eragileak emandakoa. Artikulu honek sistema-hautatzaile mota ohikoena aztertzen du, I/O eragiketetarako prest egoteari buruzko abisuetan (gertaerak, jakinarazpenak) oinarrituta, eta ez. horiek amaitzeari buruzko jakinarazpenak. Bere erabileraren adibide sinplifikatu bat bloke-diagrama honen bidez irudika daiteke:

Ezaugarri osoko bare-C I/O erreaktorea

Ikuspegi hauen arteko aldea honako hau da:

  • I/O eragiketak blokeatzea eseki erabiltzaile-fluxua arteOS behar bezala egon arte desfragmentatzen sartzen IP paketeak byte korrontean (TCP, datuak jasotzen) edo ez da nahikoa leku erabilgarri egongo barne idazketa-bufferetan ondorengo bidez bidaltzeko. NIC (datuak bidaltzen).
  • Sistema hautatzailea denborarekin programari jakinarazten dio sistema eragilea dela dagoeneko desfragmentatutako IP paketeak (TCP, datuen harrera) edo behar adina leku barne idazketa-bufferetan dagoeneko eskuragarri (datuak bidaltzea).

Laburbilduz, I/O bakoitzeko OS hari bat erreserbatzea konputazio-ahalmena xahutzea da, izan ere, errealitatean, hariek ez dute lan erabilgarria egiten ari (hortik terminoa "software etenaldia"). Sistema-hautatzaileak arazo hau konpontzen du, erabiltzailearen programari CPU baliabideak askoz ere modu ekonomikoagoan erabiltzeko aukera emanez.

I/O erreaktorearen eredua

I/O erreaktoreak sistema-hautatzailearen eta erabiltzailearen kodearen arteko geruza gisa jokatzen du. Bere funtzionamenduaren printzipioa bloke diagrama honek deskribatzen du:

Ezaugarri osoko bare-C I/O erreaktorea

  • Gogorarazten dizut gertaera bat socket jakin batek blokeatzen ez duen I/O eragiketa bat egiteko gai dela dioen jakinarazpena dela.
  • Gertaeren kudeatzailea I/O erreaktoreak gertaera bat jasotzen denean deitzen duen funtzio bat da, eta gero blokeatzen ez den I/O eragiketa bat egiten du.

Garrantzitsua da kontutan izan I/O erreaktorea definizioz hari bakarrekoa dela, baina ez dago ezerk hari anitzeko ingurune batean erabiltzeari uko egiten dion hari 1: 1 erreaktorearen proportzioan, horrela CPU nukleo guztiak birziklatuz.

Inplementazioa

Interfaze publikoa fitxategi batean jarriko dugu reactor.h, eta ezarpena - in reactor.c. reactor.h honako iragarki hauek izango ditu:

Erakutsi adierazpenak erreaktorean.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);

I/O erreaktorearen egiturak osatzen dute fitxategi deskribatzailea hautatzailea epoll и hash taulak GHashTable, socket bakoitzari mapa ematen diona CallbackData (gertaera-kudeatzaile baten egitura eta horren erabiltzailearen argumentua).

Erakutsi Reactor eta CallbackData

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

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

Kontuan izan kudeatzeko gaitasuna gaitu dugula mota osatugabea indizearen arabera. IN reactor.h egitura aldarrikatzen dugu reactoreta reactor.c definitzen dugu, horrela erabiltzaileak bere eremuak esplizituki aldatzea eragotziz. Hau da ereduetako bat datuak ezkutatzen, laburki C semantikan sartzen dena.

funtzio reactor_register, reactor_deregister и reactor_reregister eguneratu interes-socketen zerrenda eta dagozkion gertaeren kudeatzaileak sistema-hautatzailean eta hash-taulan.

Erakutsi erregistro-funtzioak

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

I/O erreaktoreak deskribatzailearekin gertaera atzeman ondoren fd, dagokion gertaeren kudeatzaileari deitzen dio, bertara pasatzen den fd, bit maskara sortutako gertaerak eta erabiltzaile-erakuslea void.

Erakutsi reactor_run() funtzioa

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

Laburbilduz, erabiltzailearen kodean funtzio-deien kateak forma hau hartuko du:

Ezaugarri osoko bare-C I/O erreaktorea

Hari bakarreko zerbitzaria

I/O erreaktorea karga handian probatzeko, edozein eskaerari irudi batekin erantzuten dion HTTP web zerbitzari soil bat idatziko dugu.

HTTP protokoloari erreferentzia azkarra

HTTP - Hau da protokoloa aplikazio maila, batez ere zerbitzari-arakatzailea elkarrekintzarako erabiltzen da.

HTTP erraz erabil daiteke garraioa protokoloa TCP, mezuak zehaztutako formatuan bidaliz eta jasoz zehaztapena.

Eskaera formatua

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

  • CRLF bi karaktereren segida bat da: r и n, eskaeraren lehen lerroa, goiburuak eta datuak bereiziz.
  • <КОМАНДА> - horietako bat CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Arakatzaileak komando bat bidaliko dio gure zerbitzariari GET, hau da, "Bidali fitxategiaren edukia".
  • <URI> - baliabideen identifikatzaile uniformea. Adibidez, URI = bada /index.html, orduan bezeroak gunearen orri nagusia eskatzen du.
  • <ВЕРСИЯ HTTP> — HTTP protokoloaren bertsioa formatuan HTTP/X.Y. Gaur egun gehien erabiltzen den bertsioa da HTTP/1.1.
  • <ЗАГОЛОВОК N> formatuan gako-balio bikotea da <КЛЮЧ>: <ЗНАЧЕНИЕ>, zerbitzariari bidalitako azterketa gehiago egiteko.
  • <ДАННЫЕ> — zerbitzariak eragiketa egiteko behar dituen datuak. Askotan sinplea da JSON edo beste edozein formatu.

Erantzun formatua

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

  • <КОД СТАТУСА> eragiketaren emaitza adierazten duen zenbakia da. Gure zerbitzariak 200. egoera itzuliko du beti (eragiketa arrakastatsua).
  • <ОПИСАНИЕ СТАТУСА> — egoera-kodearen katearen irudikapena. 200 egoera-kodearentzat hau da OK.
  • <ЗАГОЛОВОК N> — eskaeraren formatu bereko goiburua. Izenburuak itzuliko ditugu Content-Length (fitxategiaren tamaina) eta Content-Type: text/html (itzultzeko datu mota).
  • <ДАННЫЕ> — Erabiltzaileak eskatutako datuak. Gure kasuan, hau da irudiaren bidea HTML.

fitxategia http_server.c (hari bakarreko zerbitzaria) fitxategia barne hartzen du common.h, funtzio-prototipo hauek dituena:

Erakutsi funtzio-prototipoak komunean.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 funtzionala ere deskribatzen da SAFE_CALL() eta funtzioa zehazten da fail(). Makroak adierazpenaren balioa errorearekin alderatzen du, eta baldintza egiazkoa bada, funtzioari dei egiten dio fail():

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

Funtzioa fail() emandako argumentuak terminalean inprimatzen ditu (adibidez printf()) eta programa amaitzen du kodearekin 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);
}

Funtzioa new_server() sistema-deiek sortutako "zerbitzari" socketaren fitxategi-deskribatzailea itzultzen du socket(), bind() и listen() eta blokeorik gabeko moduan sarrerako konexioak onartzeko gai da.

Erakutsi new_server() funtzioa

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

  • Kontuan izan socket-a hasiera batean blokeatzerik gabeko moduan sortzen dela bandera erabiliz SOCK_NONBLOCKberaz, funtzioan on_accept() (gehiago irakurri) sistema-deia accept() ez zuen haria exekuzioa gelditu.
  • Bada reuse_port berdina da true, orduan funtzio honek socketa aukerarekin konfiguratuko du SO_REUSEPORT bidez setsockopt()ataka bera hari anitzeko ingurune batean erabiltzeko (ikus “Hari anitzeko zerbitzaria” atala).

Gertaeren kudeatzailea on_accept() OSak gertaera bat sortu ondoren deitua EPOLLIN, kasu honetan konexio berria onar daitekeela esan nahi du. on_accept() konexio berri bat onartzen du, blokeorik gabeko modura aldatzen du eta gertaeren kudeatzaile batekin erregistratzen da on_recv() I/O erreaktore batean.

Erakutsi on_accept() funtzioa

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

Gertaeren kudeatzailea on_recv() OSak gertaera bat sortu ondoren deitua EPOLLIN, kasu honetan konexioa erregistratu dela esan nahi du on_accept(), datuak jasotzeko prest.

on_recv() konexioko datuak irakurtzen ditu HTTP eskaera guztiz jaso arte, gero kudeatzaile bat erregistratzen du on_send() HTTP erantzuna bidaltzeko. Bezeroak konexioa hausten badu, socket-a deserregistratu eta itxi egingo da erabiliz close().

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

Gertaeren kudeatzailea on_send() OSak gertaera bat sortu ondoren deitua EPOLLOUT, hots, konexioa erregistratu dela on_recv(), datuak bidaltzeko prest. Funtzio honek HTML duen HTTP erantzun bat bidaltzen dio bezeroari irudi batekin eta gero gertaeren kudeatzailea berriro aldatzen du on_recv().

Erakutsi on_send() funtzioa

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

Eta azkenik, fitxategian http_server.c, funtzioan main() erabiliz I/O erreaktore bat sortzen dugu reactor_new(), sortu zerbitzari socket bat eta erregistratu, hasi erreaktorea erabiliz reactor_run() minutu batez zehatz-mehatz, eta gero baliabideak askatzen ditugu eta programatik irteten gara.

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

Egiaztatu dezagun dena espero bezala funtzionatzen duela. Konpilatzen (chmod a+x compile.sh && ./compile.sh proiektuaren erroan) eta abiarazi norberak idatzitako zerbitzaria, ireki http://127.0.0.1:18470 arakatzailean eta ikusi zer espero genuen:

Ezaugarri osoko bare-C I/O erreaktorea

Errendimenduaren neurketa

Erakutsi nire autoaren zehaztapenak

$ 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

Neur dezagun hari bakarreko zerbitzari baten errendimendua. Ireki ditzagun bi terminal: batean exekutatu egingo dugu ./http_server, beste batean - lan. Minutu bat igaro ondoren, hurrengo estatistikak bistaratuko dira bigarren terminalean:

$ 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

Gure hari bakarreko zerbitzariak minutuko 11 milioi eskaera baino gehiago prozesatu ahal izan zituen 100 konexioetatik sortutakoak. Ez da emaitza txarra, baina hobetu al daiteke?

Hari anitzeko zerbitzaria

Goian esan bezala, I/O erreaktorea hari bereizietan sor daiteke, horrela CPU nukleo guztiak erabiliz. Jar dezagun ikuspegi hau praktikan:

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

Orain hari bakoitza berea da erreaktorea:

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

Kontuan izan funtzioaren argumentua new_server() egintzak true. Horrek esan nahi du zerbitzariaren socketari aukera esleitzen diogula SO_REUSEPORThari anitzeko ingurune batean erabiltzeko. Xehetasun gehiago irakur ditzakezu Hemen.

Bigarren korrika

Orain neur dezagun hari anitzeko zerbitzari baten errendimendua:

$ 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

Minutu batean prozesatutako eskaera kopurua ~1 aldiz handitu da! Baina zenbaki biribila baino ~ 3.28 milioi falta ginen, beraz, saia gaitezen hori konpontzen.

Lehenik eta behin, ikus ditzagun sortutako estatistikak perfektua:

$ 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 Affinity erabiliz, bildumarekin -march=native, PGO, hits kopuruaren igoera cachea, areagotu MAX_EVENTS eta erabilera EPOLLET ez zuen errendimenduaren igoera nabarmenik eman. Baina zer gertatzen da aldibereko konexioen kopurua handitzen baduzu?

Aldi bereko 352 konexioren estatistikak:

$ 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

Nahi den emaitza lortu da, eta horrekin batera minutu 1ean prozesatutako eskaera kopurua konexio kopuruarekin duen mendekotasuna erakusten duen grafiko interesgarri bat:

Ezaugarri osoko bare-C I/O erreaktorea

Ehun bat konexioren ondoren, bi zerbitzarietarako prozesatutako eskaerak nabarmen jaisten direla ikusten dugu (hari anitzeko bertsioan hori nabarmenagoa da). Hau Linux TCP/IP pilaren ezarpenarekin erlazionatuta al dago? Idatzi aske grafikoaren portaera honi eta hari anitzeko eta hari bakarreko aukeren optimizazioei buruzko zure hipotesiak iruzkinetan.

Bezala adierazi iruzkinetan, errendimendu proba honek ez du I/O erreaktorearen portaera erakusten benetako kargapean, zerbitzariak ia beti datu-basearekin elkarreragiten duelako, erregistroak ateratzen dituelako, kriptografia erabiltzen duelako. TLS etab., horren ondorioz karga ez-uniformea ​​(dinamikoa) bihurtzen da. Hirugarrenen osagaiekin batera probak egingo dira I/O proaktoreari buruzko artikuluan.

I/O erreaktorearen desabantailak

Ulertu behar duzu I/O erreaktorea ez dagoela eragozpenik gabe, hots:

  • I/O erreaktore bat hari anitzeko ingurune batean erabiltzea zertxobait zailagoa da, zeren fluxuak eskuz kudeatu beharko dituzu.
  • Praktikak erakusten du kasu gehienetan karga ez-uniformea ​​dela, eta horrek hari bat erregistratzea ekar dezake beste bat lanean lanpetuta dagoen bitartean.
  • Gertaera-kudeatzaile batek hari bat blokeatzen badu, sistema-hautatzaileak berak ere blokeatuko du, eta horrek akatsak aurki ditzake.

Arazo hauek konpontzen ditu I/O proaktorea, askotan karga hari multzo batera banatzen duen programatzaile bat daukana eta API erosoagoa ere badu. Geroago hitz egingo dugu, nire beste artikuluan.

Ondorioa

Hor amaitu da teoriatik profiler ihesera zuzenean egin dugun bidaia.

Ez zenuke honetaz luzatu behar, sareko softwarea idazteko erosotasun eta abiadura maila ezberdinekin beste hainbat ikuspegi interesgarri daudelako. Interesgarriak, nire ustez, estekak behean ematen dira.

Hurrengo aldira arte!

Proiektu interesgarriak

Zer gehiago irakurri?

Iturria: www.habr.com

Gehitu iruzkin berria