Voll-Fonctiounen bare-C I/O Reaktor

Voll-Fonctiounen bare-C I/O Reaktor

Aféierung

I/O Reakter (eenzel thread Event Loop) ass e Muster fir High-load Software ze schreiwen, a ville populäre Léisunge benotzt:

An dësem Artikel wäerte mir d'Ins an d'Outs vun engem I / O Reaktor kucken a wéi et funktionnéiert, eng Implementatioun a manner wéi 200 Zeilen Code schreiwen an en einfachen HTTP-Serverprozess iwwer 40 Milliounen Ufroen / min maachen.

Viruerteel

  • Den Artikel gouf geschriwwen fir de Fonctionnement vum I / O Reaktor ze verstoen, an dofir d'Risiken ze verstoen wann se se benotzt.
  • Wëssen iwwer d'Basis ass erfuerderlech fir den Artikel ze verstoen. C Sprooch an e puer Erfahrung an der Entwécklung vun der Netzwierkapplikatioun.
  • All Code ass an C Sprooch geschriwwen strikt no (opgepasst: laang PDF) zu C11 Norm fir Linux a verfügbar op GitHub.

Firwat ass dat néideg?

Mat der wuessender Popularitéit vum Internet hunn d'Webserver ugefaang eng grouss Zuel vu Verbindungen gläichzäiteg ze handhaben, an dofir goufen zwou Approche probéiert: I/O blockéieren op enger grousser Zuel vun OS Threads an net blockéierend I/O a Kombinatioun mat en Event Notifikatiounssystem, och "System Selector" genannt (epoll/kqueue/IOCP/etc).

Déi éischt Approche huet involvéiert en neien OS Thread fir all erakommen Verbindung ze kreéieren. Säin Nodeel ass eng schlecht Skalierbarkeet: de Betribssystem muss vill ëmsetzen Kontext Iwwergäng и System rifft. Si sinn deier Operatiounen a kënnen zu engem Mangel u gratis RAM mat enger impressionanter Unzuel u Verbindungen féieren.

Déi geännert Versioun Highlights fix Zuel vun thread (Thread Pool), doduerch verhënnert datt de System d'Ausféierung ofbriechen, awer gläichzäiteg en neie Problem aféieren: wann e thread Pool de Moment duerch laang Liesoperatioune blockéiert ass, da kënnen aner Sockets, déi scho fäeg sinn Daten ze kréien, net fäeg sinn maachen esou.

Déi zweet Approche benotzt Event Notifikatioun System (System Selector) vum OS geliwwert. Dësen Artikel diskutéiert déi allgemeng Aart vu Systemselektor, baséiert op Alarmer (Evenementer, Notifikatiounen) iwwer Bereetschaft fir I/O Operatiounen, anstatt op Notifikatiounen iwwer hir Fäerdegstellung. E vereinfacht Beispill vu senger Benotzung kann duerch de folgende Blockdiagram vertruede ginn:

Voll-Fonctiounen bare-C I/O Reaktor

Den Ënnerscheed tëscht dësen Approche ass wéi follegt:

  • Blockéieren I / O Operatiounen suspendéieren Benotzer Flux bisbis d'OS richteg ass defragmentéiert erakommen IP Pakete zu Byte Stream (TCP, Daten empfänken) oder et gëtt net genuch Plaz an den internen Schreifbuffer verfügbar fir spéider ze schécken via NIC (Daten schécken).
  • System selector am Zäitoflaf informéiert de Programm datt d'OS schonn defragmentéiert IP Pakete (TCP, Dateempfang) oder genuch Plaz an intern Schreifbuffer schonn verfügbar (Daten schécken).

Fir et ze resuméieren, en OS Thread fir all I/O ze reservéieren ass e Verschwendung vu Rechenkraaft, well a Wierklechkeet maachen d'Threads keng nëtzlech Aarbecht (dofir de Begrëff "Software Ënnerbriechung"). De Systemselektor léist dëse Problem, wat de Benotzerprogramm erlaabt CPU Ressourcen vill méi wirtschaftlech ze benotzen.

I/O Reaktormodell

Den I/O Reaktor handelt als Schicht tëscht dem Systemselektor an dem Benotzercode. De Prinzip vu senger Operatioun gëtt duerch de folgende Blockdiagramm beschriwwen:

Voll-Fonctiounen bare-C I/O Reaktor

  • Loosst mech Iech drun erënneren datt en Event eng Notifikatioun ass datt e bestëmmte Socket fäeg ass eng net blockéierend I/O Operatioun auszeféieren.
  • En Event Handler ass eng Funktioun déi vum I/O Reaktor genannt gëtt wann en Event kritt gëtt, deen dann eng net blockéierend I/O Operatioun ausféiert.

Et ass wichteg ze bemierken datt den I / O Reaktor per Definitioun Single-threaded ass, awer et gëtt näischt verhënnert datt d'Konzept an engem Multi-threaded Ëmfeld benotzt gëtt an engem Verhältnis vun 1 thread: 1 Reaktor, an doduerch all CPU Cores recycléiert.

Ëmsetzung

Mir setzen den ëffentlechen Interface an engem Fichier reactor.h, an Ëmsetzung - an reactor.c. reactor.h besteet aus folgenden Ukënnegungen:

Show Deklaratioune an 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);

D'I/O Reaktorstruktur besteet aus Dateibeschreiwung selector epoll и hash Dëscher GHashTable, déi all Socket Kaarten ze CallbackData (Struktur vun engem Event Handler an engem Benotzer Argument dofir).

Show Reactor a CallbackData

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

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

Maacht weg datt mir d'Fäegkeet aktivéiert hunn ze handhaben onkomplett Typ no dem Index. IN reactor.h mir erklären d'Struktur reactoran a reactor.c mir definéieren et, doduerch datt de Benotzer seng Felder explizit verännert. Dëst ass ee vun de Musteren verstoppt Donnéeën, déi präzis an d'C Semantik passt.

Functions reactor_register, reactor_deregister и reactor_reregister update d'Lëscht vun Sockets vun Interessi an entspriechend Event Handler am System selector an hash Dësch.

Show Aschreiwung Funktiounen

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

Nodeems den I/O Reaktor d'Evenement mam Deskriptor ofgefaangen huet fd, et rifft de entspriechende Event Handler, un déi et passéiert fd, bëssen Mask generéiert Evenementer an e Benotzer Pointer op void.

Show reactor_run () Funktioun

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

Fir ze resuméieren, wäert d'Kette vu Funktiounsruffen am Benotzercode déi folgend Form huelen:

Voll-Fonctiounen bare-C I/O Reaktor

Single threaded Server

Fir den I/O Reaktor ënner héijer Belaaschtung ze testen, schreiwen mir en einfachen HTTP Webserver deen op all Ufro mat engem Bild reagéiert.

Eng séier Referenz op den HTTP-Protokoll

HTTP - dëst ass de Protokoll Applikatioun Niveau, haaptsächlech fir Server-Browser Interaktioun benotzt.

HTTP kann einfach iwwer benotzt ginn Transport Protokoll TCP, Messagen an engem spezifizéierte Format schécken a kréien Spezifizéierung.

Ufro Format

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

  • CRLF ass eng Sequenz vun zwee Zeechen: r и n, Trennt déi éischt Zeil vun der Ufro, Header an Daten.
  • <КОМАНДА> - ee vun CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. De Browser schéckt e Kommando un eise Server GET, dat heescht "Schéckt mir den Inhalt vum Fichier."
  • <URI> - eenheetlech Ressource Identifizéierer. Zum Beispill, wann URI = /index.html, da freet de Client d'Haaptsäit vum Site.
  • <ВЕРСИЯ HTTP> - Versioun vum HTTP-Protokoll am Format HTTP/X.Y. Déi meescht benotzt Versioun haut ass HTTP/1.1.
  • <ЗАГОЛОВОК N> ass e Schlësselwäertpaar am Format <КЛЮЧ>: <ЗНАЧЕНИЕ>, op de Server geschéckt fir weider Analyse.
  • <ДАННЫЕ> - Daten erfuerderlech vum Server fir d'Operatioun auszeféieren. Oft ass et einfach Language oder all aner Format.

Äntwert Format

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

  • <КОД СТАТУСА> ass eng Zuel déi d'Resultat vun der Operatioun representéiert. Eise Server gëtt ëmmer Status 200 zréck (erfollegräich Operatioun).
  • <ОПИСАНИЕ СТАТУСА> - String Representatioun vum Statuscode. Fir Status Code 200 dëst ass OK.
  • <ЗАГОЛОВОК N> - Header vum selwechte Format wéi an der Ufro. Mir ginn d'Titelen zréck Content-Length (Dateigréisst) an Content-Type: text/html (zréck Datentyp).
  • <ДАННЫЕ> - Daten gefrot vum Benotzer. An eisem Fall ass dëst de Wee zum Bild an HTML.

Fichier http_server.c (Single threaded Server) enthält Datei common.h, déi folgend Funktiounsprototypen enthält:

Show Fonktioun Prototype gemeinsam.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 funktionnelle Makro gëtt och beschriwwen SAFE_CALL() an d'Funktioun ass definéiert fail(). De Makro vergläicht de Wäert vum Ausdrock mam Feeler, a wann d'Konditioun richteg ass, rifft d'Funktioun fail():

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

Funktioun fail() dréckt déi passéiert Argumenter op den Terminal (wéi printf()) a schléisst de Programm mam Code of 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);
}

Funktioun new_server() gëtt de Dateideskriptor vum "Server" Socket zréck, deen duerch Systemriff erstallt gëtt socket(), bind() и listen() a kapabel erakommen Verbindungen an engem net-blockéierende Modus ze akzeptéieren.

Show new_server () Funktioun

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

  • Bedenkt datt de Socket am Ufank am Net-Blockéierungsmodus erstallt gëtt mam Fändel SOCK_NONBLOCKsou datt an der Funktioun on_accept() (liest méi) System Opruff accept() huet d'Thread Ausféierung net gestoppt.
  • wann reuse_port ass gläich true, da wäert dës Funktioun de Socket mat der Optioun konfiguréieren SO_REUSEPORT duerch setsockopt()déi selwecht port an engem Multi-threaded Ëmfeld ze benotzen (kuckt Rubrik "Multi-threaded Server").

Event Handler on_accept() genannt nodeems d'OS en Event generéiert EPOLLIN, an dësem Fall bedeit datt déi nei Verbindung akzeptéiert ka ginn. on_accept() akzeptéiert eng nei Verbindung, wiesselt et op net-blockéierend Modus a registréiert mat engem Eventhandler on_recv() an engem I/O Reaktor.

Show on_accept () Funktioun

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() genannt nodeems d'OS en Event generéiert EPOLLIN, an dësem Fall bedeit datt d'Verbindung ugemellt ass on_accept(), prett fir Daten ze kréien.

on_recv() liest Daten aus der Verbindung bis d'HTTP-Ufro komplett kritt ass, da registréiert en Handler on_send() eng HTTP Äntwert ze schécken. Wann de Client d'Verbindung brécht, gëtt de Socket deregistréiert an zougemaach close().

Show Funktioun 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() genannt nodeems d'OS en Event generéiert EPOLLOUT, Bedeitung, datt d'Verbindung registréiert on_recv(), prett Daten ze schécken. Dës Funktioun schéckt eng HTTP Äntwert mat HTML mat engem Bild un de Client an ännert dann den Event Handler zréck op on_recv().

Show on_send () Funktioun

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

A schlussendlech am Dossier http_server.c, an der Funktioun main() mir schafen eng ech / O Reakter benotzt reactor_new(), erstellt e Server Socket a registréiert et, start de Reakter mat reactor_run() fir genee eng Minutt, an dann Fräisetzung mir Ressourcen an Sortie de Programm.

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

Loosst eis kucken ob alles funktionnéiert wéi erwaart. Zesummesetzung (chmod a+x compile.sh && ./compile.sh am Projet root) a starten de selbstgeschriwwe Server, oppen http://127.0.0.1:18470 am Browser a kuckt wat mir erwaart hunn:

Voll-Fonctiounen bare-C I/O Reaktor

Leeschtung Miessung

Show meng Auto Spezifikatioune

$ 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

Loosst eis d'Performance vun engem Single-threaded Server moossen. Loosst eis zwee Terminaler opmaachen: an engem lafe mir ./http_server, an enger anerer - wrk. No enger Minutt ginn déi folgend Statistiken am zweeten Terminal ugewisen:

$ 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

Eise Single-threaded Server konnt iwwer 11 Milliounen Ufroe pro Minutt veraarbecht, déi aus 100 Verbindungen stamen. Net e schlecht Resultat, awer kann et verbessert ginn?

Multithreaded Server

Wéi uewen erwähnt, kann den I/O Reaktor a getrennten Threads erstallt ginn, an doduerch all CPU Cores benotzen. Loosst eis dës Approche an d'Praxis ëmsetzen:

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

Elo all thread besëtzt seng eege Reakter:

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

Maacht weg datt d'Funktioun Argument new_server() gitt true. Dëst bedeit datt mir d'Optioun un de Server Socket zouginn SO_REUSEPORTet an engem Multi-threaded Ëmfeld ze benotzen. Dir kënnt méi Detailer liesen hei.

Zweet Laf

Loosst eis elo d'Performance vun engem Multi-threaded Server moossen:

$ 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

D'Zuel vun den Ufroen, déi an 1 Minutt veraarbecht ginn ass ëm ~3.28 Mol eropgaang! Awer mir waren nëmmen ~ XNUMX Millioune knapp vun der Ronn Zuel, also loosst eis probéieren dat ze fixéieren.

Als éischt kucke mer d'Statistiken déi generéiert ginn 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

Benotzt CPU Affinitéit, Zesummesetzung mat -march=native, PGO, eng Erhéijung vun der Zuel vun Hits cache, Erhéijung MAX_EVENTS a benotzen EPOLLET huet keng bedeitend Erhéijung vun der Leeschtung ginn. Awer wat geschitt wann Dir d'Zuel vun de simultane Verbindungen erhéicht?

Statistike fir 352 simultan Verbindungen:

$ 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

Dat gewënschte Resultat gouf kritt, an domat eng interessant Grafik, déi d'Ofhängegkeet vun der Unzuel vun de veraarbechten Ufroen an 1 Minutt vun der Unzuel vun de Verbindungen weist:

Voll-Fonctiounen bare-C I/O Reaktor

Mir gesinn datt no e puer honnert Verbindungen d'Zuel vun de veraarbechten Ufroe fir béid Server staark erofgeet (an der Multi-threaded Versioun ass dëst méi bemierkbar). Ass dëst Zesummenhang mat der Linux TCP / IP Stack Implementatioun? Fillt Iech gratis Är Viraussetzungen iwwer dëst Verhalen vun der Grafik an Optimisatiounen fir Multi-threaded an Single-threaded Optiounen an de Kommentaren ze schreiwen.

wéi bemierkt an de Kommentaren weist dësen Performance Test net d'Behuele vum I/O Reaktor ënner reale Lasten, well bal ëmmer de Server mat der Datebank interagéiert, Logbicher ausgëtt, benotzt Kryptografie mat TLS etc., als Resultat vun deem d'Laascht net eenheetlech gëtt (dynamesch). Tester zesumme mat Drëtt-Partei Komponente ginn am Artikel iwwer den I/O Proactor duerchgefouert.

Nodeeler vun ech / O Reakter

Dir musst verstoen datt den I/O Reaktor net ouni seng Nodeeler ass, nämlech:

  • En I/O Reaktor an engem Multi-threaded Ëmfeld ze benotzen ass e bësse méi schwéier, well Dir musst d'Flëss manuell managen.
  • D'Praxis weist datt d'Laascht an de meeschte Fäll net eenheetlech ass, wat zu engem Fuedemprotokoll kann féieren, während en aneren mat der Aarbecht beschäftegt ass.
  • Wann een Event-Handler e Fuedem blockéiert, blockéiert de Systemselektor selwer och, wat zu schwéier fonnte Bugs féiere kann.

Léist dës Problemer I/O Proaktor, déi dacks e Scheduler huet, deen d'Laascht gläichméisseg op e Pool vu Threads verdeelt, an och e méi prakteschen API huet. Mir wäerte méi spéit doriwwer schwätzen, a mengem aneren Artikel.

Konklusioun

Dëst ass wou eis Rees vun der Theorie direkt an de Profiler Auspuff op en Enn komm ass.

Dir sollt net op dëst wunnen, well et vill aner gläich interessant Approche fir Netzwierksoftware mat verschiddene Komfort a Geschwindegkeet ze schreiwen. Interessant, menger Meenung no, Linken ginn ënnendrënner.

Gitt dech erëm

Interessant Projeten

Wat soss ze liesen?

Source: will.com

Setzt e Commentaire