Толық функционалды жалаң C енгізу/шығару реакторы

Толық функционалды жалаң C енгізу/шығару реакторы

Кіріспе

енгізу/шығару реакторы (бір жіпті оқиға циклі) - бұл көптеген танымал шешімдерде қолданылатын жоғары жүктемелі бағдарламалық құралды жазу үлгісі:

Бұл мақалада біз енгізу-шығару реакторының ішкі және сыртқы мүмкіндіктерін және оның қалай жұмыс істейтінін қарастырамыз, 200-ден аз код жолында іске асыруды жазамыз және 40 миллионнан астам сұраныс/минуттан астам қарапайым HTTP сервер процесін жасаймыз.

Алғы сөз

  • Мақала енгізу/шығару реакторының жұмысын түсінуге көмектесу, сондықтан оны пайдалану кезіндегі қауіптерді түсіну үшін жазылған.
  • Мақаланы түсіну үшін негіздерді білу қажет. C тілі және желілік қосымшаларды әзірлеудегі кейбір тәжірибе.
  • Барлық код қатаң түрде Си тілінде жазылған (Абайлаңыз: ұзақ PDF) C11 стандартына Linux үшін және қол жетімді GitHub.

Неліктен бұл қажет?

Интернеттің танымалдылығының артуына байланысты веб-серверлер бір уақытта көптеген қосылымдарды өңдеуді қажет ете бастады, сондықтан екі тәсіл қолданылды: ОЖ ағындарының үлкен санында енгізу/шығаруды блоктау және блокталмаған енгізу/шығару. «жүйе селекторы» деп те аталатын оқиға туралы хабарландыру жүйесі (эполл/кезек/IOCP/т.б.).

Бірінші тәсіл әрбір кіріс қосылым үшін жаңа ОЖ ағынын жасауды қамтыды. Оның кемшілігі нашар масштабталады: операциялық жүйеге көптеген енгізуге тура келеді контекстік ауысулар и жүйелік қоңыраулар. Олар қымбат операциялар және қосылымдардың әсерлі саны бар бос ЖЖҚ жетіспеушілігіне әкелуі мүмкін.

Өзгертілген нұсқа бөлектеледі жіптердің бекітілген саны (ағынды пул), осылайша жүйенің орындауды тоқтатуына жол бермейді, бірақ сонымен бірге жаңа мәселені енгізеді: егер ағын пулы қазіргі уақытта ұзақ оқу әрекеттерімен блокталған болса, онда деректерді қабылдай алатын басқа ұяшықтар мүмкін емес. солай істе.

Екінші тәсіл қолданылады оқиғаларды хабарлау жүйесі (жүйе селекторы) ОЖ қамтамасыз етеді. Бұл мақалада енгізу/шығару операцияларына дайындығы туралы ескертулерге (оқиғалар, хабарландырулар) негізделген жүйе селекторының ең көп тараған түрі талқыланады. олардың аяқталуы туралы хабарламалар. Оны пайдаланудың жеңілдетілген мысалын келесі блок-схемамен көрсетуге болады:

Толық функционалды жалаң C енгізу/шығару реакторы

Бұл тәсілдердің айырмашылығы келесідей:

  • Енгізу/шығару операцияларын блоктау тоқтата тұру пайдаланушы ағыны оған дейінОЖ дұрыс болғанша дефрагментациялау кіріс IP пакеттері байт ағынына (TCP, деректерді қабылдау) немесе келесі арқылы жіберу үшін ішкі жазу буферінде бос орын жеткіліксіз болады. NIC (деректерді жіберу).
  • Жүйе селекторы біраз уақыттан соң ОЖ бар екенін бағдарламаға хабарлайды қазірдің өзінде дефрагментацияланған IP пакеттері (TCP, деректерді қабылдау) немесе ішкі жазу буферінде жеткілікті орын қазірдің өзінде қолжетімді (деректерді жіберу).

Қорытындылай келе, әрбір енгізу/шығару үшін ОЖ ағынын резервтеу есептеу қуатын ысырап етеді, өйткені шын мәнінде ағындар пайдалы жұмыс істемейді (демек термин «бағдарламалық қамтамасыз етудің үзілуі»). Жүйе селекторы бұл мәселені шешеді, бұл пайдаланушы бағдарламасына CPU ресурстарын әлдеқайда үнемді пайдалануға мүмкіндік береді.

Енгізу/шығару реакторының моделі

Енгізу/шығару реакторы жүйе селекторы мен пайдаланушы коды арасындағы қабат ретінде әрекет етеді. Оның жұмыс істеу принципі келесі блок-схемамен сипатталған:

Толық функционалды жалаң C енгізу/шығару реакторы

  • Еске сала кетейін, оқиға белгілі бір розетка блокталмаған енгізу-шығару операциясын орындауға қабілетті екендігі туралы хабарлама болып табылады.
  • Оқиға өңдегіші – оқиға қабылданған кезде енгізу/шығару реакторы шақыратын функция, содан кейін блокталмаған енгізу/шығару әрекетін орындайды.

Енгізу/шығару реакторы анықтамасы бойынша бір ағынды екенін ескеру маңызды, бірақ концепцияны көп ағынды ортада 1 ағын: 1 реактор арақатынасында қолдануға, осылайша барлық процессор өзектерін қайта өңдеуге ештеңе кедергі келтірмейді.

Реализация

Біз жалпы интерфейсті файлға орналастырамыз reactor.h, және жүзеге асыру - жылы reactor.c. reactor.h мынадай хабарландырулардан тұрады:

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

Енгізу/шығару реакторының құрылымы мыналардан тұрады файл дескрипторы селектор эполл и хэш кестелері GHashTable, ол әрбір розеткаға сәйкес келеді CallbackData (оқиға өңдеушісінің құрылымы және оған арналған пайдаланушы аргументі).

Reactor және CallbackData көрсету

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

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

өңдеу мүмкіндігін қосқанымызды ескеріңіз толық емес түрі көрсеткішке сәйкес. IN reactor.h құрылымын жариялаймыз reactor, ал reactor.c біз оны анықтаймыз, осылайша пайдаланушыға оның өрістерін анық өзгертуге жол бермейді. Бұл үлгілердің бірі деректерді жасыру, ол C семантикасына қысқаша сәйкес келеді.

Функциялар reactor_register, reactor_deregister и reactor_reregister қызығушылық ұяларының тізімін және жүйе селекторындағы және хэш кестесіндегі сәйкес оқиға өңдеушілерін жаңартыңыз.

Тіркеу функцияларын көрсету

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

Енгізу/шығару реакторы оқиғаны дескриптормен тоқтатқаннан кейін fd, ол өтетін сәйкес оқиға өңдеушісін шақырады fd, бит маскасы жасалған оқиғалар және пайдаланушы көрсеткіші void.

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

Қорытындылай келе, пайдаланушы кодындағы функцияларды шақыру тізбегі келесі пішінді алады:

Толық функционалды жалаң C енгізу/шығару реакторы

Бір ағынды сервер

Енгізу/шығару реакторын жоғары жүктемеде сынау үшін біз кез келген сұрауға суретпен жауап беретін қарапайым HTTP веб-серверін жазамыз.

HTTP протоколына жылдам сілтеме

HTTP - бұл хаттама қолдану деңгейі, негізінен сервер мен шолғыш әрекеттесуі үшін пайдаланылады.

HTTP арқылы оңай пайдалануға болады тасымалдау хаттама TCP, көрсетілген пішімде хабарларды жіберу және алу спецификация.

Сұраныс пішімі

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

  • CRLF екі таңба тізбегі болып табылады: r и n, сұраудың бірінші жолын, тақырыптарды және деректерді бөлу.
  • <КОМАНДА> - бірі CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Браузер біздің серверге пәрмен жібереді GET, «Маған файлдың мазмұнын жіберу» дегенді білдіреді.
  • <URI> - біркелкі ресурс идентификаторы. Мысалы, егер URI = /index.html, содан кейін клиент сайттың негізгі бетін сұрайды.
  • <ВЕРСИЯ HTTP> — форматтағы HTTP протоколының нұсқасы HTTP/X.Y. Бүгінгі таңда ең жиі қолданылатын нұсқасы HTTP/1.1.
  • <ЗАГОЛОВОК N> пішімдегі кілт-мән жұбы болып табылады <КЛЮЧ>: <ЗНАЧЕНИЕ>, одан әрі талдау үшін серверге жіберілді.
  • <ДАННЫЕ> — операцияны орындау үшін серверге қажет деректер. Көбінесе бұл қарапайым JSON немесе кез келген басқа пішім.

Жауап пішімі

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

  • <КОД СТАТУСА> операцияның нәтижесін көрсететін сан болып табылады. Біздің сервер әрқашан 200 күйін қайтарады (сәтті жұмыс).
  • <ОПИСАНИЕ СТАТУСА> — күй кодының жолдық көрінісі. 200 күй коды үшін бұл OK.
  • <ЗАГОЛОВОК N> — сұраныстағыдай пішімдегі тақырып. Біз атауларды қайтарамыз Content-Length (файл өлшемі) және Content-Type: text/html (деректер түрін қайтару).
  • <ДАННЫЕ> — пайдаланушы сұраған деректер. Біздің жағдайда бұл кескінге апаратын жол HTML.

файл http_server.c (бір ағынды сервер) файлды қамтиды common.h, ол келесі функция прототиптерін қамтиды:

Жалпы функция прототиптерін көрсету.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);

Функционалдық макрос та сипатталған SAFE_CALL() және функциясы анықталады fail(). Макрос өрнектің мәнін қатемен салыстырады, ал егер шарт ақиқат болса, функцияны шақырады fail():

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

функция fail() терминалға жіберілген аргументтерді басып шығарады (мысалы printf()) және бағдарламаны кодпен аяқтайды 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);
}

функция new_server() жүйелік шақырулар арқылы жасалған «сервер» ұясының файл дескрипторын қайтарады socket(), bind() и listen() және кіріс қосылымдарды блокталмаған режимде қабылдауға қабілетті.

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

  • Розетка бастапқыда жалаушаның көмегімен блокталмаған режимде жасалғанын ескеріңіз SOCK_NONBLOCKсондықтан функцияда on_accept() (толығырақ оқу) жүйелік қоңырау accept() жіптің орындалуын тоқтатпады.
  • егер reuse_port тең true, содан кейін бұл функция розетканы опциямен конфигурациялайды SO_REUSEPORT арқылы setsockopt()көп ағынды ортада бір портты пайдалану үшін («Көп ағынды сервер» бөлімін қараңыз).

Оқиға өңдеушісі on_accept() ОЖ оқиғаны жасағаннан кейін шақырылады EPOLLIN, бұл жағдайда жаңа қосылымды қабылдауға болатынын білдіреді. on_accept() жаңа қосылымды қабылдайды, оны бұғаттамайтын режимге ауыстырады және оқиға өңдеушісімен тіркейді on_recv() енгізу/шығару реакторында.

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

Оқиға өңдеушісі on_recv() ОЖ оқиғаны жасағаннан кейін шақырылады EPOLLIN, бұл жағдайда қосылым тіркелгенін білдіреді on_accept(), деректерді қабылдауға дайын.

on_recv() HTTP сұрауы толығымен алынғанша қосылымдағы деректерді оқиды, содан кейін өңдеушіні тіркейді on_send() HTTP жауабын жіберу үшін. Егер клиент қосылымды бұзса, розетка тіркеуден шығарылады және оны пайдаланып жабылады close().

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

Оқиға өңдеушісі on_send() ОЖ оқиғаны жасағаннан кейін шақырылады EPOLLOUT, қосылым тіркелгенін білдіреді on_recv(), деректерді жіберуге дайын. Бұл функция клиентке кескіні бар HTML бар HTTP жауабын жібереді, содан кейін оқиға өңдегішті қайта өзгертеді on_recv().

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

Соңында, файлда http_server.c, функциясында main() көмегімен енгізу/шығару реакторын жасаймыз reactor_new(), сервер ұясын жасаңыз және оны тіркеңіз, реакторды пайдаланып іске қосыңыз reactor_run() тура бір минут, содан кейін біз ресурстарды босатып, бағдарламадан шығамыз.

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

Барлығы күткендей жұмыс істеп тұрғанын тексерейік. құрастыру (chmod a+x compile.sh && ./compile.sh жобаның түбірінде) және өздігінен жазылған серверді іске қосыңыз, ашыңыз http://127.0.0.1:18470 шолғышта және біз күткен нәрсені көріңіз:

Толық функционалды жалаң C енгізу/шығару реакторы

Өнімділікті өлшеу

Менің көлігімнің техникалық сипаттамаларын көрсету

$ 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

Бір ағынды сервердің өнімділігін өлшейік. Екі терминалды ашайық: біреуінде біз іске қосамыз ./http_server, басқаша - бұзылу. Бір минуттан кейін екінші терминалда келесі статистика көрсетіледі:

$ 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

Біздің бір ағынды сервер 11 қосылымнан туындаған минутына 100 миллионнан астам сұранысты өңдей алды. Жаман нәтиже емес, бірақ оны жақсартуға болады ма?

Көп ағынды сервер

Жоғарыда айтылғандай, енгізу/шығару реакторын жеке ағындарда жасауға болады, осылайша барлық процессор өзектерін пайдаланады. Бұл тәсілді іс жүзінде қолданайық:

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

Енді әрбір жіп өз иелігінде реактор:

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

Функция аргументі екенін ескеріңіз new_server() адвокаттар true. Бұл опцияны сервер ұясына тағайындайтынымызды білдіреді SO_REUSEPORTоны көп ағынды ортада пайдалану үшін. Толық мәліметтерді оқи аласыз осында.

Екінші жүгіру

Енді көп ағынды сервердің өнімділігін өлшейік:

$ 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

1 минут ішінде өңделген сұраулар саны ~3.28 есе өсті! Бірақ біз дөңгелек санға ~ XNUMX миллионға жетпей қалдық, сондықтан оны түзетуге тырысайық.

Алдымен жасалған статистикаға назар аударайық Perf:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         1 813 074      context-switches          #    0,007 M/sec                  
             4 689      cpu-migrations            #    0,019 K/sec                  
               254      page-faults               #    0,001 K/sec                  
   895 324 830 170      cycles                    #    3,693 GHz                    
   621 378 066 808      instructions              #    0,69  insn per cycle         
   119 926 709 370      branches                  #  494,653 M/sec                  
     3 227 095 669      branch-misses             #    2,69% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

CPU Affinity пайдалану, көмегімен құрастыру -march=native, PGO, хит санының артуы кэш, арттыру MAX_EVENTS және пайдалану EPOLLET өнімділіктің айтарлықтай артуына әкелмеді. Бірақ бір уақыттағы қосылымдардың санын көбейтсеңіз не болады?

Бір уақыттағы 352 қосылымның статистикасы:

$ 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

Қажетті нәтиже алынды және онымен 1 минут ішінде өңделген сұраулар санының қосылымдар санына тәуелділігін көрсететін қызықты график алынды:

Толық функционалды жалаң C енгізу/шығару реакторы

Біз бірнеше жүз қосылымнан кейін екі серверге де өңделген сұраныстардың саны күрт төмендейтінін көреміз (көп ағынды нұсқада бұл айтарлықтай байқалады). Бұл Linux TCP/IP стекін енгізуге қатысты ма? Түсініктемелерде графиктің осы әрекеті және көп ағынды және бір ағынды опцияларға арналған оңтайландырулар туралы өз болжамдарыңызды жазыңыз.

қалай атап өтті түсініктемелерде бұл өнімділік сынағы нақты жүктемелер кезінде енгізу-шығару реакторының әрекетін көрсетпейді, өйткені сервер әрқашан дерлік дерекқормен әрекеттеседі, журналдарды шығарады, криптографияны пайдаланады. TLS т.б., нәтижесінде жүктеме біркелкі емес (динамикалық) болады. Үшінші тарап компоненттерімен бірге сынақтар енгізу/шығару проакторы туралы мақалада жүргізіледі.

Енгізу/шығару реакторының кемшіліктері

Сіз енгізу-шығару реакторының кемшіліктері жоқ емес екенін түсінуіңіз керек, атап айтқанда:

  • Көп ағынды ортада енгізу/шығару реакторын пайдалану біршама қиынырақ, өйткені ағындарды қолмен басқаруға тура келеді.
  • Тәжірибе көрсеткендей, көп жағдайда жүктеме біркелкі емес, бұл бір жіпті тіркеуге әкелуі мүмкін, ал екіншісі жұмыспен айналысады.
  • Егер бір оқиға өңдегіші ағынды блоктаса, жүйе селекторының өзі де блоктайды, бұл табу қиын қателерге әкелуі мүмкін.

Осы мәселелерді шешеді I/O проакторы, ол көбінесе жүктемені ағындар пулына біркелкі тарататын жоспарлаушыға ие, сонымен қатар ыңғайлырақ API бар. Бұл туралы кейінірек, менің басқа мақаламда айтатын боламыз.

қорытынды

Міне, біздің теориядан тікелей профильді шығаруға дейінгі саяхатымыз аяқталды.

Сіз бұл туралы тоқталмауыңыз керек, өйткені ыңғайлылық пен жылдамдықтың әртүрлі деңгейлері бар желілік бағдарламалық жасақтаманы жазудың көптеген басқа да қызықты тәсілдері бар. Қызықты, менің ойымша, сілтемелер төменде берілген.

Келесі кездескенше!

Қызықты жобалар

Тағы не оқуым керек?

Ақпарат көзі: www.habr.com

пікір қалдыру