Pilnas C I/O reaktorius

Pilnas C I/O reaktorius

įvedimas

I/O reaktorius (vieno sriegio renginio kilpa) yra didelės apkrovos programinės įrangos rašymo modelis, naudojamas daugelyje populiarių sprendimų:

Šiame straipsnyje apžvelgsime įvesties / išvesties reaktoriaus ypatybes ir trūkumus bei kaip jis veikia, parašysime įgyvendinimą per mažiau nei 200 kodo eilučių ir atliksime paprastą HTTP serverio procesą daugiau nei 40 milijonų užklausų per minutę.

pratarmė

  • Straipsnis buvo parašytas siekiant padėti suprasti įvesties / išvesties reaktoriaus veikimą ir suprasti jo naudojimo riziką.
  • Norint suprasti straipsnį, reikalingos pagrindinės žinios. C kalba ir šiek tiek patirties kuriant tinklo programas.
  • Visas kodas parašytas C kalba griežtai laikantis (Dėmesio: ilgas PDF) pagal C11 standartą skirta Linux ir pasiekiama GitHub.

Kodėl tai padaryti?

Augant interneto populiarumui, žiniatinklio serveriams vienu metu ėmė tvarkyti daug jungčių, todėl buvo išbandyti du būdai: blokuoti I/O daugelyje OS gijų ir neblokuoti I/O kartu su pranešimų apie įvykius sistema, dar vadinama „sistemos parinkikliu“ (epoll/kqueue/IOCP/tt).

Pirmasis būdas buvo sukurti naują OS giją kiekvienam gaunamam ryšiui. Jo trūkumas yra prastas mastelio keitimas: operacinė sistema turės įdiegti daugybę konteksto perėjimai и sistemos skambučiai. Tai yra brangios operacijos ir gali pritrūkti laisvos RAM su įspūdingu jungčių skaičiumi.

Modifikuota versija pabrėžia fiksuotas siūlų skaičius (gijų baseinas), tokiu būdu neleidžiant sistemai nutraukti vykdymo, tačiau kartu atsiranda nauja problema: jei gijų telkinys šiuo metu yra užblokuotas dėl ilgų skaitymo operacijų, kiti lizdai, kurie jau gali priimti duomenis, negalės daryk taip.

Antrasis metodas naudojamas pranešimų apie įvykius sistema (sistemos parinkiklis), kurį teikia OS. Šiame straipsnyje aptariamas dažniausiai naudojamas sistemos parinkiklis, pagrįstas įspėjimais (įvykiais, pranešimais) apie pasirengimą I/O operacijoms, o ne pranešimus apie jų užbaigimą. Supaprastintą jo naudojimo pavyzdį galima pavaizduoti šioje blokinėje diagramoje:

Pilnas C I/O reaktorius

Skirtumas tarp šių metodų yra toks:

  • I/O operacijų blokavimas sustabdyti vartotojų srautas ikikol OS tinkamai veikia defragmentai įeinantys IP paketai į baitų srautą (TCP, gaunantys duomenis), arba vidiniuose rašymo buferiuose nebus pakankamai vietos tolesniam siuntimui per NIC (duomenų siuntimas).
  • Sistemos parinkiklis su laiku praneša programai, kad OS jau defragmentuoti IP paketai (TCP, duomenų priėmimas) arba pakankamai vietos vidiniuose rašymo buferiuose jau prieinama (duomenų siuntimas).

Apibendrinant galima pasakyti, kad OS gijos rezervavimas kiekvienai įvesties/išvesties sistemai yra skaičiavimo galios švaistymas, nes iš tikrųjų gijos neatlieka naudingo darbo (taigi ir terminas "programinės įrangos pertraukimas"). Sistemos parinkiklis išsprendžia šią problemą, leisdamas vartotojo programai daug ekonomiškiau naudoti procesoriaus išteklius.

I/O reaktoriaus modelis

Įvesties / išvesties reaktorius veikia kaip sluoksnis tarp sistemos parinkiklio ir vartotojo kodo. Jo veikimo principas aprašytas šioje blokinėje diagramoje:

Pilnas C I/O reaktorius

  • Leiskite jums priminti, kad įvykis yra pranešimas, kad tam tikras lizdas gali atlikti neblokuojančią įvesties / išvesties operaciją.
  • Įvykių tvarkytojas yra funkcija, kurią iškviečia įvesties / išvesties reaktorius, kai gaunamas įvykis, ir tada atlieka neblokuojančią įvesties / išvesties operaciją.

Svarbu pažymėti, kad įvesties / išvesties reaktorius pagal apibrėžimą yra vienos sriegis, tačiau niekas netrukdo šiai koncepcijai naudoti kelių sriegių aplinkoje santykiu 1 gija: 1 reaktorius, taip perdirbant visas procesoriaus šerdis.

Vykdymas

Viešąją sąsają patalpinsime į failą reactor.h, o įgyvendinimas – in reactor.c. reactor.h sudarys iš šių pranešimų:

Rodyti deklaracijas reaktoriuje.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 reaktoriaus struktūra susideda iš failo aprašas parinkiklis epoll и maišos lentelės GHashTable, kuri susieja kiekvieną lizdą CallbackData (įvykių tvarkyklės struktūra ir vartotojo argumentas jai).

Rodyti reaktorių ir atgalinio skambučio duomenis

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

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

Atminkite, kad įgalinome galimybę tvarkyti nepilnas tipas pagal indeksą. IN reactor.h deklaruojame struktūrą reactorIr reactor.c mes jį apibrėžiame, taip neleisdami vartotojui aiškiai keisti jo laukų. Tai vienas iš modelių slepia duomenis, kuris glaustai patenka į C semantiką.

Funkcijos reactor_register, reactor_deregister и reactor_reregister atnaujinti dominančių lizdų ir atitinkamų įvykių tvarkyklių sąrašą sistemos parinkiklyje ir maišos lentelėje.

Rodyti registracijos funkcijas

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

Įvesties/išvesties reaktoriui sulaikius įvykį su aprašu fd, jis iškviečia atitinkamą įvykių tvarkyklę, kuriai perduodama fd, bitų kaukė sugeneruotus įvykius ir vartotojo žymeklį void.

Rodyti funkciją 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;
}

Apibendrinant galima pasakyti, kad funkcijų iškvietimų grandinė vartotojo kode bus tokia:

Pilnas C I/O reaktorius

Vienos gijos serveris

Norėdami patikrinti I/O reaktorių esant didelei apkrovai, parašysime paprastą HTTP žiniatinklio serverį, kuris į bet kokią užklausą atsakys vaizdu.

Greita nuoroda į HTTP protokolą

HTTP - tai yra protokolas taikymo lygis, pirmiausia naudojamas serverio ir naršyklės sąveikai.

HTTP gali būti lengvai naudojamas per transportas protokolas TCP, siųsti ir gauti pranešimus nurodytu formatu specifikacija.

Prašymo formatas

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

  • CRLF yra dviejų simbolių seka: r и n, atskiriant pirmą užklausos eilutę, antraštes ir duomenis.
  • <КОМАНДА> - vienas iš CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Naršyklė atsiųs komandą į mūsų serverį GET, reiškiantis „Atsiųskite man failo turinį“.
  • <URI> - vienodas išteklių identifikatorius. Pavyzdžiui, jei URI = /index.html, tada klientas paprašo pagrindinio svetainės puslapio.
  • <ВЕРСИЯ HTTP> — HTTP protokolo versija formatu HTTP/X.Y. Šiandien dažniausiai naudojama versija HTTP/1.1.
  • <ЗАГОЛОВОК N> yra rakto-reikšmių pora formatu <КЛЮЧ>: <ЗНАЧЕНИЕ>, išsiųstas į serverį tolesnei analizei.
  • <ДАННЫЕ> — duomenys, reikalingi serveriui operacijai atlikti. Dažnai tai paprasta JSON ar bet kokiu kitu formatu.

Atsakymo formatas

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

  • <КОД СТАТУСА> yra skaičius, nurodantis operacijos rezultatą. Mūsų serveris visada grąžins būseną 200 (sėkmingas veikimas).
  • <ОПИСАНИЕ СТАТУСА> — būsenos kodo eilutės atvaizdavimas. Būsenos kodui 200 tai yra OK.
  • <ЗАГОЛОВОК N> — to paties formato antraštė kaip ir prašyme. Mes grąžinsime titulus Content-Length (failo dydis) ir Content-Type: text/html (grąžinimo duomenų tipas).
  • <ДАННЫЕ> — vartotojo prašomi duomenys. Mūsų atveju tai yra kelias į vaizdą HTML.

byla http_server.c (vienos gijos serveris) apima failą common.h, kuriame yra šie funkcijų prototipai:

Rodyti bendrus funkcijų prototipus.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);

Taip pat aprašyta funkcinė makrokomanda SAFE_CALL() ir funkcija apibrėžta fail(). Makrokomandas lygina išraiškos reikšmę su klaida ir, jei sąlyga teisinga, iškviečia funkciją fail():

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

Funkcija fail() atspausdina perduotus argumentus į terminalą (pvz printf()) ir baigia programą su kodu 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);
}

Funkcija new_server() grąžina sistemos iškvietimų sukurto "serverio" lizdo failo aprašą socket(), bind() и listen() ir gali priimti įeinančius ryšius neblokuojančiu režimu.

Rodyti new_server() funkciją

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

  • Atminkite, kad lizdas iš pradžių sukuriamas neblokuojančiu režimu, naudojant vėliavėlę SOCK_NONBLOCKkad funkcijoje on_accept() (skaityti daugiau) sistemos skambutis accept() nesustabdė gijos vykdymo.
  • jei reuse_port yra lygus true, tada ši funkcija sukonfigūruos lizdą su parinktimi SO_REUSEPORT per setsockopt()naudoti tą patį prievadą kelių gijų aplinkoje (žr. skyrių „Kelių gijų serveris“).

Renginių vedėjas on_accept() iškviečiamas OS sugeneravus įvykį EPOLLIN, šiuo atveju reiškia, kad naujas ryšys gali būti priimtas. on_accept() priima naują ryšį, perjungia jį į neblokavimo režimą ir užsiregistruoja įvykių tvarkyklėje on_recv() I/O reaktoriuje.

Rodyti on_accept() funkciją

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

Renginių vedėjas on_recv() iškviečiamas OS sugeneravus įvykį EPOLLIN, šiuo atveju reiškia, kad ryšys užregistruotas on_accept(), paruoštas priimti duomenis.

on_recv() nuskaito duomenis iš ryšio, kol visiškai gaunama HTTP užklausa, tada užregistruoja tvarkyklę on_send() siųsti HTTP atsakymą. Jei klientas nutraukia ryšį, lizdas išregistruojamas ir uždaromas naudojant close().

Rodyti funkciją 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);
    }
}

Renginių vedėjas on_send() iškviečiamas OS sugeneravus įvykį EPOLLOUT, tai reiškia, kad ryšys užregistruotas on_recv(), paruoštas siųsti duomenis. Ši funkcija klientui siunčia HTTP atsakymą su HTML su vaizdu ir vėl pakeičia įvykių tvarkyklę į on_recv().

Rodyti funkciją 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);
}

Ir galiausiai, faile http_server.c, veikiantis main() sukuriame I/O reaktorių naudodami reactor_new(), sukurkite serverio lizdą ir užregistruokite jį, paleiskite reaktorių naudodami reactor_run() lygiai vieną minutę, o tada išleidžiame išteklius ir išeiname iš programos.

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

Patikrinkime, ar viskas veikia taip, kaip tikėtasi. Kompiliavimas (chmod a+x compile.sh && ./compile.sh projekto šaknyje) ir paleiskite savarankiškai parašytą serverį, atidarykite http://127.0.0.1:18470 naršyklėje ir pažiūrėkite, ko tikėjomės:

Pilnas C I/O reaktorius

Našumo matavimas

Rodyti mano automobilio specifikacijas

$ 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

Išmatuokime vienos gijos serverio našumą. Atidarykime du terminalus: viename paleisime ./http_server, kitaip - wrk. Po minutės antrajame terminale bus rodoma ši 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

Mūsų vienos gijos serveris galėjo apdoroti daugiau nei 11 milijonų užklausų per minutę, gautas iš 100 jungčių. Neblogas rezultatas, bet ar galima jį pagerinti?

Kelių gijų serveris

Kaip minėta pirmiau, įvesties / išvesties reaktorius gali būti sukurtas atskiromis gijomis, taip naudojant visas procesoriaus šerdis. Įgyvendinkime šį metodą praktiškai:

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

Dabar kiekviena gija turi savo reaktorius:

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

Atkreipkite dėmesį, kad funkcijos argumentas new_server() advokatai true. Tai reiškia, kad mes priskiriame parinktį serverio lizdui SO_REUSEPORTnaudoti jį kelių gijų aplinkoje. Galite perskaityti daugiau informacijos čia.

Antras bėgimas

Dabar išmatuokime kelių gijų serverio našumą:

$ 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

Per 1 minutę apdorotų užklausų skaičius išaugo ~3.28 karto! Tačiau iki apvalaus skaičiaus mums pritrūko tik ~XNUMX milijonų, todėl pabandykime tai ištaisyti.

Pirmiausia pažvelkime į sugeneruotą statistiką puikus:

$ 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 naudojimas, kompiliacija su -march=native, PGO, pataikymų skaičiaus padidėjimas talpykla, padidinti MAX_EVENTS ir naudoti EPOLLET nedavė reikšmingo našumo padidėjimo. Bet kas atsitiks, jei padidinsite vienalaikių ryšių skaičių?

352 prisijungimų vienu metu statistika:

$ 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

Gautas norimas rezultatas, o kartu ir įdomus grafikas, rodantis apdorotų užklausų skaičiaus per 1 minutę priklausomybę nuo jungčių skaičiaus:

Pilnas C I/O reaktorius

Matome, kad po poros šimtų prisijungimų abiejų serverių apdorojamų užklausų skaičius smarkiai krenta (daugiagija versijoje tai labiau pastebima). Ar tai susiję su Linux TCP/IP kamino diegimu? Nedvejodami rašykite komentaruose savo prielaidas apie tokį grafiko elgesį ir kelių gijų bei vienos gijos parinkčių optimizavimą.

Kaip pažymėjo komentaruose šis našumo testas neparodo I/O reaktoriaus elgsenos esant realioms apkrovoms, nes beveik visada serveris sąveikauja su duomenų baze, išveda žurnalus, naudoja kriptografiją su TLS ir kt., dėl ko apkrova tampa netolygi (dinaminė). Bandymai kartu su trečiųjų šalių komponentais bus atliekami straipsnyje apie I/O proaktorius.

I/O reaktoriaus trūkumai

Turite suprasti, kad I/O reaktorius neturi trūkumų, būtent:

  • Naudoti įvesties/išvesties reaktorių kelių gijų aplinkoje yra šiek tiek sunkiau, nes turėsite rankiniu būdu valdyti srautus.
  • Praktika rodo, kad daugeliu atvejų apkrova yra nevienoda, todėl viena gija gali užsiregistruoti, o kita užsiėmusi darbu.
  • Jei viena įvykių tvarkytoja blokuoja giją, blokuoja ir pats sistemos parinkiklis, o tai gali sukelti sunkiai aptinkamų klaidų.

Išsprendžia šias problemas I/O proaktorius, kuris dažnai turi planuoklį, kuris tolygiai paskirsto apkrovą gijų telkiniui, taip pat turi patogesnę API. Apie tai kalbėsime vėliau, kitame mano straipsnyje.

išvada

Štai čia ir baigėsi mūsų kelionė nuo teorijos iki profiliavimo išmetimo sistemos.

Neturėtumėte prie to pasilikti, nes yra daug kitų vienodai įdomių būdų, kaip rašyti tinklo programinę įrangą su skirtingu patogumo ir greičio lygiu. Įdomios, mano nuomone, žemiau pateiktos nuorodos.

Iki kito karto!

Įdomūs projektai

Ką dar skaityti?

Šaltinis: www.habr.com

Добавить комментарий