راکتور ورودی/خروجی بدون C با امکانات کامل

راکتور ورودی/خروجی بدون C با امکانات کامل

معرفی

راکتور ورودی/خروجی (تک نخ حلقه رویداد) الگویی برای نوشتن نرم افزار با بار بالا است که در بسیاری از راه حل های محبوب استفاده می شود:

در این مقاله، ما به جزئیات یک راکتور ورودی/خروجی و نحوه کار آن نگاه می‌کنیم، پیاده‌سازی را در کمتر از 200 خط کد می‌نویسیم و یک سرور HTTP ساده را بیش از 40 میلیون درخواست در دقیقه پردازش می‌کنیم.

پیش گفتار

  • این مقاله برای کمک به درک عملکرد راکتور ورودی/خروجی و در نتیجه درک خطرات هنگام استفاده از آن نوشته شده است.
  • دانستن اصول اولیه برای درک مقاله لازم است. زبان C و کمی تجربه در توسعه برنامه های کاربردی شبکه.
  • تمام کدها به زبان C نوشته شده است و دقیقاً مطابق با (احتیاط: PDF طولانی) به استاندارد C11 برای لینوکس و در دسترس است GitHub.

چرا آن را انجام؟

با محبوبیت روزافزون اینترنت، سرورهای وب شروع به رسیدگی به تعداد زیادی اتصال به طور همزمان کردند و بنابراین دو رویکرد امتحان شد: مسدود کردن I/O در تعداد زیادی از رشته‌های سیستم عامل و عدم مسدود کردن I/O در ترکیب با یک سیستم اعلان رویداد که به آن «انتخابگر سیستم» نیز گفته می شود (epoll/صف/IOCP/و غیره).

اولین رویکرد شامل ایجاد یک رشته سیستم عامل جدید برای هر اتصال ورودی بود. نقطه ضعف آن مقیاس پذیری ضعیف است: سیستم عامل باید بسیاری را پیاده سازی کند انتقال زمینه и تماس های سیستمی. آنها عملیات گران قیمت هستند و می توانند منجر به کمبود RAM رایگان با تعداد قابل توجه اتصالات شوند.

نسخه اصلاح شده برجسته است تعداد ثابت رشته ها (Thread Pool)، در نتیجه از توقف اجرای سیستم جلوگیری می کند، اما در عین حال مشکل جدیدی را ایجاد می کند: اگر یک Thread Pool در حال حاضر توسط عملیات خواندن طولانی مسدود شده باشد، دیگر سوکت هایی که قبلاً قادر به دریافت داده هستند، نمی توانند انجام دهید.

رویکرد دوم استفاده می کند سیستم اطلاع رسانی رویداد (انتخاب کننده سیستم) ارائه شده توسط سیستم عامل. این مقاله رایج ترین نوع انتخابگر سیستم را مورد بحث قرار می دهد، بر اساس هشدارها (رویدادها، اعلان ها) در مورد آمادگی برای عملیات I/O، نه بر اساس اطلاعیه در مورد تکمیل آنها. یک مثال ساده از استفاده از آن را می توان با بلوک دیاگرام زیر نشان داد:

راکتور ورودی/خروجی بدون C با امکانات کامل

تفاوت این رویکردها به شرح زیر است:

  • مسدود کردن عملیات I/O تعلیق کند جریان کاربر تا زمانتا زمانی که سیستم عامل به درستی انجام شود یکپارچه سازی ورودی بسته های IP به جریان بایت (TCP، دریافت داده ها) یا فضای کافی در بافرهای نوشتن داخلی برای ارسال بعدی از طریق وجود نخواهد داشت. NIC (ارسال داده ها).
  • انتخابگر سیستم در طول زمان به برنامه اطلاع می دهد که سیستم عامل قبلا بسته های IP یکپارچه شده (TCP، دریافت داده) یا فضای کافی در بافرهای نوشتن داخلی قبلا موجود (ارسال داده ها).

به طور خلاصه، رزرو یک رشته سیستم عامل برای هر ورودی/خروجی اتلاف قدرت محاسباتی است، زیرا در واقعیت، رشته ها کار مفیدی انجام نمی دهند (از این رو اصطلاح "وقفه نرم افزاری"). انتخابگر سیستم این مشکل را حل می کند و به برنامه کاربر اجازه می دهد تا از منابع CPU بسیار اقتصادی تر استفاده کند.

مدل راکتور ورودی/خروجی

راکتور I/O به عنوان یک لایه بین انتخابگر سیستم و کد کاربر عمل می کند. اصل عملکرد آن توسط بلوک دیاگرام زیر توضیح داده شده است:

راکتور ورودی/خروجی بدون C با امکانات کامل

  • اجازه دهید به شما یادآوری کنم که یک رویداد اعلانی است مبنی بر اینکه یک سوکت خاص قادر است یک عملیات I/O غیر مسدود کننده را انجام دهد.
  • کنترل کننده رویداد تابعی است که توسط راکتور ورودی/خروجی هنگام دریافت رویداد فراخوانی می‌شود و سپس یک عملیات ورودی/خروجی غیرمسدود را انجام می‌دهد.

توجه به این نکته مهم است که راکتور ورودی/خروجی بنا به تعریف تک رشته ای است، اما هیچ چیز مانع استفاده از این مفهوم در یک محیط چند رشته ای با نسبت 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;

لطفا توجه داشته باشید که ما قابلیت رسیدگی را فعال کرده ایم نوع ناقص با توجه به شاخص که در 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;
}

پس از اینکه راکتور 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، که شامل نمونه های اولیه تابع زیر است:

نمایش نمونه های اولیه تابع در common.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() اجرای thread را متوقف نکرد.
  • اگر 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 به طور کامل دریافت شود، سپس یک handler را ثبت می کند 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()، آماده ارسال داده است. این تابع یک پاسخ HTTP حاوی HTML با یک تصویر را برای مشتری ارسال می کند و سپس کنترل کننده رویداد را به آن تغییر می دهد. 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 -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, 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 با امکانات کامل

می بینیم که پس از چند صد اتصال، تعداد درخواست های پردازش شده برای هر دو سرور به شدت کاهش می یابد (در نسخه چند رشته ای این بیشتر قابل توجه است). آیا این مربوط به اجرای پشته TCP/IP لینوکس است؟ با خیال راحت می توانید فرضیات خود را در مورد این رفتار نمودار و بهینه سازی گزینه های چند رشته ای و تک رشته ای در نظرات بنویسید.

مانند اشاره شد در نظرات، این تست عملکرد رفتار راکتور I/O را تحت بارهای واقعی نشان نمی دهد، زیرا تقریباً همیشه سرور با پایگاه داده تعامل دارد، گزارش ها را خروجی می کند، از رمزنگاری با استفاده می کند. TLS و غیره که در نتیجه بار غیر یکنواخت (دینامیک) می شود. آزمایشات همراه با مؤلفه های شخص ثالث در مقاله مربوط به پرواکتور I/O انجام می شود.

معایب راکتور ورودی/خروجی

باید بدانید که راکتور I/O بدون اشکال نیست، یعنی:

  • استفاده از یک راکتور I/O در یک محیط چند رشته ای تا حدودی دشوارتر است، زیرا شما باید به صورت دستی جریان ها را مدیریت کنید.
  • تمرین نشان می دهد که در بیشتر موارد بار غیریکنواخت است، که می تواند منجر به ورود یک رشته شود در حالی که دیگری مشغول کار است.
  • اگر یک کنترل کننده رویداد یک رشته را مسدود کند، خود انتخابگر سیستم نیز مسدود می شود، که می تواند منجر به باگ های سخت شود.

این مشکلات را حل می کند پرواکتور ورودی/خروجی، که اغلب دارای یک زمانبندی است که بار را به طور مساوی به مجموعه ای از نخ ها توزیع می کند و همچنین دارای یک API راحت تر است. بعداً در مقاله دیگرم در مورد آن صحبت خواهیم کرد.

نتیجه

اینجاست که سفر ما از تئوری مستقیم به اگزوز پروفیلر به پایان رسیده است.

شما نباید روی این موضوع تمرکز کنید، زیرا بسیاری از رویکردهای به همان اندازه جالب دیگر برای نوشتن نرم افزار شبکه با سطوح مختلف راحتی و سرعت وجود دارد. جالب است، به نظر من، لینک های زیر داده شده است.

تا دفعه بعد!

پروژه های جالب

دیگه چی بخونم؟

منبع: www.habr.com

اضافه کردن نظر