ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

መግቢያ

I/O ሬአክተር (ነጠላ ክር ክስተት loop) በብዙ ታዋቂ መፍትሄዎች ውስጥ ጥቅም ላይ የሚውል ከፍተኛ ጭነት ያለው ሶፍትዌር ለመጻፍ ንድፍ ነው፡

በዚህ ጽሁፍ ውስጥ የI/O ሬአክተርን እና እንዴት እንደሚሰራ እንመለከታለን፣ከ200 ባነሰ የኮድ መስመር ትግበራን እንፅፋለን፣እና ቀላል የኤችቲቲፒ አገልጋይ ሂደት ከ40 ሚሊዮን በላይ ጥያቄዎች/ደቂቃ እናሰራለን።

መቅድም

  • ጽሁፉ የተጻፈው የ I/O ሬአክተርን ተግባር ለመረዳት እንዲረዳ ነው፣ እና ስለዚህ እሱን ሲጠቀሙ ስጋቶችን ለመረዳት።
  • ጽሑፉን ለመረዳት ስለ መሰረታዊ ነገሮች እውቀት ያስፈልጋል. ሐ ቋንቋ እና በአውታረ መረብ መተግበሪያ ልማት ውስጥ የተወሰነ ልምድ።
  • ሁሉም ኮድ የተፃፈው በ C ቋንቋ ነው (በ)ጥንቃቄ: ረጅም ፒዲኤፍ) ወደ C11 መደበኛ ለሊኑክስ እና በ ላይ ይገኛል። የፊልሙ.

ይህ አስፈላጊ የሆነው ለምንድን ነው?

የበይነመረብ ተወዳጅነት እየጨመረ በመምጣቱ የድር አገልጋዮች ብዙ ግንኙነቶችን በአንድ ጊዜ ማስተናገድ ጀመሩ ፣ እና ስለሆነም ሁለት አቀራረቦች ተሞክረዋል- I/Oን በበርካታ የስርዓተ ክወና ክሮች ላይ ማገድ እና I/Oን ከ ጋር በማጣመር የክስተት ማሳወቂያ ስርዓት፣ እንዲሁም “የስርዓት መራጭ” ተብሎም ይጠራል (epoll/kqueue/አይኦሲፒ/ወዘተ)።

የመጀመሪያው አቀራረብ ለእያንዳንዱ ገቢ ግንኙነት አዲስ ስርዓተ ክወና መፍጠርን ያካትታል። ጉዳቱ ደካማ መጠነ-ሰፊ ነው-ስርዓተ ክወናው ብዙዎችን መተግበር አለበት። የአውድ ሽግግሮች и የስርዓት ጥሪዎች. ውድ ኦፕሬሽኖች ናቸው እና በሚያስደንቅ የግንኙነት ብዛት ወደ ነፃ ራም እጥረት ሊያመራ ይችላል።

የተሻሻለው እትም ያደምቃል ቋሚ የክሮች ብዛት (ክር ገንዳ) ፣ በዚህም ስርዓቱ እንዳይበላሽ ይከላከላል ፣ ግን በተመሳሳይ ጊዜ አዲስ ችግርን ያስተዋውቃል-የክር ገንዳ በአሁኑ ጊዜ በረጅም ንባብ ክዋኔዎች ከታገደ ፣ ከዚያ ቀደም ሲል መረጃን መቀበል የሚችሉ ሌሎች ሶኬቶች ማድረግ አይችሉም። ስለዚህ.

ሁለተኛው ዘዴ ይጠቀማል የክስተት ማሳወቂያ ስርዓት (የስርዓት መራጭ) በ OS የቀረበ። ይህ መጣጥፍ በጣም የተለመደውን የስርዓት መምረጫ አይነት ያብራራል፣ ከማንቂያዎች (ክስተቶች፣ ማሳወቂያዎች) ጋር ስለመዘጋጀት ለአይ/ኦ ኦፕሬሽኖች ከመሆን ይልቅ። ስለ ማጠናቀቃቸው ማሳወቂያዎች. የአጠቃቀም ቀላል ምሳሌ በሚከተለው የማገጃ ንድፍ ሊወከል ይችላል።

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

በእነዚህ ዘዴዎች መካከል ያለው ልዩነት እንደሚከተለው ነው.

  • የI/O ስራዎችን ማገድ ማገድ የተጠቃሚ ፍሰት ድረስስርዓተ ክወናው በትክክል እስኪሰራ ድረስ ማበላሸት ገቢ የአይፒ ፓኬቶች ወደ ባይት ዥረት (TCP፣ መረጃ መቀበል) ወይም ለቀጣይ በመላክ ለመላክ በውስጥ መጻፊያ ቋቶች ውስጥ በቂ ቦታ አይኖርም። ምንም ነገር (መረጃ በመላክ ላይ)።
  • የስርዓት መራጭ ተጨማሪ ሰአት ስርዓተ ክወናው ለፕሮግራሙ ያሳውቃል ገና የተበላሹ የአይፒ ጥቅሎች (TCP፣ የውሂብ መቀበያ) ወይም በውስጣዊ መጻፊያ ቋቶች ውስጥ በቂ ቦታ ገና ይገኛል (ውሂብ መላክ).

ለማጠቃለል ያህል ለእያንዳንዱ I/O የስርዓተ ክወናን ክር ማቆየት የኮምፒዩተር ሃይልን ማባከን ነው፣ ምክንያቱም እንደ እውነቱ ከሆነ ክሮቹ ጠቃሚ ስራዎችን እየሰሩ አይደሉም (ስለዚህ ቃሉ "የሶፍትዌር ማቋረጥ"). የስርዓት መምረጫው ይህንን ችግር ይፈታል, የተጠቃሚው ፕሮግራም የሲፒዩ ሀብቶችን የበለጠ በኢኮኖሚ እንዲጠቀም ያስችለዋል.

I/O ሬአክተር ሞዴል

የ I/O ሬአክተር በስርዓት መራጭ እና በተጠቃሚ ኮድ መካከል እንደ ንብርብር ሆኖ ይሰራል። የአሠራሩ መርህ በሚከተለው የማገጃ ንድፍ ተገልጿል.

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

  • አንድ ክስተት አንድ የተወሰነ ሶኬት የማያግድ የአይ/ኦ ኦፕሬሽን ማከናወን እንደሚችል ማሳወቂያ መሆኑን ላስታውስዎ።
  • የክስተት ተቆጣጣሪ አንድ ክስተት ሲደርስ በ I/O ሬአክተር የሚጠራ ተግባር ነው፣ ከዚያም የማያግድ የI/O ክወናን ያከናውናል።

የ I/O ሬአክተር በትርጉሙ ነጠላ-ክር መሆኑን ልብ ማለት ያስፈልጋል፣ ነገር ግን ሀሳቡ ባለብዙ-ክር አካባቢ በ1 ክር ጥምርታ 1 ሬአክተር፣ በዚህም ሁሉንም የሲፒዩ ኮሮች እንደገና ጥቅም ላይ ከማዋል የሚያግደው ምንም ነገር የለም።

ትግበራ

ይፋዊ በይነገጹን በፋይል ውስጥ እናስቀምጣለን። reactor.h, እና ትግበራ - ውስጥ reactor.c. reactor.h የሚከተሉትን ማስታወቂያዎች ያካትታል:

መግለጫዎችን በ reactor.h. አሳይ

typedef struct reactor Reactor;

/*
 * Указатель на функцию, которая будет вызываться I/O реактором при поступлении
 * события от системного селектора.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Возвращает `NULL` в случае ошибки, не-`NULL` указатель на `Reactor` в
 * противном случае.
 */
Reactor *reactor_new(void);

/*
 * Освобождает системный селектор, все зарегистрированные сокеты в данный момент
 * времени и сам I/O реактор.
 *
 * Следующие функции возвращают -1 в случае ошибки, 0 в случае успеха.
 */
int reactor_destroy(Reactor *reactor);

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg);

/*
 * Запускает цикл событий с тайм-аутом `timeout`.
 *
 * Эта функция передаст управление вызывающему коду если отведённое время вышло
 * или/и при отсутствии зарегистрированных сокетов.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

የ I/O ሬአክተር መዋቅር ያካትታል የፋይል ገላጭ መራጭ 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;
}

ለማጠቃለል በተጠቃሚ ኮድ ውስጥ ያለው የተግባር ጥሪዎች ሰንሰለት የሚከተለውን ቅጽ ይወስዳል።

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

ነጠላ ክር አገልጋይ

በከፍተኛ ጭነት ውስጥ ያለውን የአይ/ኦ ሬአክተርን ለመሞከር፣ ማንኛውንም ጥያቄ በምስል የሚመልስ ቀላል የኤችቲቲፒ ዌብ ሰርቨር እንጽፋለን።

ለኤችቲቲፒ ፕሮቶኮል ፈጣን ማጣቀሻ

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/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 (የመመለሻ የውሂብ አይነት).
  • <ДАННЫЕ> - በተጠቃሚው የተጠየቀ ውሂብ. በእኛ ሁኔታ ፣ ወደ ምስሉ የሚወስደው መንገድ ይህ ነው። ኤችቲኤምኤል.

ፋይል 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 reactor ውስጥ።

on_ተቀበል() ተግባር አሳይ

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() የኤችቲቲፒ ጥያቄ ሙሉ በሙሉ እስኪቀበል ድረስ ከግንኙነቱ የተገኘውን መረጃ ያነባል፣ ከዚያ ተቆጣጣሪ ይመዘግባል on_send() የኤችቲቲፒ ምላሽ ለመላክ። ደንበኛው ግንኙነቱን ካቋረጠ, ሶኬቱ ይሰረዛል እና ተጠቅሞ ይዘጋል close().

ተግባር በ_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(), ውሂብ ለመላክ ዝግጁ. ይህ ተግባር ኤችቲቲኤምኤልን የያዘ ምስል ለደንበኛው ይልካል እና ከዚያ የክስተት ተቆጣጣሪውን ወደ ኋላ ይለውጠዋል 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 እንፈጥራለን 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 በአሳሹ ውስጥ እና እኛ የጠበቅነውን ይመልከቱ-

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

የአፈጻጸም መለኪያ

የመኪናዬን መመዘኛዎች አሳይ

$ 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 reactor በተለየ ክሮች ውስጥ ሊፈጠር ይችላል, በዚህም ሁሉንም የሲፒዩ ኮርሶች ይጠቀማል. ይህን አካሄድ ወደ ተግባር እናውለው፡-

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 ደቂቃ ውስጥ የተከናወኑ ጥያቄዎች ብዛት በግንኙነቶች ብዛት ላይ ጥገኛ መሆኑን የሚያሳይ አስደሳች ግራፍ።

ሙሉ ባህሪ ያለው ባዶ-ሲ አይ/ኦ ሬአክተር

ከአንድ ሁለት መቶ ግንኙነቶች በኋላ የሁለቱም አገልጋዮች የተስተካከሉ ጥያቄዎች ቁጥር በከፍተኛ ሁኔታ እየቀነሰ መሆኑን እናያለን (በባለብዙ ክር ስሪት ውስጥ ይህ የበለጠ የሚታይ ነው)። ይህ ከሊኑክስ TCP/IP ቁልል ትግበራ ጋር የተያያዘ ነው? ስለዚህ የግራፍ ባህሪ እና ለአስተያየቶች ባለብዙ-ክር እና ነጠላ-ክር አማራጮች ማመቻቸት የእርስዎን ግምቶች ለመፃፍ ነፃነት ይሰማዎ።

እንዴት ተጠቅሷል በአስተያየቶቹ ውስጥ ይህ የአፈፃፀም ሙከራ የ I / O ሬአክተርን ባህሪ በእውነተኛ ጭነቶች ውስጥ አያሳይም ፣ ምክንያቱም ሁል ጊዜ አገልጋዩ ከመረጃ ቋቱ ጋር ይገናኛል ፣ ምዝግብ ማስታወሻዎችን ያወጣል ፣ ምስጠራን ይጠቀማል። TLS ወዘተ, በዚህ ምክንያት ጭነቱ አንድ ወጥ ያልሆነ (ተለዋዋጭ) ይሆናል. ከሶስተኛ ወገን አካላት ጋር ሙከራዎች ስለ I / O ፕሮፖክተር በሚለው ጽሑፍ ውስጥ ይከናወናሉ.

የ I/O ሬአክተር ጉዳቶች

የ I/O ሬአክተር ከድክመቶቹ ውጭ እንዳልሆነ መረዳት አለቦት፡-

  • ባለብዙ-ክር አካባቢ ውስጥ I/O ሬአክተር መጠቀም በተወሰነ ደረጃ አስቸጋሪ ነው፣ ምክንያቱም ፍሰቶቹን እራስዎ ማስተዳደር አለብዎት.
  • ልምምድ እንደሚያሳየው በአብዛኛዎቹ ሁኔታዎች ሸክሙ ወጥ ያልሆነ ነው, ይህም ወደ አንድ ክር መግባትን ሊያመጣ ይችላል, ሌላኛው ደግሞ በስራ የተጠመደ ነው.
  • አንድ የክስተት ተቆጣጣሪ ክርን ከከለከለ፣ የስርዓት መራጩ ራሱ እንዲሁ ያግዳል፣ ይህ ደግሞ ለማግኘት አስቸጋሪ የሆኑ ስህተቶችን ያስከትላል።

እነዚህን ችግሮች ይፈታል አይ/ኦ ፕሮአክተር, ብዙውን ጊዜ ጭነቱን ወደ ክሮች ገንዳ እኩል የሚያከፋፍል የጊዜ መርሐግብር ያለው እና የበለጠ ምቹ ኤፒአይ አለው። በሌላኛው ጽሑፌ ውስጥ ስለ እሱ በኋላ እንነጋገራለን.

መደምደሚያ

ከቲዎሪ በቀጥታ ወደ ፕሮፋይለር ጭስ ማውጫ ጉዟችን ያበቃው እዚህ ላይ ነው።

በዚህ ላይ ማተኮር የለብዎትም ፣ ምክንያቱም የኔትወርክ ሶፍትዌሮችን በተለያዩ የምቾት እና የፍጥነት ደረጃዎች ለመፃፍ ሌሎች ብዙ እኩል አስደሳች አቀራረቦች አሉ። የሚገርመው, በእኔ አስተያየት, አገናኞች ከዚህ በታች ተሰጥተዋል.

በቅርቡ እንገናኝ!

አስደሳች ፕሮጀክቶች

ሌላ ምን ማንበብ አለብኝ?

ምንጭ: hab.com

አስተያየት ያክሉ