Full-featured bare-C I / O reactor

Full-featured bare-C I / O reactor

Ynlieding

I/O reaktor (single thread evenemint loop) is in patroan foar it skriuwen fan software mei hege lading, brûkt yn in protte populêre oplossingen:

Yn dit artikel sille wy sjen nei de ins en outs fan in I / O-reaktor en hoe't it wurket, skriuw in ymplemintaasje yn minder dan 200 rigels koade, en meitsje in ienfâldich HTTP-tsjinnerproses oer 40 miljoen oanfragen / min.

Foarwurd

  • It artikel is skreaun om te helpen it funksjonearjen fan 'e I / O-reaktor te begripen, en dêrom de risiko's te begripen by it brûken.
  • Kennis fan 'e basis is nedich om it artikel te begripen. C taal en wat ûnderfining yn ûntwikkeling fan netwurkapplikaasjes.
  • Alle koade is skreaun yn C-taal strikt neffens (foarsichtich: lange PDF) oan C11 standert foar Linux en beskikber op GitHub.

Wêrom is dat nedich?

Mei de tanimmende populariteit fan it ynternet begûnen webservers in grut oantal ferbiningen tagelyk te behanneljen, en dêrom waarden twa oanpakken besocht: blokkearjen fan I/O op in grut oantal OS-threads en net-blokkearjende I/O yn kombinaasje mei in systeem foar notifikaasje fan eveneminten, ek wol "systeemselektor" neamd (epoll/kqueue/IOCP/etc).

De earste oanpak befette it meitsjen fan in nije OS-thread foar elke ynkommende ferbining. It neidiel is minne skalberens: it bestjoeringssysteem sil in protte moatte ymplementearje kontekst oergongen и systeem oproppen. Se binne djoere operaasjes en kinne liede ta in gebrek oan frije RAM mei in yndrukwekkend oantal ferbinings.

De wizige ferzje hichtepunten fêst oantal triedden (thread pool), dêrmei foarkomt dat it systeem de útfiering ôfbrekke, mar tagelyk in nij probleem yntrodusearret: as in thread pool op it stuit blokkearre is troch lange lêzen operaasjes, dan kinne oare sockets dy't al gegevens ûntfange kinne net doch sa.

De twadde oanpak brûkt evenemint notifikaasje systeem (systeemselektor) levere troch it OS. Dit artikel besprekt it meast foarkommende type systeemselektor, basearre op warskôgings (eveneminten, notifikaasjes) oer reewilligens foar I/O-operaasjes, ynstee fan op notifikaasjes oer har foltôging. In ferienfâldige foarbyld fan it gebrûk kin wurde fertsjintwurdige troch it folgjende blokdiagram:

Full-featured bare-C I / O reactor

It ferskil tusken dizze oanpak is as folget:

  • Blocking I / O operaasjes suspend brûker flow oantoant it OS goed is defragments ynkommende IP pakketten byte stream (TCP, gegevens ûntfange) of d'r sil net genôch romte beskikber wêze yn 'e ynterne skriuwbuffers foar it folgjende ferstjoeren fia NIC (gegevens ferstjoere).
  • Systeem selector oer de tiid meldt it programma dat it OS al defragmentearre IP-pakketten (TCP, gegevensûntfangst) of genôch romte yn ynterne skriuwbuffers al beskikber (ferstjoere gegevens).

Om it gear te nimmen, it reservearjen fan in OS-thread foar elke I/O is in fergriemerij fan komputerkrêft, om't yn 'e realiteit de threaden gjin nuttich wurk dogge (dêrfandinne de term "software ûnderbrekking"). De systeemselektor lost dit probleem op, wêrtroch it brûkerprogramma CPU-boarnen folle ekonomysker kin brûke.

I / O reactor model

De I / O-reaktor fungearret as in laach tusken de systeemselektor en de brûkerskoade. It prinsipe fan syn wurking wurdt beskreaun troch it folgjende blokdiagram:

Full-featured bare-C I / O reactor

  • Lit my jo herinnerje dat in evenemint in notifikaasje is dat in bepaalde socket in net-blokkearjende I / O-operaasje kin útfiere.
  • In evenemint handler is in funksje neamd troch de I / O reaktor as in evenemint wurdt ûntfongen, dy't dan fiert in net-blocking I / O operaasje.

It is wichtich om te notearjen dat de I / O-reaktor per definysje single-threaded is, mar d'r is neat dat it konsept stopet om te brûken yn in multi-threaded omjouwing yn in ferhâlding fan 1 thread: 1 reaktor, en recycle dêrmei alle CPU-kearnen.

Ymplemintaasje

Wy sille de iepenbiere ynterface yn in bestân pleatse reactor.h, en ymplemintaasje - yn reactor.c. reactor.h sil bestean út de folgjende oankundigingen:

Lit deklaraasjes sjen yn reactor.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);

De I/O-reaktorstruktuer bestiet út triem descriptor selector epoll и hash tabellen GHashTable, dy't elke socket yn kaart bringt CallbackData (struktuer fan in evenemint handler en in brûker argumint foar it).

Lit Reactor en CallbackData sjen

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

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

Tink derom dat wy de mooglikheid hawwe ynskeakele om te behanneljen ûnfolsleine type neffens de yndeks. YN reactor.h wy ferklearje de struktuer reactor, en yn reactor.c wy definiearje it, sadat de brûker foarkomt om syn fjilden eksplisyt te feroarjen. Dit is ien fan 'e patroanen hiding gegevens, dy't koart yn C-semantyk past.

Funksjes reactor_register, reactor_deregister и reactor_reregister update de list fan sockets fan belang en korrespondearjende evenemint handlers yn it systeem selector en hash tabel.

Lit registraasje funksjes sjen

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

Nei't de I / O-reaktor it barren hat ûnderskept mei de deskriptor fd, neamt it de korrespondearjende evenemintehanneler, dêr't it troch giet fd, bytsje masker generearre eveneminten en in brûker oanwizer nei void.

Lit reactor_run () funksje sjen

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

Om gearfetsje, sil de ketting fan funksje-oanroppen yn brûkerskoade de folgjende foarm nimme:

Full-featured bare-C I / O reactor

Single threaded tsjinner

Om de I / O-reaktor ûnder hege lading te testen, sille wy in ienfâldige HTTP-webserver skriuwe dy't reagearret op elk fersyk mei in ôfbylding.

In rappe ferwizing nei it HTTP-protokol

HTTP - dit is it protokol applikaasje nivo, benammen brûkt foar tsjinner-blêder ynteraksje.

HTTP kin maklik oer brûkt wurde ferfier protokol TCP, ferstjoeren en ûntfangen fan berjochten yn in spesifisearre opmaak spesifikaasje.

Fersyk Format

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

  • CRLF is in sekwinsje fan twa karakters: r и n, skieden de earste line fan it fersyk, kopteksten en gegevens.
  • <КОМАНДА> - ien fan CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. De browser sil in kommando stjoere nei ús server GET, wat betsjut "Stjoer my de ynhâld fan it bestân."
  • <URI> - unifoarme boarne identifier. Bygelyks, as URI = /index.html, dan freget de kliïnt de haadside fan 'e side.
  • <ВЕРСИЯ HTTP> - ferzje fan it HTTP-protokol yn it formaat HTTP/X.Y. De meast brûkte ferzje hjoed is HTTP/1.1.
  • <ЗАГОЛОВОК N> is in kaai-wearde-pear yn it formaat <КЛЮЧ>: <ЗНАЧЕНИЕ>, stjoerd nei de tsjinner foar fierdere analyze.
  • <ДАННЫЕ> - gegevens nedich troch de tsjinner om de operaasje út te fieren. Faak is it ienfâldich JSON of in oar formaat.

Antwurd Format

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

  • <КОД СТАТУСА> is in nûmer dat it resultaat fan 'e operaasje fertsjintwurdiget. Us tsjinner sil altyd weromkomme status 200 (suksesfolle operaasje).
  • <ОПИСАНИЕ СТАТУСА> - string fertsjintwurdiging fan de status koade. Foar status koade 200 dit is OK.
  • <ЗАГОЛОВОК N> - koptekst fan itselde formaat as yn it fersyk. Wy sille de titels werombringe Content-Length (triemgrutte) en Content-Type: text/html (werom gegevens type).
  • <ДАННЫЕ> - gegevens oanfrege troch de brûker. Yn ús gefal is dit it paad nei it byld yn HTML.

file http_server.c (single threaded server) befettet triem common.h, dy't de folgjende funksjeprototypes befettet:

Lit funksje prototypes mienskiplik.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);

De funksjonele makro wurdt ek beskreaun SAFE_CALL() en de funksje is definiearre fail(). De makro fergeliket de wearde fan 'e útdrukking mei de flater, en as de betingst wier is, neamt de funksje fail():

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

function fail() print de trochjûne arguminten nei it terminal (lykas printf()) en beëiniget it programma mei de koade 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() jout de triembeskriuwing werom fan 'e "server" socket makke troch systeemoproppen socket(), bind() и listen() en yn steat om ynkommende ferbiningen te akseptearjen yn in net-blokkearjende modus.

Lit new_server () funksje sjen

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

  • Tink derom dat de socket yn earste ynstânsje is makke yn net-blokkearjende modus mei de flagge SOCK_NONBLOCKsadat yn 'e funksje on_accept() (lês mear) systeem oprop accept() net stopje de tried útfiering.
  • as reuse_port is gelyk oan true, dan sil dizze funksje de socket konfigurearje mei de opsje SO_REUSEPORT troch setsockopt()om deselde haven te brûken yn in multi-threaded omjouwing (sjoch paragraaf "Multi-threaded server").

Event Handler on_accept() neamd nei it OS generearret in evenemint EPOLLIN, yn dit gefal betsjut dat de nije ferbining kin wurde akseptearre. on_accept() akseptearret in nije ferbining, skeakelt it nei net-blokkearjende modus en registrearret mei in evenemint handler on_recv() yn in I/O-reaktor.

Toan on_accept () funksje

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

Event Handler on_recv() neamd nei it OS generearret in evenemint EPOLLIN, yn dit gefal betsjut dat de ferbining registrearre on_accept(), klear om gegevens te ûntfangen.

on_recv() lêst gegevens fan 'e ferbining oant it HTTP-fersyk folslein ûntfongen is, dan registrearret it in handler on_send() om in HTTP-antwurd te stjoeren. As de klant brekt de ferbining, de socket wurdt deregistrearre en sluten mei help close().

Funksje sjen litte 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);
    }
}

Event Handler on_send() neamd nei it OS generearret in evenemint EPOLLOUT, betsjut dat de ferbining registrearre on_recv(), klear om gegevens te ferstjoeren. Dizze funksje stjoert in HTTP-antwurd mei HTML mei in ôfbylding nei de kliïnt en feroaret dan de evenemintehanneler werom nei on_recv().

Toan on_send () funksje

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

En as lêste, yn 'e triem http_server.c, yn funksje main() wy meitsje in I / O reaktor mei help reactor_new(), meitsje in tsjinner socket en registrearje it, start de reaktor mei help reactor_run() foar krekt ien minút, en dan loslitte wy boarnen en ferlitte it programma.

Lit http_server.c sjen

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

Litte wy kontrolearje dat alles wurket lykas ferwachte. kompilearjen (chmod a+x compile.sh && ./compile.sh yn 'e projektroot) en start de selsskreaune tsjinner, iepenje http://127.0.0.1:18470 yn 'e browser en sjoch wat wy ferwachte:

Full-featured bare-C I / O reactor

Performance mjitting

Lit myn auto spesifikaasjes sjen

$ 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

Litte wy de prestaasjes mjitte fan in tsjinner mei ien thread. Litte wy twa terminals iepenje: yn ien sille wy rinne ./http_server, yn in oare - wrk. Nei in minút sille de folgjende statistiken wurde werjûn yn 'e twadde terminal:

$ 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

Us single-threaded tsjinner koe mear dan 11 miljoen oanfragen per minút ferwurkje dy't ûntstien binne fan 100 ferbiningen. Net in min resultaat, mar kin it ferbettere wurde?

Multithreaded tsjinner

Lykas hjirboppe neamd, kin de I / O-reaktor wurde makke yn aparte triedden, wêrtroch alle CPU-kearnen brûke. Litte wy dizze oanpak yn 'e praktyk bringe:

Lit http_server_multithreaded.c sjen

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

No elke tried hat syn eigen reaktor:

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

Tink derom dat de funksje argumint new_server() advokaten true. Dit betsjut dat wy de opsje tawize oan de tsjinner socket SO_REUSEPORTom it te brûken yn in multi-threaded omjouwing. Jo kinne mear details lêze hjir.

Twadde run

Litte wy no de prestaasjes fan in multi-threaded tsjinner mjitte:

$ 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

It oantal oanfragen ferwurke yn 1 minút tanommen mei ~3.28 kear! Mar wy wiene mar ~ XNUMX miljoen tekoart oan it rûne nûmer, dus litte wy besykje dat te reparearjen.

Litte wy earst sjen nei de generearre statistiken perfekt:

$ 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

It brûken fan CPU Affinity, kompilaasje mei -march=native, PGO, in tanimming fan it oantal hits cache, tanimme MAX_EVENTS en gebrûk EPOLLET joech gjin signifikante ferheging fan prestaasjes. Mar wat bart der as jo it oantal simultane ferbiningen ferheegje?

Statistiken foar 352 simultane ferbiningen:

$ 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

It winske resultaat waard krigen, en dêrmei in nijsgjirrige grafyk dy't de ôfhinklikens fan it oantal ferwurke oanfragen yn 1 minút sjen lit fan it oantal ferbiningen:

Full-featured bare-C I / O reactor

Wy sjogge dat nei in pear hûndert ferbiningen it oantal ferwurke oanfragen foar beide servers skerp sakket (yn 'e multi-threaded ferzje is dit mear opfallend). Is dit relatearre oan de ymplemintaasje fan Linux TCP/IP-stapel? Fiel jo frij om jo oannames te skriuwen oer dit gedrach fan 'e grafyk en optimisaasjes foar opsjes mei meardere triedden en single-threaded yn' e kommentaren.

Hoe notearre yn 'e kommentaren lit dizze prestaasjetest it gedrach fan' e I/O-reaktor net sjen ûnder echte loads, om't hast altyd de tsjinner ynteraksje mei de databank, logs útfiert, brûkt kryptografy mei TLS ensfh., dêrtroch wurdt de lading net-unifoarm (dynamysk). Tests tegearre mei komponinten fan tredden sille wurde útfierd yn it artikel oer de I / O proactor.

Neidielen fan I / O reactor

Jo moatte begripe dat de I / O-reaktor net sûnder syn neidielen is, nammentlik:

  • It brûken fan in I / O-reaktor yn in multi-threaded omjouwing is wat dreger, omdat jo moatte de streamen manuell beheare.
  • Praktyk docht bliken dat de lading yn 'e measte gefallen net-unifoarm is, wat liede kin ta it loggen fan ien tried, wylst in oar dwaande is mei wurk.
  • As ien barrenhanneler in thread blokkearret, dan sil de systeemselektor sels ek blokkearje, wat kin liede ta hurd te finen bugs.

Lost dizze problemen op I/O proactor, dy't faaks in planner hat dy't de lading evenredich ferdield nei in pool fan triedden, en hat ek in handiger API. Wy sille der letter oer prate, yn myn oare artikel.

konklúzje

Dit is wêr't ús reis fan teory rjocht yn 'e profilerútlaat ta in ein kaam is.

Jo moatte hjir net oer stilhâlde, om't d'r in protte oare like nijsgjirrige oanpak binne foar it skriuwen fan netwurksoftware mei ferskate nivo's fan gemak en snelheid. Ynteressant, nei myn miening, wurde links hjirûnder jûn.

Oant gau!

Nijsgjirrige projekten

Wat moat ik oars lêze?

Boarne: www.habr.com

Add a comment