مکمل خصوصیات والا ننگا-C I/O ری ایکٹر

مکمل خصوصیات والا ننگا-C I/O ری ایکٹر

تعارف

I/O ری ایکٹر (سنگل تھریڈڈ ایونٹ لوپ) ہائی لوڈ سافٹ ویئر لکھنے کا ایک نمونہ ہے، جو بہت سے مشہور حلوں میں استعمال ہوتا ہے:

اس آرٹیکل میں، ہم I/O ری ایکٹر کے ان اور آؤٹس کو دیکھیں گے اور یہ کیسے کام کرتا ہے، کوڈ کی 200 سے کم لائنوں میں ایک نفاذ لکھیں گے، اور 40 ملین درخواستوں/منٹ سے زیادہ ایک سادہ HTTP سرور عمل کریں گے۔

کردار

  • یہ مضمون I/O ری ایکٹر کے کام کو سمجھنے میں مدد کے لیے لکھا گیا تھا، اور اس لیے اسے استعمال کرتے وقت خطرات کو سمجھیں۔
  • مضمون کو سمجھنے کے لیے بنیادی باتوں کا علم درکار ہے۔ سی زبان اور نیٹ ورک ایپلی کیشن ڈویلپمنٹ میں کچھ تجربہ۔
  • تمام کوڈ C زبان میں سختی سے لکھا جاتا ہے (احتیاط: لمبی پی ڈی ایف) C11 کے معیار پر لینکس کے لیے اور دستیاب ہے۔ GitHub کے.

یہ کیوں ضروری ہے؟

انٹرنیٹ کی بڑھتی ہوئی مقبولیت کے ساتھ، ویب سرورز کو ایک ساتھ بڑی تعداد میں کنکشنز کو سنبھالنے کی ضرورت پڑنے لگی، اور اس لیے دو طریقے آزمائے گئے: بڑی تعداد میں OS تھریڈز پر I/O کو بلاک کرنا اور I/O کو بلاک نہ کرنا۔ ایک واقعہ کی اطلاع کا نظام، جسے "سسٹم سلیکٹر" بھی کہا جاتا ہے (epoll/قطار/IOCP/etc)۔

پہلے نقطہ نظر میں ہر آنے والے کنکشن کے لیے ایک نیا OS تھریڈ بنانا شامل تھا۔ اس کا نقصان ناقص اسکیل ایبلٹی ہے: آپریٹنگ سسٹم کو بہت سے لاگو کرنا ہوں گے۔ سیاق و سباق کی منتقلی и سسٹم کالز. یہ مہنگے آپریشن ہیں اور متاثر کن کنکشنز کے ساتھ مفت RAM کی کمی کا باعث بن سکتے ہیں۔

ترمیم شدہ ورژن نمایاں کرتا ہے۔ دھاگوں کی مقررہ تعداد (تھریڈ پول)، اس طرح سسٹم کو عملدرآمد کو ختم کرنے سے روکتا ہے، لیکن ساتھ ہی ایک نیا مسئلہ بھی پیش کرتا ہے: اگر فی الحال تھریڈ پول کو طویل پڑھنے کی کارروائیوں کے ذریعے بلاک کر دیا گیا ہے، تو دوسرے ساکٹ جو پہلے سے ڈیٹا حاصل کرنے کے قابل ہیں، اس قابل نہیں ہوں گے۔ ایسا کرو

دوسرا طریقہ استعمال کرتا ہے۔ واقعہ کی اطلاع کا نظام (سسٹم سلیکٹر) OS کے ذریعہ فراہم کردہ۔ یہ مضمون I/O آپریشنز کے لیے تیاری کے بارے میں انتباہات (واقعات، اطلاعات) کی بنیاد پر، سسٹم سلیکٹر کی سب سے عام قسم پر بحث کرتا ہے۔ ان کی تکمیل کے بارے میں اطلاعات. اس کے استعمال کی ایک آسان مثال کو مندرجہ ذیل بلاک ڈایاگرام سے دکھایا جا سکتا ہے۔

مکمل خصوصیات والا ننگا-C I/O ری ایکٹر

ان طریقوں کے درمیان فرق مندرجہ ذیل ہے:

  • I/O آپریشنز کو مسدود کرنا معطل صارف کا بہاؤ جب تکجب تک کہ OS ٹھیک نہ ہو۔ ڈیفراگمنٹس آنے والا آئی پی پیکٹ بائٹ سٹریم کے لیے (ٹی سی پی، ڈیٹا وصول کرنا) یا اندرونی تحریری بفرز میں بعد میں بھیجنے کے لیے کافی جگہ دستیاب نہیں ہوگی این آئی سی (ڈیٹا بھیجنا)۔
  • سسٹم سلیکٹر اضافی وقت پروگرام کو مطلع کرتا ہے کہ OS پہلے ہی ڈیفراگمنٹڈ IP پیکٹ (TCP، ڈیٹا ریسپشن) یا اندرونی تحریری بفرز میں کافی جگہ پہلے ہی دستیاب (ڈیٹا بھیجنا)۔

اس کا خلاصہ یہ ہے کہ ہر I/O کے لیے OS تھریڈ کو محفوظ کرنا کمپیوٹنگ پاور کا ضیاع ہے، کیونکہ حقیقت میں، تھریڈز مفید کام نہیں کر رہے ہیں (لہذا اصطلاح "سافٹ ویئر مداخلت")۔ سسٹم سلیکٹر اس مسئلے کو حل کرتا ہے، جس سے صارف پروگرام کو سی پی یو کے وسائل کو زیادہ اقتصادی طور پر استعمال کرنے کی اجازت دیتا ہے۔

I/O ری ایکٹر ماڈل

I/O ری ایکٹر سسٹم سلیکٹر اور یوزر کوڈ کے درمیان ایک پرت کے طور پر کام کرتا ہے۔ اس کے آپریشن کے اصول کو مندرجہ ذیل بلاک ڈایاگرام کے ذریعے بیان کیا گیا ہے۔

مکمل خصوصیات والا ننگا-C I/O ری ایکٹر

  • میں آپ کو یاد دلاتا ہوں کہ ایک واقعہ ایک اطلاع ہے کہ ایک مخصوص ساکٹ غیر مسدود I/O آپریشن کرنے کے قابل ہے۔
  • ایک ایونٹ ہینڈلر ایک فنکشن ہے جسے I/O ری ایکٹر کے ذریعہ بلایا جاتا ہے جب کوئی واقعہ موصول ہوتا ہے، جو پھر غیر مسدود I/O آپریشن کرتا ہے۔

یہ نوٹ کرنا ضروری ہے کہ 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);

I/O ری ایکٹر کی ساخت پر مشتمل ہے۔ فائل کی وضاحت کرنے والا سلیکٹر epoll и ہیش میزیں GHashTable، جو ہر ساکٹ کا نقشہ بناتا ہے۔ 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.

ری ایکٹر_رن () فنکشن دکھائیں۔

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 ری ایکٹر

سنگل تھریڈڈ سرور

زیادہ بوجھ کے تحت I/O ری ایکٹر کو جانچنے کے لیے، ہم ایک سادہ HTTP ویب سرور لکھیں گے جو تصویر کے ساتھ کسی بھی درخواست کا جواب دیتا ہے۔

HTTP پروٹوکول کا فوری حوالہ

HTTP - یہ پروٹوکول ہے۔ درخواست کی سطح، بنیادی طور پر سرور براؤزر کے تعامل کے لیے استعمال کیا جاتا ہے۔

HTTP آسانی سے استعمال کیا جا سکتا ہے نقل و حمل پروٹوکول ٹی سی پی، ایک مخصوص فارمیٹ میں پیغامات بھیجنا اور وصول کرنا تفصیلات.

فارمیٹ کی درخواست کریں۔

<КОМАНДА> <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، جس میں مندرجہ ذیل فنکشن پروٹو ٹائپس شامل ہیں:

کامن میں فنکشن پروٹو ٹائپ دکھائیں۔

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
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()ڈیٹا بھیجنے کے لیے تیار ہے۔ یہ فنکشن کلائنٹ کو ایک تصویر کے ساتھ ایچ ٹی ایم ایل پر مشتمل ایک 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 ملین سے زیادہ درخواستوں پر کارروائی کرنے کے قابل تھا۔ برا نتیجہ نہیں، لیکن کیا اسے بہتر کیا جا سکتا ہے؟

ملٹی تھریڈ سرور

جیسا کہ اوپر ذکر کیا گیا ہے، 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 وابستگی کا استعمالکے ساتھ تالیف -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 I/O ری ایکٹر

ہم دیکھتے ہیں کہ چند سو کنکشن کے بعد، دونوں سرورز کے لیے پروسیس شدہ درخواستوں کی تعداد میں تیزی سے کمی آتی ہے (ملٹی تھریڈڈ ورژن میں یہ زیادہ قابل توجہ ہے)۔ کیا یہ لینکس TCP/IP اسٹیک کے نفاذ سے متعلق ہے؟ گراف کے اس رویے اور کمنٹس میں ملٹی تھریڈڈ اور سنگل تھریڈڈ آپشنز کے لیے آپٹیمائزیشن کے بارے میں بلا جھجھک اپنے مفروضے لکھیں۔

جیسا کہ نوٹ کیا تبصروں میں، کارکردگی کا یہ ٹیسٹ حقیقی بوجھ کے تحت I/O ری ایکٹر کے رویے کو نہیں دکھاتا ہے، کیونکہ تقریباً ہمیشہ سرور ڈیٹا بیس کے ساتھ تعامل کرتا ہے، لاگ آؤٹ کرتا ہے، کرپٹوگرافی کا استعمال کرتا ہے۔ TLS وغیرہ، جس کے نتیجے میں بوجھ غیر یکساں (متحرک) ہو جاتا ہے۔ تیسرے فریق کے اجزاء کے ساتھ ٹیسٹ I/O پرویکٹر کے بارے میں مضمون میں کئے جائیں گے۔

I/O ری ایکٹر کے نقصانات

آپ کو یہ سمجھنے کی ضرورت ہے کہ I/O ری ایکٹر اپنی خرابیوں کے بغیر نہیں ہے، یعنی:

  • کثیر تھریڈ والے ماحول میں I/O ری ایکٹر کا استعمال کچھ زیادہ مشکل ہے، کیونکہ آپ کو دستی طور پر بہاؤ کا انتظام کرنا پڑے گا۔
  • پریکٹس سے پتہ چلتا ہے کہ زیادہ تر معاملات میں بوجھ غیر یکساں ہوتا ہے، جس کی وجہ سے ایک تھریڈ لاگنگ ہوتا ہے جبکہ دوسرا کام میں مصروف ہوتا ہے۔
  • اگر ایک ایونٹ ہینڈلر کسی تھریڈ کو بلاک کرتا ہے، تو سسٹم سلیکٹر خود بھی بلاک کر دے گا، جس سے ڈھونڈنے میں مشکل پیدا ہو سکتی ہے۔

ان مسائل کو حل کرتا ہے۔ I/O پرویکٹر، جس میں اکثر ایک شیڈیولر ہوتا ہے جو دھاگوں کے تالاب میں بوجھ کو یکساں طور پر تقسیم کرتا ہے، اور اس میں زیادہ آسان API بھی ہوتا ہے۔ ہم اس کے بارے میں بعد میں اپنے دوسرے مضمون میں بات کریں گے۔

حاصل يہ ہوا

یہ وہ جگہ ہے جہاں تھیوری سے سیدھے پروفائلر ایگزاسٹ تک ہمارا سفر اختتام کو پہنچا۔

آپ کو اس پر غور نہیں کرنا چاہیے، کیونکہ نیٹ ورک سافٹ ویئر لکھنے کے لیے بہت سے دوسرے یکساں طور پر دلچسپ طریقے ہیں جن کی مختلف سطحوں کی سہولت اور رفتار ہے۔ دلچسپ، میری رائے میں، لنکس ذیل میں دیئے گئے ہیں.

جلد ہی ملیں گے!

دلچسپ منصوبے

مجھے اور کیا پڑھنا چاہیے؟

ماخذ: www.habr.com

نیا تبصرہ شامل کریں