Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Pîrozbahiyê

Reaktora I/O (yek têl çerxa bûyerê) ji bo nivîsandina nermalava bargiraniyê nimûneyek e, ku di gelek çareseriyên populer de tê bikar anîn:

Di vê gotarê de, em ê li der û hundurên reaktorek I/O û çawa dixebite binihêrin, di kêmtirî 200 rêzikên kodê de pêkanînek binivîsin, û pêvajoyek servera HTTP-ya hêsan a li ser 40 mîlyon daxwaz / hûrdem çêkin.

Pêşniyar

  • Gotar hate nivîsandin ku ji bo fêmkirina fonksiyona reaktora I/O-yê alîkar be, û ji ber vê yekê xetereyên dema karanîna wê fêm bikin.
  • Ji bo fêmkirina gotarê zanîna bingehîn hewce ye. ziman C û hin ezmûn di pêşkeftina serîlêdana torê de.
  • Hemî kod bi zimanê C bi hişkî li gorî (hişyarî: PDF dirêj) standarda C11 ji bo Linux û li ser heye GitHub.

Çima ev hewce ye?

Bi mezinbûna populerbûna Înternetê re, pêşkêşkerên malperê dest pê kirin ku hewce ne ku bi hevdemî hejmareke mezin ji girêdanan re mijûl bibin, û ji ber vê yekê du nêzîkatî hatin ceribandin: astengkirina I/O li ser hejmareke mezin ji mijarên OS-ê û ne-astengkirina I/O bi hev re. pergalek agahdarkirina bûyerê, ku jê re "hilbijêrê pergalê" jî tê gotin (epoll/kqueue/IOCP/ hwd.).

Nêzîkatiya yekem ji bo her pêwendiya gihîştî diafirîne mijarek OS-ya nû. Kêmasiya wê pîvandina qels e: pergala xebitandinê dê neçar be ku gelekan bicîh bîne veguherînên çarçoveyê и bangên pergalê. Ew operasyonên biha ne û dikarin bibin sedema kêmbûna RAM-a belaş bi hejmareke balkêş a girêdanan.

Guhertoya guherbar ronî dike hejmara sabît ji mijarên (hewza Mijarê), bi vî rengî rê nade ku pergalê înfazê rawestîne, lê di heman demê de pirsgirêkek nû destnîşan dike: heke hewzek tîrêjê niha ji hêla operasyonên xwendina dirêj ve were asteng kirin, wê hingê soketên din ên ku berê dikarin daneyan bistînin dê nikaribin wisa bike.

Rêbaza duyemîn bikar tîne pergala ragihandina bûyerê (hilbijêrê pergalê) ku ji hêla OS-ê ve hatî peyda kirin. Ev gotar li ser bingeha hişyariyên (bûyer, agahdarî) di derbarê amadebûna ji bo operasyonên I/O de, li şûna li ser bingeha herî gelemperî celebê hilbijêra pergalê nîqaş dike. notifications li ser temamkirina wan. Nimûneyek hêsan a karanîna wê dikare bi diyagrama blokê ya jêrîn were destnîşan kirin:

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Cûdahiya van rêbazan wiha ye:

  • Astengkirina operasyonên I/O dardekirin herikîna bikarhêner taheta ku OS rast e defragments hatin pakêtên IP ji bo byte stream (TCP, wergirtina daneyan) an dê di tamponên nivîsandina hundurîn de cîhek têr tune be ji bo şandina paşê bi rêya NOTHING (daneyên şandin).
  • Hilbijêra pergalê bi derbasbûna demê bernameya ku OS agahdar dike êdî pakêtên IP-ê yên defragmented (TCP, wergirtina daneyê) an cîhê têr di tamponên nivîsandina navxweyî de êdî heye (dane şandin).

Bi kurtî, veqetandina têlek OS-ê ji bo her I/O windakirina hêza hesabkirinê ye, ji ber ku di rastiyê de, xêzan karê kêrhatî nakin (ji ber vê yekê term "navbera nermalavê"). Hilbijêra pergalê vê pirsgirêkê çareser dike, dihêle ku bernameya bikarhêner çavkaniyên CPU-ê pir aborîtir bikar bîne.

I/O modela reaktorê

Reaktora I/O wekî qatek di navbera hilbijêra pergalê û koda bikarhêner de tevdigere. Prensîba xebata wê ji hêla diyagrama bloka jêrîn ve tête diyar kirin:

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

  • Bihêle ez ji we re bi bîr bînim ku bûyerek agahdariyek e ku hin soketek karibe karek I/O ya ne-astengker bike.
  • Rêvekera bûyerê fonksiyonek e ku dema bûyerek tê wergirtin ji hêla reaktora I/O ve tê gotin, ku dûv re karek I/O ya ne-astengkirî pêk tîne.

Girîng e ku were zanîn ku reaktora I/O ji hêla pênaseyê ve yek-têlek e, lê tiştek rê li ber vê yekê nagire ku têgîn di hawîrdorek pir-mijarî de bi rêjeya 1 Mijar: 1 reaktor were bikar anîn, bi vî rengî hemî navikên CPU-ê ji nû ve vezîvirîne.

Реализация

Em ê pêwendiya gelemperî di pelê de cîh bikin reactor.h, û pêkanîn - di reactor.c. reactor.h dê ji van daxuyaniyên jêrîn pêk were:

Daxuyaniyên di reaktorê de nîşan bide.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);

Avahiya reaktora I/O ji pêk tê ravekerê pelê hilbijêr epoll и maseyên hash GHashTable, ku nexşeya her soketê li ser dike CallbackData (avahiya hilgirê bûyerê û argûkek bikarhêner ji bo wê).

Reactor û CallbackData nîşan bide

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

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

Ji kerema xwe bala xwe bidin ku me şiyana hilanînê çalak kiriye tîpa netemam li gor index. LI reactor.h em avaniyê radigihînin reactor, û bi reactor.c em wê pênase dikin, bi vî rengî rê nadin ku bikarhêner bi eşkere zeviyên xwe biguhezîne. Ev yek ji nimûneyan e daneyan vedişêre, ku bi kurtî di nav semantîka C de cih digire.

Karkerên reactor_register, reactor_deregister и reactor_reregister Di hilbijêra pergalê û tabloya haş de navnîşa soketên berjewendî û rêvebirên bûyerê yên têkildar nûve bikin.

Fonksiyonên qeydkirinê nîşan bide

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

Piştî ku reaktora I/O bûyerê bi ravekerê vegirt fd, ew gazî birêvebirê bûyerê yê têkildar dike, ku jê re derbas dibe fd, bit mask bûyerên çêkirî û nîşanek bikarhênerek void.

Fonksiyona reactor_run() nîşan bide

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

Bi kurtasî, zincîra bangên fonksiyonê di koda bikarhêner de dê forma jêrîn bigire:

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Pêşkêşkara yekane

Ji bo ceribandina reaktora I/O di bin barek zêde de, em ê web serverek HTTP-ya hêsan binivîsin ku bi wêneyek bersivê dide her daxwazê.

Referansek bilez a protokola HTTP

HTTP - ev protokol e asta serîlêdanê, di serî de ji bo têkiliya server-gerokê tê bikar anîn.

HTTP dikare bi hêsanî were bikar anîn neqilkirin protokol TCP, şandin û wergirtina peyaman di forma diyarkirî de specification.

Daxwaza Forma

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

  • CRLF rêzeka du tîpan e: r и n, rêza yekem a daxwazê, sernav û daneyan ji hev vediqetîne.
  • <КОМАНДА> - yek ji CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Gerok dê fermanek ji servera me re bişîne GET, tê wateya "Naveroka pelê ji min re bişîne."
  • <URI> - nasnameya çavkaniya yekgirtî. Mînakî, heke URI = /index.html, paşê xerîdar rûpela sereke ya malperê daxwaz dike.
  • <ВЕРСИЯ HTTP> - guhertoya protokola HTTP-ê di formatê de HTTP/X.Y. Guhertoya ku îro herî zêde tê bikar anîn ev e HTTP/1.1.
  • <ЗАГОЛОВОК N> di formatê de cotek key-nirx e <КЛЮЧ>: <ЗНАЧЕНИЕ>, ji bo analîzkirina bêtir ji serverê re şandin.
  • <ДАННЫЕ> - Daneyên ku ji hêla serverê ve hewce dike ku operasyonê pêk bîne. Gelek caran ew hêsan e JSON an her formatek din.

Forma bersivê

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

  • <КОД СТАТУСА> hejmareke ku encama operasyonê temsîl dike. Pêşkêşkara me dê her gav statûya 200 vegerîne (operasyona serketî).
  • <ОПИСАНИЕ СТАТУСА> - temsîla rêzê ya koda statûyê. Ji bo koda statûyê 200 ev e OK.
  • <ЗАГОЛОВОК N> - sernivîsa heman forma ku di daxwazê ​​de ye. Em ê sernavan vegerînin Content-Length (mezinahiya pelê) û Content-Type: text/html (cureyê daneya vegere).
  • <ДАННЫЕ> - Daneyên ku ji hêla bikarhêner ve hatî xwestin. Di rewşa me de, ev riya wêneyê tê de ye HTML.

file http_server.c (Pêşkêşkera yekane) pelê vedihewîne common.h, ku prototîpên fonksiyonê yên jêrîn dihewîne:

Prototîpên fonksiyonê yên hevpar nîşan bidin.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);

Makroya fonksiyonel jî tête diyar kirin SAFE_CALL() û fonksiyonê tête diyar kirin fail(). Makro nirxa îfadeyê bi xeletiyê re berhev dike, û heke şert rast be, fonksiyonê vedixwîne fail():

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

function fail() argumanên derbasbûyî li termînalê çap dike (wek printf()) û bernameyê bi kodê diqedîne 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() ravekera pelê ya soketa "server" ku ji hêla bangên pergalê ve hatî çêkirin vedigerîne socket(), bind() и listen() û karibe girêdanên hatinê di moda ne-astengkirinê de qebûl bike.

Fonksiyona new_server() nîşan bide

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

  • Têbînî ku soket di destpêkê de di moda ne-astengkirinê de bi karanîna ala ve hatî afirandin SOCK_NONBLOCKda ku di fonksiyonê de on_accept() (bixwîne) banga pergalê accept() înfaza mijarê nesekinî.
  • ger reuse_port wekhev e true, wê hingê ev fonksiyon dê soketê bi vebijarkê mîheng bike SO_REUSEPORT bi rêya setsockopt()ji bo ku heman portê di hawîrdorek pir-mijarî de bikar bînin (binihêrin beşa "Pêşkêşkera pir-têl").

Event Handler on_accept() piştî ku OS bûyerek çêdike tê gotin EPOLLIN, di vê rewşê de tê wateya ku girêdana nû dikare were pejirandin. on_accept() pêwendiyek nû qebûl dike, wê diguhezîne moda ne-astengkirinê û bi rêvekerek bûyerê re qeyd dike on_recv() di reaktoreke I/O de.

Fonksiyona on_accept() nîşan bide

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() piştî ku OS bûyerek çêdike tê gotin EPOLLIN, di vê rewşê de tê wateya ku girêdana qeydkirî ye on_accept(), ji bo wergirtina daneyan amade ye.

on_recv() Daneyên ji pêwendiyê dixwîne heya ku daxwaza HTTP bi tevahî neyê wergirtin, dûv re ew hilberek tomar dike on_send() ku bersivek HTTP bişîne. Ger xerîdar pêwendiyê bişkîne, soket tê rakirin û bi kar tê girtin close().

Fonksiyonê nîşan bide 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() piştî ku OS bûyerek çêdike tê gotin EPOLLOUT, tê wateya ku pêwendiya qeydkirî ye on_recv(), ji bo şandina daneyan amade ye. Vê fonksiyonê bersivek HTTP ya ku HTML-ê dihewîne bi wêneyek ji xerîdar re dişîne û dûv re rêvebirê bûyerê vedigere on_recv().

Fonksiyona on_send() nîşan bide

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

Û di dawiyê de, di pelê de http_server.c, di fonksiyonê de main() em bi karanîna reaktorek I/O ava dikin reactor_new(), soketek serverê biafirînin û wê tomar bikin, reaktorê bikar bînin dest pê bikin reactor_run() tam yek deqîqe, û dûv re em çavkaniyan berdidin û ji bernameyê derdikevin.

http_server.c nîşan bide

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

Werin em kontrol bikin ku her tişt wekî ku tê hêvî kirin dixebite. Berhevkirin (chmod a+x compile.sh && ./compile.sh di koka projeyê de) û servera xwe-nivîsandî dest pê bikin, vekin http://127.0.0.1:18470 di gerokê de û bibînin ka em çi hêvî dikin:

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Pîvana performansê

Taybetmendiyên gerîdeya min nîşan bide

$ 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

Werin em performansa serverek yek-têkilî bipîvin. Ka em du termînalan vekin: di yekê de em ê birevin ./http_server, bi awayekî cuda - wink. Piştî deqeyek, statîstîkên jêrîn dê di termînala duyemîn de bêne xuyang kirin:

$ 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

Pêşkêşkara meya yek-têkilî karîbû di her hûrdemê de zêdetirî 11 mîlyon daxwazên ku ji 100 girêdanan derketine pêvajo bike. Ne encamek xirab e, lê gelo ew dikare çêtir bibe?

Pêşkêşkara pir-threaded

Wekî ku li jor hatî behs kirin, reaktora I/O dikare di mijarên cûda de were afirandin, bi vî rengî hemî navikên CPU bikar bînin. Werin em vê nêzîkatiyê bixin pratîkê:

http_server_multithreaded.c nîşan bide

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

Niha her mijar xwediyê xwe ye rîaktor:

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

Ji kerema xwe not bikin ku argumana fonksiyonê new_server() parêzvanan true. Ev tê vê wateyê ku em vebijarkê ji bo soketa serverê veqetînin SO_REUSEPORTji bo ku wê di hawîrdorek pir-mijarî de bikar bînin. Hûn dikarin bêtir agahdarî bixwînin vir.

Rêza duyemîn

Naha werin em performansa serverek pir-mijarî bipîvin:

$ 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

Hejmara daxwazên ku di 1 hûrdemê de hatine kirin ~ 3.28 carî zêde bû! Lê em tenê ~ XNUMX mîlyon ji jimareya dorpêçê kêm bûn, ji ber vê yekê em hewl bidin ku wê rast bikin.

Pêşî em li statîstîkên ku hatine çêkirin binêrin lhevderketî:

$ 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

Bikaranîna CPU Affinity, berhevkirin bi -march=native, PGO, zêdebûna hejmara lêdan cache, zêde kirin MAX_EVENTS û bikar bînin EPOLLET di performansê de zêdebûnek girîng neda. Lê eger hûn hejmara girêdanên hevdem zêde bikin çi dibe?

Statîstîkên ji bo 352 girêdanên hevdem:

$ 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

Encama xwestî hate bidestxistin, û bi wê re grafiyek balkêş ku girêdayîbûna hejmara daxwazên pêvajoyî di 1 hûrdemê de li ser hejmara girêdanan nîşan dide:

Reaktora I/O ya bare-C ya bi tevahî taybetmendî

Em dibînin ku piştî çend sed girêdanan, jimara daxwazên pêvajoyî yên ji bo her du serveran bi tundî dadikeve (di guhertoya pir-mijarî de ev bêtir xuyang e). Ma ev bi pêkanîna stackê ya Linux TCP/IP ve girêdayî ye? Di şîroveyan de hûn gumanên xwe yên di derbarê vê tevgera grafîkê û xweşbîniyên ji bo vebijarkên pir-mijal û yek-têlan de binivîsin.

çawa diyar kirin di şîroveyan de, ev ceribandina performansê tevgera reaktora I/O di bin barkirinên rastîn de nîşan nade, ji ber ku hema hema her gav server bi databasê re têkildar dibe, têketin derdixe, krîptografiyê bi kar tîne. TLS hwd., wekî encamek ku bark ne-yekhev (dînamîk) dibe. Dê di gotara li ser proaktora I/O de bi hev re bi pêkhateyên sêyemîn re ceribandin werin kirin.

Dezawantajên reaktora I/O

Pêdivî ye ku hûn fêhm bikin ku reaktora I/O ne bê kêmasiyên wê ye, yanî:

  • Bikaranîna reaktorek I/O di hawîrdorek pir-mijarî de hinekî dijwartir e, ji ber hûn ê neçar bibin ku bi destan herikînan birêve bibin.
  • Pratîk destnîşan dike ku di pir rewşan de bar ne-yekhev e, ku dikare bibe sedema têketinek yek di dema ku yekî din bi kar re mijûl e.
  • Ger yek rêvebirê bûyerê mijarek bloke bike, hilbijêra pergalê bixwe jî dê asteng bike, ku dikare bibe sedema xeletiyên ku dijwar têne dîtin.

Van pirsgirêkan çareser dike I/O proactor, ya ku bi gelemperî xwedan nexşeyek e ku bi rengek wekhev barkirinê li hewzek têlan belav dike, û di heman demê de xwedan API-yek hêsantir e. Em ê li ser wê paşê, di gotara xwe ya din de biaxivin.

encamê

Li vir rêwîtiya me ya ji teoriyê rasterast berbi eksê profîlerê bi dawî bûye.

Pêdivî ye ku hûn li ser vê yekê nesekinin, ji ber ku ji bo nivîsandina nermalava torê ya bi astên cûda yên rehetî û bilez gelek nêzîkatiyên din ên wekhev balkêş hene. Balkêş e, bi dîtina min, girêdan li jêr têne dayîn.

Heta paşê!

Projeyên balkêş

Din ez çi bixwînim?

Source: www.habr.com

Add a comment