Жылаңач C боюнча толук иштеген киргизүү/чыгаруучу реактор

Жылаңач C боюнча толук иштеген киргизүү/чыгаруучу реактор

тааныштыруу

I/O реактору (бир жип окуя цикли) көптөгөн популярдуу чечимдерде колдонулган жогорку жүктөмдүү программаны жазуу үлгүсү:

Бул макалада биз I/O реакторунун инструменттерин жана анын кантип иштешин карап чыгабыз, ишке ашырууну 200 саптан аз код менен жазабыз жана 40 миллиондон ашык суроо-талап/мүнөттө жөнөкөй HTTP сервер процессин жасайбыз.

сөздөр

  • Макала I/O реакторунун иштешин түшүнүүгө жардам берүү үчүн жазылган, ошондуктан аны колдонууда тобокелдиктерди түшүнүү.
  • Макаланы түшүнүү үчүн негиздерди билүү талап кылынат. C тили жана тармактык тиркемелерди иштеп чыгуу боюнча кээ бир тажрыйба.
  • Бардык код C тилинде так жазылган (сак: узун PDF) C11 стандартына Linux үчүн жана жеткиликтүү GitHub.

Ал эмне үчүн керек?

Интернеттин популярдуулугунун өсүшү менен веб-серверлер бир эле учурда көп сандагы туташуулар менен иштөөнү талап кыла баштады, ошондуктан эки ыкма аракет кылынды: көп сандагы OS жиптериндеги киргизүү/чыгарууларды бөгөттөө жана блокировка кылбоочу I/O менен айкалышта. окуя кабарлоо системасы, ошондой эле "система селектору" деп аталат (epoll/kqueue/IOCP/жана башкалар).

Биринчи ыкма ар бир кирүүчү байланыш үчүн жаңы OS жипти түзүүнү камтыйт. Анын кемчилиги начар масштабдалуу болуп саналат: операциялык система көптөгөн ишке ашырууга туура келет контексттик өтүүлөр и системалык чалуулар. Алар кымбат операциялар жана байланыштардын таасирдүү саны менен бош RAM жетишсиздигине алып келиши мүмкүн.

өзгөртүлгөн версия баса белгилейт жиптердин белгиленген саны (жип пулу), ошону менен системанын аткарууну токтотуусуна жол бербейт, бирок ошол эле учурда жаңы көйгөйдү киргизет: эгерде жип пулу учурда узак окуу операциялары менен бөгөттөлсө, анда буга чейин маалыматтарды ала алган башка розеткалар ушундай кыл.

Экинчи ыкманы колдонот окуя кабарлоо системасы (системалык селектор) ОС тарабынан берилген. Бул макалада киргизүү/чыгаруу операцияларына эмес, эскертүүлөргө (окуялар, эскертмелер) негизделген система селекторунун эң кеңири таралган түрү талкууланат. алардын аяктагандыгы жөнүндө билдирүүлөр. Аны колдонуунун жөнөкөйлөштүрүлгөн мисалы төмөнкү блок-схема менен көрсөтүлүшү мүмкүн:

Жылаңач C боюнча толук иштеген киргизүү/чыгаруучу реактор

Бул ыкмалардын ортосундагы айырма төмөнкүдөй:

  • I/O операцияларын бөгөттөө токтотуу колдонуучу агымы ушуга чейин, ушул убакка чейинOS туура болгонго чейин дефрагментациялоо кирүүчү IP пакеттери байт агымына (TCP, маалыматтарды кабыл алуу) же ички жазуу буферлеринде кийинки аркылуу жөнөтүү үчүн орун жетишсиз болот. NIC (маалыматтарды жөнөтүү).
  • Системалык селектор убакыттын өтүшү менен OS деп программага кабарлайт буга чейин дефрагментацияланган IP пакеттери (TCP, маалыматтарды кабыл алуу) же ички жазуу буферлеринде жетиштүү орун буга чейин жеткиликтүү (маалыматтарды жөнөтүү).

Жыйынтыктап айтканда, ар бир киргизүү/чыгаруу үчүн ОС жибин резервдештирүү эсептөө күчүн текке кетирүү болуп саналат, анткени чындыгында жиптер пайдалуу иштерди жасабайт (ошондуктан бул термин "программалык үзгүлтүк"). Системалык селектор бул көйгөйдү чечип, колдонуучу программасына CPU ресурстарын алда канча үнөмдүү пайдаланууга мүмкүндүк берет.

I/O реакторунун модели

Киргизүү/чыгаруу реактору система селектору менен колдонуучунун коду ортосундагы катмардын ролун аткарат. Анын иштөө принциби төмөнкү блок-схема менен сүрөттөлөт:

Жылаңач C боюнча толук иштеген киргизүү/чыгаруучу реактор

  • Эске сала кетейин, окуя - бул белгилүү бир розетка бөгөттөлбөгөн киргизүү/чыгаруу операциясын аткара ала тургандыгы жөнүндө билдирүү.
  • Окуяны иштетүүчү - бул окуя кабыл алынганда киргизүү/чыгаруу реактору тарабынан чакырылган функция, андан кийин блокировка кылбаган киргизүү/чыгарма операциясын аткарат.

Белгилей кетчү нерсе, киргизүү/чыгаруу реактору аныктамасы боюнча бир жиптүү, бирок концепцияны көп жиптүү чөйрөдө 1 жип: 1 реактор катышында колдонууга эч нерсе тоскоол болбойт, ошону менен бардык CPU өзөктөрү кайра иштетилет.

Реализация

Биз жалпы интерфейсти файлга жайгаштырабыз 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);

Киргизүүчү/чыгаруучу реактордун түзүмү төмөнкүлөрдөн турат файлдын дескриптору селектор epoll и хэш таблицалар 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 биз аны аныктайбыз, ошону менен колдонуучу анын талааларын ачык өзгөртүүсүнө жол бербейбиз. Бул үлгүлөрдүн бири болуп саналат маалыматтарды жашыруу, ал кыскача С семантикасына туура келет.

милдеттери 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;
}

I/O реактору окуяны дескриптор менен кармап алгандан кийин 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 боюнча толук иштеген киргизүү/чыгаруучу реактор

Жалгыз жиптүү сервер

I/O реакторун жогорку жүктөмдө сынап көрүү үчүн биз каалаган суроого сүрөт менен жооп берген жөнөкөй 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() OS окуяны жараткандан кийин чакырылат EPOLLIN, бул учурда жаңы байланышты кабыл алууга болот дегенди билдирет. on_accept() жаңы туташууну кабыл алып, аны бөгөттөгөн режимге которот жана окуяны иштетүүчү менен каттайт on_recv() I/O реакторунда.

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() OS окуяны жараткандан кийин чакырылат 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() OS окуяны жараткандан кийин чакырылат 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() колдонуп I/O реакторун түзөбүз 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. Бир мүнөттөн кийин, экинчи терминалда төмөнкү статистика көрсөтүлөт:

$ 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 миллиондон ашык суроону иштете алды. Натыйжа жаман эмес, бирок аны жакшыртууга болобу?

Көп агымдуу сервер

Жогоруда айтылгандай, I/O реактору өзүнчө жиптерде түзүлүшү мүмкүн, муну менен бардык CPU өзөктөрү колдонулат. Келгиле, бул ыкманы иш жүзүндө колдонолу:

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 миллион гана жетишпей калдык, андыктан аны оңдоого аракет кылалы.

Алгач түзүлгөн статистиканы карап көрөлү перф:

$ 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, Башкы прокуратуранын, хиттердин санынын өсүшү кэш, жогорулатуу 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 стекинин ишке ашырылышына байланыштуубу? Графиктин бул жүрүм-туруму жана көп жиптүү жана бир жиптүү варианттар үчүн оптималдаштыруу боюнча өз божомолдоруңузду комментарийлерде жазыңыз.

кантип белгиленген комментарийлерде, бул өндүрүмдүүлүк тести реалдуу жүктөмдө I/O реакторунун жүрүм-турумун көрсөтпөйт, анткени дээрлик ар дайым сервер маалымат базасы менен иштешет, журналдарды чыгарат, криптографияны колдонот. TLS ж.б., мунун натыйжасында жүк бирдей эмес (динамикалык) болуп калат. Үчүнчү тараптын компоненттери менен бирге сыноолор I/O проактору жөнүндө макалада жүргүзүлөт.

I/O реакторунун кемчиликтери

Сиз I/O реакторунун кемчиликтери жок эмес экенин түшүнүү керек, атап айтканда:

  • Көп жиптүү чөйрөдө I/O реакторун колдонуу бир аз кыйыныраак, анткени агымдарды кол менен башкарууга туура келет.
  • Практика көрсөткөндөй, көпчүлүк учурларда жүк бирдей эмес, бул бир жиптин кесилишине алып келиши мүмкүн, ал эми экинчиси жумуш менен алек.
  • Эгерде бир окуя иштеткич жипти бөгөттөсө, система селекторунун өзү да бөгөттөп, табуу кыйын мүчүлүштүктөргө алып келиши мүмкүн.

Бул көйгөйлөрдү чечет I/O проактору, ал көбүнчө жиптердин көлмөсүнө жүктү бирдей бөлүштүрүүчү пландаштыргычка ээ, ошондой эле ыңгайлуураак API'ге ээ. Бул тууралуу кийинчерээк, башка макаламда сүйлөшөбүз.

жыйынтыктоо

Бул жерде биздин теориядан түз профайлдын түтүктөрүнө болгон саякатыбыз аяктады.

Сиз бул жөнүндө көп ойлонбоңуз, анткени ар кандай деңгээлдеги ыңгайлуулук жана ылдамдык менен тармактык программалык камсыздоону жазуу үчүн бирдей кызыктуу ыкмалар бар. Кызыктуу, менин оюмча, шилтемелер төмөндө келтирилген.

Көрүшкөнгө чейин!

Кызыктуу долбоорлор

Дагы эмнени окушум керек?

Source: www.habr.com

Комментарий кошуу