Бүрэн боломжит нүцгэн C I/O реактор

Бүрэн боломжит нүцгэн C I/O реактор

Танилцуулга

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

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

Өмнөх үг

  • Уг нийтлэлийг оролт/гаралтын реакторын ажиллагааг ойлгоход туслах зорилгоор бичсэн бөгөөд иймээс түүнийг ашиглах үед үүсэх эрсдлийг ойлгоход тусална.
  • Өгүүллийг ойлгохын тулд үндсэн мэдлэг шаардлагатай. C хэл болон сүлжээний програм хөгжүүлэлтийн зарим туршлага.
  • Бүх кодыг C хэл дээр хатуу бичдэг (Анхааруулга: урт PDF) C11 стандартын дагуу Linux-д зориулагдсан бөгөөд дээр ашиглах боломжтой GitHub.

Энэ яагаад хэрэгтэй вэ?

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

Эхний арга нь ирж буй холболт бүрт шинэ үйлдлийн систем үүсгэх явдал байв. Үүний сул тал бол өргөтгөх чадвар муу юм: үйлдлийн систем нь олон зүйлийг хэрэгжүүлэх шаардлагатай болно контекст шилжилтүүд и системийн дуудлага. Эдгээр нь үнэтэй үйлдлүүд бөгөөд гайхалтай олон тооны холболттой үнэгүй RAM-ийн дутагдалд хүргэж болзошгүй юм.

Өөрчлөгдсөн хувилбарыг онцлон тэмдэглэв утаснуудын тогтмол тоо (thread pool), ингэснээр системийг эвдрэхээс сэргийлж, харин нэгэн зэрэг шинэ асуудал үүсгэж байна: хэрэв урсгалын сан одоогоор удаан унших үйлдлээр хаагдсан бол өгөгдөл хүлээн авах боломжтой бусад залгуурууд үүнийг хийх боломжгүй болно. тийм.

Хоёр дахь аргыг ашигладаг үйл явдлын мэдэгдлийн систем (систем сонгогч) үйлдлийн системээр хангагдсан. Энэ нийтлэлд оролт гаралтын үйлдлүүдийн бэлэн байдлын тухай сэрэмжлүүлэг (үйл явдал, мэдэгдэл) дээр тулгуурласан систем сонгогчийн хамгийн түгээмэл төрлийг авч үзэх болно. тэдгээрийг дуусгах тухай мэдэгдэл. Үүнийг ашиглах хялбаршуулсан жишээг дараах блок диаграммаар дүрсэлж болно.

Бүрэн боломжит нүцгэн C I/O реактор

Эдгээр аргуудын ялгаа нь дараах байдалтай байна.

  • I/O үйлдлийг блоклох түр зогсоох хэрэглэгчийн урсгал хүртэлOS зөв ажиллах хүртэл дефрагментууд ирж байна IP пакетууд байт урсгал руу (TCP, өгөгдөл хүлээн авах) эсвэл дараа нь дамжуулан илгээхэд дотоод бичих буферт хангалттай зай байхгүй болно. юу ч (өгөгдөл илгээх).
  • Систем сонгогч цаг хугацаа өнгөрөх тусам OS гэж програмд ​​мэдэгдэнэ аль хэдийн defragmented IP пакетууд (TCP, өгөгдөл хүлээн авах) эсвэл дотоод бичих буферт хангалттай зай аль хэдийн боломжтой (өгөгдөл илгээх).

Дүгнэж хэлэхэд, оролт/гаралт бүрд үйлдлийн системийн утсыг нөөцлөх нь тооцоолох хүчийг дэмий үрсэн хэрэг юм, учир нь бодит байдал дээр утаснууд нь ашигтай ажил хийдэггүй (энэ нэр томъёо эндээс гаралтай). "програм хангамжийн тасалдал"). Систем сонгогч нь энэ асуудлыг шийдэж, хэрэглэгчийн програмд ​​CPU-ийн нөөцийг илүү хэмнэлттэй ашиглах боломжийг олгодог.

I/O реакторын загвар

Оролтын гаралтын реактор нь системийн сонгогч болон хэрэглэгчийн код хооронд давхаргын үүрэг гүйцэтгэдэг. Түүний үйл ажиллагааны зарчмыг дараах блок диаграмаар дүрсэлсэн болно.

Бүрэн боломжит нүцгэн C I/O реактор

  • Үйл явдал нь тодорхой сокет нь блоклохгүй оролт гаралтын үйлдлийг гүйцэтгэх боломжтой гэсэн мэдэгдэл гэдгийг танд сануулъя.
  • Үйл явдал зохицуулагч нь үйл явдал хүлээн авах үед оролт гаралтын реактор дууддаг функц бөгөөд дараа нь блоклохгүй оролт гаралтын үйлдлийг гүйцэтгэдэг.

Оролт/гаралтын реактор нь тодорхойлолтоороо нэг урсгалтай гэдгийг анхаарах нь чухал боловч олон урсгалтай орчинд 1 реактор: 1 реакторын харьцаагаар ашиглахад юу ч саад болохгүй бөгөөд ингэснээр бүх CPU цөмийг дахин боловсруулна.

Реализация

Бид нийтийн интерфейсийг файлд байрлуулах болно reactor.h, болон хэрэгжилт - in 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 болон Callback Data-г харуулах

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

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

Гүйцэтгэлийн хэмжилт

Миний машины техникийн үзүүлэлтүүдийг харуул

$ 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 минут тутамд XNUMX сая гаруй хүсэлтийг боловсруулах боломжтой болсон. Муу үр дүн биш, гэхдээ үүнийг сайжруулах боломжтой юу?

Олон урсгалтай сервер

Дээр дурдсанчлан оролт/гаралтын реакторыг тус тусад нь үүсгэж болох бөгөөд ингэснээр бүх 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 сая дутуу байсан тул үүнийг засахыг хичээцгээе.

Эхлээд үүсгэсэн статистикийг харцгаая 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, ТББ-ын, хандалтын тоо нэмэгдсэн кэш, нэмэгдүүлэх 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 I/O реактор

Хэдэн зуун холболт хийсний дараа хоёр серверийн боловсруулсан хүсэлтийн тоо огцом буурч байгааг бид харж байна (олон урсгалтай хувилбарт энэ нь илүү мэдэгдэхүйц юм). Энэ нь Linux TCP/IP стекийн хэрэгжилттэй холбоотой юу? Графикийн энэ үйлдлийн талаархи таамаглал, олон урсгалтай ба нэг урсгалтай сонголтуудын оновчлолын талаар санал бодлоо чөлөөтэй бичээрэй.

Хэрхэн тэмдэглэв Тайлбар дээр энэхүү гүйцэтгэлийн тест нь бодит ачаалалд байгаа оролт/гаралтын реакторын үйлдлийг харуулахгүй, учир нь сервер нь мэдээллийн сантай бараг үргэлж харилцаж, бүртгэл гаргаж, криптограф ашигладаг. TLS гэх мэт, үүний үр дүнд ачаалал жигд бус (динамик) болдог. Гуравдагч талын бүрэлдэхүүн хэсгүүдийн хамт туршилтыг I/O проакторын тухай нийтлэлд хийх болно.

I/O реакторын сул тал

Оролтын гаралтын реактор нь сул талгүй гэдгийг та ойлгох хэрэгтэй, тухайлбал:

  • Оролт гаралтын реакторыг олон урсгалтай орчинд ашиглах нь арай илүү хэцүү, учир нь Та урсгалыг гараар удирдах хэрэгтэй болно.
  • Практикаас харахад ихэнх тохиолдолд ачаалал жигд бус байдаг бөгөөд энэ нь нэг утас таслах, нөгөө нь ажилдаа завгүй байхад хүргэдэг.
  • Хэрэв нэг үйл явдал зохицуулагч хэлхээг блокловол систем сонгогч өөрөө блоклох бөгөөд энэ нь олоход хэцүү алдаануудыг үүсгэж болзошгүй.

Эдгээр асуудлыг шийддэг I/O проактор, энэ нь ихэвчлэн урсгалын санд ачааллыг жигд хуваарилдаг хуваарьтай, мөн илүү тохиромжтой API-тай байдаг. Энэ тухай бид дараа нь, миний нөгөө нийтлэлд ярих болно.

дүгнэлт

Бидний онолоос шууд профайлын яндан руу хийсэн аялал маань энд дууслаа.

Та үүнд анхаарлаа хандуулж болохгүй, учир нь өөр өөр түвшний тав тухтай байдал, хурдтай сүлжээний программ бичих сонирхолтой аргууд байдаг. Сонирхолтой нь миний бодлоор холбоосыг доор өгөв.

Дараагийн удаа хүртэл!

Сонирхолтой төслүүд

Би өөр юу унших ёстой вэ?

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх