Adweithydd I/O noeth llawn sylw

Adweithydd I/O noeth llawn sylw

Cyflwyniad

adweithydd I/O (edau sengl dolen digwyddiad) yn batrwm ar gyfer ysgrifennu meddalwedd llwyth uchel, a ddefnyddir mewn llawer o atebion poblogaidd:

Yn yr erthygl hon, byddwn yn edrych ar i mewn a thu allan adweithydd I / O a sut mae'n gweithio, yn ysgrifennu gweithrediad mewn llai na 200 llinell o god, ac yn gwneud proses gweinydd HTTP syml dros 40 miliwn o geisiadau / mun.

Rhagair

  • Ysgrifennwyd yr erthygl i helpu i ddeall gweithrediad yr adweithydd I/O, ac felly i ddeall y risgiau wrth ei ddefnyddio.
  • Mae angen gwybodaeth o'r pethau sylfaenol i ddeall yr erthygl. C iaith a pheth profiad o ddatblygu cymwysiadau rhwydwaith.
  • Mae'r holl god wedi'i ysgrifennu yn iaith C yn unol â (rhybudd: PDF hir) i safon C11 ar gyfer Linux ac ar gael ar GitHub.

Pam mae angen hyn?

Gyda phoblogrwydd cynyddol y Rhyngrwyd, dechreuodd gweinyddwyr gwe fod angen trin nifer fawr o gysylltiadau ar yr un pryd, ac felly rhoddwyd cynnig ar ddau ddull: blocio I/O ar nifer fawr o edafedd OS a pheidio â rhwystro I/O ar y cyd â system hysbysu digwyddiad, a elwir hefyd yn "ddewisydd system" (epol/kciw/IOCP/etc).

Roedd y dull cyntaf yn cynnwys creu edefyn OS newydd ar gyfer pob cysylltiad sy'n dod i mewn. Ei anfantais yw scalability gwael: bydd yn rhaid i'r system weithredu weithredu llawer trawsnewidiadau cyd-destun и galwadau system. Maent yn weithrediadau drud a gallant arwain at ddiffyg RAM am ddim gyda nifer drawiadol o gysylltiadau.

Mae'r fersiwn addasedig yn amlygu nifer sefydlog o edafedd (pwll edau), a thrwy hynny atal y system rhag erthylu gweithredu, ond ar yr un pryd yn cyflwyno problem newydd: os yw pwll edau yn cael ei rwystro ar hyn o bryd gan weithrediadau darllen hir, yna ni fydd socedi eraill sydd eisoes yn gallu derbyn data yn gallu gwneud hynny.

Mae'r ail ddull yn defnyddio system hysbysu digwyddiad (detholwr system) a ddarperir gan yr OS. Mae'r erthygl hon yn trafod y math mwyaf cyffredin o ddewiswr system, yn seiliedig ar rybuddion (digwyddiadau, hysbysiadau) ynghylch parodrwydd ar gyfer gweithrediadau I/O, yn hytrach nag ar hysbysiadau am eu cwblhau. Gellir cynrychioli enghraifft symlach o'i ddefnydd gan y diagram bloc canlynol:

Adweithydd I/O noeth llawn sylw

Mae'r gwahaniaeth rhwng y dulliau hyn fel a ganlyn:

  • Rhwystro gweithrediadau I/O atal llif defnyddiwr nesnes bod yr OS yn iawn defragments yn dod i mewn Pecynnau IP i ffrwd beit (TCP, derbyn data) neu ni fydd digon o le ar gael yn y byfferau ysgrifennu mewnol ar gyfer anfon ymlaen wedyn NIC (anfon data).
  • Dewisydd system dros amser yn hysbysu'r rhaglen bod yr OS eisoes pecynnau IP wedi'u darnio (TCP, derbyn data) neu ddigon o le mewn byfferau ysgrifennu mewnol eisoes ar gael (anfon data).

I grynhoi, mae cadw edefyn OS ar gyfer pob I/O yn wastraff o bŵer cyfrifiadurol, oherwydd mewn gwirionedd, nid yw'r edafedd yn gwneud gwaith defnyddiol (a dyna pam y mae'r term "toriad meddalwedd"). Mae'r dewisydd system yn datrys y broblem hon, gan ganiatáu i'r rhaglen ddefnyddwyr ddefnyddio adnoddau CPU yn llawer mwy darbodus.

Model adweithydd I/O

Mae'r adweithydd I/O yn gweithredu fel haen rhwng y dewisydd system a'r cod defnyddiwr. Disgrifir egwyddor ei weithrediad gan y diagram bloc canlynol:

Adweithydd I/O noeth llawn sylw

  • Gadewch imi eich atgoffa bod digwyddiad yn hysbysiad bod soced penodol yn gallu cyflawni gweithrediad I/O nad yw'n rhwystro.
  • Mae triniwr digwyddiad yn swyddogaeth a elwir gan yr adweithydd I/O pan dderbynnir digwyddiad, sydd wedyn yn cyflawni gweithrediad I/O nad yw'n rhwystro.

Mae'n bwysig nodi bod yr adweithydd I/O trwy ddiffiniad yn un edau, ond nid oes dim yn atal y cysyniad rhag cael ei ddefnyddio mewn amgylchedd aml-edau ar gymhareb o adweithydd 1 edau: 1, a thrwy hynny ailgylchu holl greiddiau CPU.

Gweithredu

Byddwn yn gosod y rhyngwyneb cyhoeddus mewn ffeil reactor.h, a gweithredu - yn reactor.c. reactor.h bydd yn cynnwys y cyhoeddiadau a ganlyn:

Dangos datganiadau 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);

Mae strwythur yr adweithydd I/O yn cynnwys disgrifydd ffeil detholwr epol и byrddau stwnsh GHashTable, sy'n mapio pob soced i CallbackData (strwythur trafodwr digwyddiad a dadl defnyddiwr ar ei gyfer).

Dangos Data Adweithydd a Alwad

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

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

Sylwch ein bod wedi galluogi'r gallu i drin math anghyflawn yn ôl y mynegai. YN reactor.h rydym yn datgan y strwythur reactor, ac yn reactor.c rydym yn ei ddiffinio, gan atal y defnyddiwr rhag newid ei feysydd yn benodol. Dyma un o'r patrymau cuddio data, sy'n cyd-fynd yn gryno â semanteg C.

Swyddogaethau reactor_register, reactor_deregister и reactor_reregister diweddaru'r rhestr o socedi o ddiddordeb a thrinwyr digwyddiadau cyfatebol yn y dewisydd system a thabl stwnsh.

Dangos swyddogaethau cofrestru

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

Ar ôl i'r adweithydd I/O ryng-gipio'r digwyddiad gyda'r disgrifydd fd, mae'n galw'r triniwr digwyddiad cyfatebol, y mae'n mynd iddo fd, mwgwd did digwyddiadau a gynhyrchir a phwyntydd defnyddiwr i void.

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

I grynhoi, bydd y gadwyn o alwadau swyddogaeth yn y cod defnyddiwr ar y ffurf ganlynol:

Adweithydd I/O noeth llawn sylw

Gweinydd edau sengl

Er mwyn profi'r adweithydd I / O dan lwyth uchel, byddwn yn ysgrifennu gweinydd gwe HTTP syml sy'n ymateb i unrhyw gais gyda delwedd.

Cyfeiriad cyflym at brotocol HTTP

HTTP - dyma'r protocol lefel cais, a ddefnyddir yn bennaf ar gyfer rhyngweithio gweinydd-porwr.

Gellir defnyddio HTTP yn hawdd drosodd trafnidiaeth protocol TCP, anfon a derbyn negeseuon mewn fformat penodol manyleb.

Fformat Cais

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

  • CRLF yn ddilyniant o ddau gymeriad: r и n, gan wahanu llinell gyntaf y cais, penawdau a data.
  • <КОМАНДА> - un o CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Bydd y porwr yn anfon gorchymyn i'n gweinydd GET, sy'n golygu "Anfon cynnwys y ffeil ataf."
  • <URI> - dynodwr adnoddau unffurf. Er enghraifft, os URI = /index.html, yna mae'r cleient yn gofyn am brif dudalen y wefan.
  • <ВЕРСИЯ HTTP> — fersiwn o'r protocol HTTP yn y fformat HTTP/X.Y. Y fersiwn a ddefnyddir amlaf heddiw yw HTTP/1.1.
  • <ЗАГОЛОВОК N> yn bâr gwerth allweddol yn y fformat <КЛЮЧ>: <ЗНАЧЕНИЕ>, wedi'i anfon at y gweinydd i'w ddadansoddi ymhellach.
  • <ДАННЫЕ> - data sydd ei angen ar y gweinydd i gyflawni'r llawdriniaeth. Yn aml mae'n syml JSON neu unrhyw fformat arall.

Fformat Ymateb

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

  • <КОД СТАТУСА> yw rhif sy'n cynrychioli canlyniad y llawdriniaeth. Bydd ein gweinydd bob amser yn dychwelyd statws 200 (gweithrediad llwyddiannus).
  • <ОПИСАНИЕ СТАТУСА> — cynrychioliad llinynnol o'r cod statws. Ar gyfer cod statws 200 mae hyn OK.
  • <ЗАГОЛОВОК N> — pennawd o'r un fformat ag yn y cais. Byddwn yn dychwelyd y teitlau Content-Length (maint ffeil) a Content-Type: text/html (math o ddata dychwelyd).
  • <ДАННЫЕ> — data y gofynnodd y defnyddiwr amdano. Yn ein hachos ni, dyma'r llwybr i'r ddelwedd i mewn HTML.

file http_server.c (gweinydd edefyn sengl) yn cynnwys ffeil common.h, sy'n cynnwys y prototeipiau swyddogaeth canlynol:

Dangos prototeipiau ffwythiant yn gyffredin.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);

Disgrifir y macro swyddogaethol hefyd SAFE_CALL() ac mae'r swyddogaeth wedi'i diffinio fail(). Mae'r macro yn cymharu gwerth y mynegiant gyda'r gwall, ac os yw'r cyflwr yn wir, mae'n galw'r ffwythiant fail():

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

Swyddogaeth fail() yn argraffu'r dadleuon a basiwyd i'r derfynell (fel printf()) ac yn terfynu'r rhaglen gyda'r cod 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);
}

Swyddogaeth new_server() yn dychwelyd disgrifydd ffeil y soced "gweinydd" a grëwyd gan alwadau system socket(), bind() и listen() ac yn gallu derbyn cysylltiadau sy'n dod i mewn mewn modd nad yw'n rhwystro.

Dangos swyddogaeth new_server().

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

  • Sylwch fod y soced yn cael ei greu i ddechrau yn y modd di-flocio gan ddefnyddio'r faner SOCK_NONBLOCKfel bod yn y swyddogaeth on_accept() (darllen mwy) galwad system accept() ni ataliodd y gweithrediad edefyn.
  • Os reuse_port yn hafal true, yna bydd y swyddogaeth hon yn ffurfweddu'r soced gyda'r opsiwn SO_REUSEPORT trwodd setsockopt()i ddefnyddio'r un porthladd mewn amgylchedd aml-threaded (gweler yr adran “Gweinydd aml-edau”).

Triniwr Digwyddiad on_accept() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLIN, yn yr achos hwn yn golygu y gellir derbyn y cysylltiad newydd. on_accept() yn derbyn cysylltiad newydd, yn ei newid i fodd di-flocio ac yn cofrestru gyda thriniwr digwyddiad on_recv() mewn adweithydd I/O.

Dangos swyddogaeth on_accept().

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

Triniwr Digwyddiad on_recv() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLIN, yn yr achos hwn yn golygu bod y cysylltiad cofrestredig on_accept(), yn barod i dderbyn data.

on_recv() yn darllen data o'r cysylltiad nes bod y cais HTTP wedi'i dderbyn yn llwyr, yna mae'n cofrestru triniwr on_send() i anfon ymateb HTTP. Os bydd y cleient yn torri'r cysylltiad, caiff y soced ei ddadgofrestru a'i gau gan ddefnyddio close().

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

Triniwr Digwyddiad on_send() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLOUT, sy'n golygu bod y cysylltiad wedi'i gofrestru on_recv(), yn barod i anfon data. Mae'r swyddogaeth hon yn anfon ymateb HTTP sy'n cynnwys HTML gyda delwedd i'r cleient ac yna'n newid y triniwr digwyddiad yn ôl i on_recv().

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

Ac yn olaf, yn y ffeil http_server.c, mewn swyddogaeth main() rydym yn creu adweithydd I/O gan ddefnyddio reactor_new(), creu soced gweinydd a'i gofrestru, dechreuwch yr adweithydd gan ddefnyddio reactor_run() am funud yn union, ac yna rydym yn rhyddhau adnoddau ac yn gadael y rhaglen.

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

Gadewch i ni wirio bod popeth yn gweithio yn ôl y disgwyl. Wrthi'n llunio (chmod a+x compile.sh && ./compile.sh yng ngwraidd y prosiect) a lansio'r gweinydd hunan-ysgrifenedig, agor http://127.0.0.1:18470 yn y porwr a gweld beth oeddem yn ei ddisgwyl:

Adweithydd I/O noeth llawn sylw

Mesur perfformiad

Dangoswch fy manylebau car

$ 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

Gadewch i ni fesur perfformiad gweinydd un edau. Gadewch i ni agor dwy derfynell: mewn un byddwn yn rhedeg ./http_server, mewn gwahanol - wrk. Ar ôl munud, bydd yr ystadegau canlynol yn cael eu harddangos yn yr ail derfynell:

$ 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

Roedd ein gweinydd un edau yn gallu prosesu dros 11 miliwn o geisiadau y funud yn deillio o 100 o gysylltiadau. Ddim yn ganlyniad gwael, ond a ellir ei wella?

Gweinydd aml-threaded

Fel y soniwyd uchod, gellir creu'r adweithydd I / O mewn edafedd ar wahân, a thrwy hynny ddefnyddio'r holl greiddiau CPU. Gadewch i ni roi'r dull hwn ar waith:

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

Nawr pob llinyn yn berchen ar ei eiddo ei hun adweithydd:

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

Sylwch fod y ddadl swyddogaeth new_server() eiriolwyr true. Mae hyn yn golygu ein bod yn aseinio'r opsiwn i soced y gweinydd SO_REUSEPORTi'w ddefnyddio mewn amgylchedd aml-edau. Gallwch ddarllen mwy o fanylion yma.

Ail rediad

Nawr gadewch i ni fesur perfformiad gweinydd aml-edau:

$ 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

Cynyddodd nifer y ceisiadau a broseswyd mewn 1 munud gan ~3.28 gwaith! Ond dim ond ~XNUMX filiwn oedden ni'n brin o'r rhif crwn, felly gadewch i ni geisio trwsio hynny.

Yn gyntaf, gadewch i ni edrych ar yr ystadegau a gynhyrchwyd perff:

$ 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

Defnyddio CPU Affinity, crynhoad gyda -march=native, PBL, cynnydd yn nifer yr ymweliadau celc, cynyddu MAX_EVENTS a defnydd EPOLLET ni roddodd gynnydd sylweddol mewn perfformiad. Ond beth sy'n digwydd os ydych chi'n cynyddu nifer y cysylltiadau cydamserol?

Ystadegau ar gyfer 352 o gysylltiadau cydamserol:

$ 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

Cafwyd y canlyniad dymunol, a chydag ef graff diddorol yn dangos dibyniaeth nifer y ceisiadau wedi'u prosesu mewn 1 munud ar nifer y cysylltiadau:

Adweithydd I/O noeth llawn sylw

Gwelwn, ar ôl cwpl o gannoedd o gysylltiadau, fod nifer y ceisiadau wedi'u prosesu ar gyfer y ddau weinydd yn gostwng yn sydyn (yn y fersiwn aml-edau mae hyn yn fwy amlwg). A yw hyn yn gysylltiedig â gweithredu stack Linux TCP/IP? Mae croeso i chi ysgrifennu eich rhagdybiaethau am yr ymddygiad hwn o'r graff ac optimeiddiadau ar gyfer opsiynau aml-edau ac un edau yn y sylwadau.

Fel nodwyd yn y sylwadau, nid yw'r prawf perfformiad hwn yn dangos ymddygiad yr adweithydd I/O o dan lwythi real, oherwydd bron bob amser mae'r gweinydd yn rhyngweithio â'r gronfa ddata, logiau allbynnau, yn defnyddio cryptograffeg gyda TLS ac ati, ac o ganlyniad mae'r llwyth yn dod yn anwisg (deinamig). Bydd profion ynghyd â chydrannau trydydd parti yn cael eu cynnal yn yr erthygl am yr proactor I/O.

Anfanteision adweithydd I/O

Mae angen i chi ddeall nad yw'r adweithydd I/O heb ei anfanteision, sef:

  • Mae defnyddio adweithydd I/O mewn amgylchedd aml-edau braidd yn anoddach, oherwydd bydd yn rhaid i chi reoli'r llifau â llaw.
  • Mae ymarfer yn dangos nad yw'r llwyth yn unffurf yn y rhan fwyaf o achosion, a all arwain at logio un edefyn tra bod un arall yn brysur gyda gwaith.
  • Os bydd un triniwr digwyddiad yn blocio edefyn, bydd dewisydd y system ei hun hefyd yn blocio, a all arwain at fygiau anodd eu darganfod.

Yn datrys y problemau hyn Rhagweithredwr I/O, sydd yn aml â rhaglennydd sy'n dosbarthu'r llwyth yn gyfartal i gronfa o edafedd, ac mae ganddo hefyd API mwy cyfleus. Byddwn yn siarad amdano yn nes ymlaen, yn fy erthygl arall.

Casgliad

Dyma lle mae ein taith o theori yn syth i'r gwacáu proffiliwr wedi dod i ben.

Ni ddylech aros ar hyn, oherwydd mae yna lawer o ddulliau eraill yr un mor ddiddorol o ysgrifennu meddalwedd rhwydwaith gyda gwahanol lefelau o gyfleustra a chyflymder. Yn ddiddorol, yn fy marn i, rhoddir dolenni isod.

Welwn ni chi eto!

Prosiectau diddorol

Beth arall i'w ddarllen?

Ffynhonnell: hab.com

Ychwanegu sylw