To'liq xususiyatli yalang'och C I/O reaktori

To'liq xususiyatli yalang'och C I/O reaktori

kirish

I/U reaktori (bitta ipli voqea tsikli) ko'plab mashhur echimlarda qo'llaniladigan yuqori yuklangan dasturiy ta'minotni yozish uchun namunadir:

Ushbu maqolada biz kiritish-chiqarish reaktorining ichki va tashqi tomonlarini va uning qanday ishlashini ko'rib chiqamiz, 200 dan kam kod satrida dasturni yozamiz va 40 million so'rovlar / min dan ortiq oddiy HTTP server jarayonini amalga oshiramiz.

muqaddima

  • Maqola I/U reaktorining ishlashini tushunishga yordam berish va shuning uchun uni ishlatishda xavflarni tushunish uchun yozilgan.
  • Maqolani tushunish uchun asoslarni bilish kerak. C tili va tarmoq ilovalarini ishlab chiqishda ba'zi tajriba.
  • Barcha kodlar C tilida qat'iy ravishda ( ga muvofiq yozilgan)Diqqat: uzoq PDF) C11 standartiga Linux uchun va mavjud GitHub.

Nima uchun buni?

Internetning tobora ommalashib borishi bilan veb-serverlar bir vaqtning o'zida juda ko'p sonli ulanishlarni boshqarishga muhtoj bo'la boshladilar va shuning uchun ikkita yondashuv sinab ko'rildi: OTning ko'p sonli iplarida kiritish/chiqarishni bloklash va bloklanmasdan kirish/chiqish bilan birgalikda. "tizim selektori" deb ham ataladigan hodisa haqida xabar berish tizimi (epoll/navbat/IOCP/va boshqalar).

Birinchi yondashuv har bir kiruvchi ulanish uchun yangi operatsion tizimni yaratishni o'z ichiga oladi. Uning kamchiligi yomon miqyosda: operatsion tizim ko'pchilikni amalga oshirishi kerak bo'ladi kontekstga o'tish и tizim qo'ng'iroqlari. Ular qimmat operatsiyalardir va ta'sirchan miqdordagi ulanishlar bilan bepul RAM etishmasligiga olib kelishi mumkin.

O'zgartirilgan versiya diqqatga sazovordir belgilangan iplar soni (thread pool), shu bilan tizimning bajarilishini to'xtatib qo'yishiga yo'l qo'ymaydi, lekin shu bilan birga yangi muammoni keltirib chiqaradi: agar iplar hovuzi hozirda uzoq o'qish operatsiyalari bilan bloklangan bo'lsa, u holda ma'lumotni qabul qila oladigan boshqa rozetkalar buni amalga oshira olmaydi. shunday qiling.

Ikkinchi yondashuv qo'llaniladi hodisalar haqida xabar berish tizimi (tizim selektori) OT tomonidan taqdim etiladi. Ushbu maqolada emas, balki kiritish-chiqarish operatsiyalariga tayyorligi haqidagi ogohlantirishlarga (hodisalar, bildirishnomalar) asoslangan tizim selektorining eng keng tarqalgan turi muhokama qilinadi. ularning tugallanganligi to'g'risida bildirishnomalar. Uni ishlatishning soddalashtirilgan misoli quyidagi blok diagramma bilan ifodalanishi mumkin:

To'liq xususiyatli yalang'och C I/O reaktori

Ushbu yondashuvlar orasidagi farq quyidagicha:

  • I/U operatsiyalarini bloklash to'xtatib turish foydalanuvchi oqimi qadarOS to'g'ri bo'lgunga qadar defragmentatsiyalar kiruvchi IP paketlar bayt oqimiga (TCP, ma'lumotlarni qabul qilish) yoki keyinchalik yuborish uchun ichki yozish buferlarida etarli joy bo'lmaydi. HECH (ma'lumotlarni yuborish).
  • Tizim selektori Bir oz vaqtdan keyin dasturga OS haqida xabar beradi allaqachon defragmentatsiya qilingan IP-paketlar (TCP, ma'lumotlarni qabul qilish) yoki ichki yozish buferlarida etarli joy allaqachon mavjud (ma'lumotlarni yuborish).

Xulosa qilib aytadigan bo'lsak, har bir kiritish-chiqarish uchun OS ipini zaxiralash hisoblash quvvatini behuda sarflashdir, chunki aslida iplar foydali ish qilmayapti (bu atama shu erdan kelib chiqqan) "dasturiy ta'minot uzilishi"). Tizim selektori ushbu muammoni hal qiladi, bu foydalanuvchi dasturiga CPU resurslaridan ancha tejamkor foydalanish imkonini beradi.

I/U reaktor modeli

I/U reaktori tizim selektori va foydalanuvchi kodi o'rtasida qatlam vazifasini bajaradi. Uning ishlash printsipi quyidagi blok diagramma bilan tavsiflanadi:

To'liq xususiyatli yalang'och C I/O reaktori

  • Shuni eslatib o'tamanki, voqea ma'lum bir rozetka bloklanmaydigan kiritish-chiqarish operatsiyasini bajarishga qodirligi haqidagi bildirishnomadir.
  • Hodisa ishlov beruvchisi hodisa qabul qilinganda kiritish-chiqarish reaktori tomonidan chaqiriladigan funksiya bo‘lib, u keyin bloklanmaydigan kiritish-chiqarish operatsiyasini bajaradi.

Shuni ta'kidlash kerakki, kiritish-chiqarish reaktori ta'rifi bo'yicha bitta tishli, lekin kontseptsiyani ko'p tarmoqli muhitda 1 ip: 1 reaktor nisbatida ishlatishga hech narsa to'sqinlik qilmaydi va shu bilan barcha CPU yadrolarini qayta ishlaydi.

Реализация

Biz umumiy interfeysni faylga joylashtiramiz reactor.h, va amalga oshirish - yilda reactor.c. reactor.h quyidagi e'lonlardan iborat bo'ladi:

Deklaratsiyalarni reactor.h da ko'rsatish

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/U reaktor tuzilishi quyidagilardan iborat fayl deskriptori selektor epoll и hash jadvallari GHashTable, bu har bir rozetkaga mos keladi CallbackData (hodisalar ishlovchisining tuzilishi va uning uchun foydalanuvchi argumenti).

Reactor va CallbackMa'lumotlarni ko'rsatish

struct reactor {
    int epoll_fd;
    GHashTable *table; // (int, CallbackData)
};

typedef struct {
    Callback callback;
    void *arg;
} CallbackData;

Esda tutingki, biz ishlov berish imkoniyatini yoqdik to'liq bo'lmagan turi indeks bo'yicha. IN reactor.h tuzilishini e’lon qilamiz reactorvaqt ichida reactor.c biz uni aniqlaymiz, shu bilan foydalanuvchi o'z maydonlarini aniq o'zgartirishga yo'l qo'ymaydi. Bu naqshlardan biri ma'lumotlarni yashirish, bu qisqacha C semantikasiga mos keladi.

Vazifalar reactor_register, reactor_deregister и reactor_reregister tizim selektori va xesh jadvalidagi qiziqarli rozetkalar ro'yxatini va tegishli hodisa ishlov beruvchilarini yangilang.

Ro'yxatga olish funktsiyalarini ko'rsatish

#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/U reaktori hodisani tavsiflovchi bilan to'xtatgandan so'ng fd, u o'tadigan tegishli hodisa ishlov beruvchisini chaqiradi fd, bit niqob yaratilgan hodisalar va foydalanuvchi ko'rsatgichi void.

reactor_run() funktsiyasini ko'rsatish

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

Xulosa qilib aytganda, foydalanuvchi kodidagi funksiya chaqiruvlari zanjiri quyidagi shaklni oladi:

To'liq xususiyatli yalang'och C I/O reaktori

Yagona tarmoqli server

Kirish-chiqarish reaktorini yuqori yuk ostida sinab ko'rish uchun biz har qanday so'rovga tasvir bilan javob beradigan oddiy HTTP veb-serverini yozamiz.

HTTP protokoliga tezkor havola

HTTP - bu protokol dastur darajasi, birinchi navbatda server-brauzer o'zaro aloqasi uchun ishlatiladi.

HTTP-dan osongina foydalanish mumkin transport protokol TCP, belgilangan formatda xabarlarni yuborish va qabul qilish spetsifikatsiya.

So'rov formati

<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • CRLF ikki belgilar ketma-ketligidir: r и n, so'rovning birinchi qatorini, sarlavhalarni va ma'lumotlarni ajratish.
  • <КОМАНДА> - bittasi CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Brauzer bizning serverimizga buyruq yuboradi GET, ya'ni "Menga fayl mazmunini yuboring".
  • <URI> - yagona resurs identifikatori. Masalan, agar URI = /index.html, keyin mijoz saytning asosiy sahifasini so'raydi.
  • <ВЕРСИЯ HTTP> — formatdagi HTTP protokoli versiyasi HTTP/X.Y. Bugungi kunda eng ko'p ishlatiladigan versiya HTTP/1.1.
  • <ЗАГОЛОВОК N> formatdagi kalit-qiymat juftligidir <КЛЮЧ>: <ЗНАЧЕНИЕ>, qo'shimcha tahlil qilish uchun serverga yuboriladi.
  • <ДАННЫЕ> — operatsiyani bajarish uchun server tomonidan talab qilinadigan ma'lumotlar. Ko'pincha bu oddiy JSON yoki boshqa format.

Javob formati

<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • <КОД СТАТУСА> operatsiya natijasini ifodalovchi raqamdir. Bizning serverimiz har doim 200 holatini qaytaradi (muvaffaqiyatli operatsiya).
  • <ОПИСАНИЕ СТАТУСА> — holat kodining satrli tasviri. 200 holat kodi uchun bu OK.
  • <ЗАГОЛОВОК N> — so‘rovdagi kabi formatdagi sarlavha. Sarlavhalarni qaytaramiz Content-Length (fayl hajmi) va Content-Type: text/html (ma'lumotlar turini qaytarish).
  • <ДАННЫЕ> — foydalanuvchi tomonidan so'ralgan ma'lumotlar. Bizning holatda, bu tasvirga olib boradigan yo'l HTML.

Fayl http_server.c (bitta tishli server) faylni o'z ichiga oladi common.h, unda quyidagi funksiya prototiplari mavjud:

Umumiy funksiya prototiplarini ko'rsatish.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);

Funktsional makro ham tasvirlangan SAFE_CALL() va funksiya aniqlanadi fail(). Makros ifoda qiymatini xato bilan taqqoslaydi va agar shart rost bo'lsa, funksiyani chaqiradi fail():

#define SAFE_CALL(call, error)                                                 
    do {                                                                       
        if ((call) == error) {                                                   
            fail("%s", #call);                                                 
        }                                                                      
    } while (false)

vazifa fail() o'tkazilgan argumentlarni terminalga chop etadi (masalan printf()) va dasturni kod bilan tugatadi 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);
}

vazifa new_server() tizim chaqiruvlari orqali yaratilgan "server" soketining fayl deskriptorini qaytaradi socket(), bind() и listen() va bloklanmaydigan rejimda kiruvchi ulanishlarni qabul qila oladi.

new_server() funktsiyasini ko'rsatish

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

  • E'tibor bering, rozetka dastlab bayroq yordamida bloklanmaydigan rejimda yaratilgan SOCK_NONBLOCKshuning uchun funktsiyada on_accept() (batafsil o'qing) tizim chaqiruvi accept() ip bajarilishini to'xtatmadi.
  • agar reuse_port tengdir true, keyin bu funksiya opsiya bilan rozetkani sozlaydi SO_REUSEPORT orqali setsockopt()bir xil portni ko'p tarmoqli muhitda ishlatish uchun ("Ko'p tarmoqli server" bo'limiga qarang).

Voqealar boshqaruvchisi on_accept() OS voqea yaratgandan keyin chaqiriladi EPOLLIN, bu holda yangi ulanishni qabul qilish mumkinligini anglatadi. on_accept() yangi ulanishni qabul qiladi, uni bloklanmaydigan rejimga o'tkazadi va voqea ishlov beruvchisi bilan ro'yxatdan o'tadi on_recv() I/U reaktorida.

on_accept() funktsiyasini ko'rsatish

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

Voqealar boshqaruvchisi on_recv() OS voqea yaratgandan keyin chaqiriladi EPOLLIN, bu holda ulanish qayd etilganligini bildiradi on_accept(), ma'lumotlarni olishga tayyor.

on_recv() HTTP so'rovi to'liq qabul qilinmaguncha ulanishdan ma'lumotlarni o'qiydi, so'ngra ishlov beruvchini ro'yxatdan o'tkazadi on_send() HTTP javobini yuborish uchun. Agar mijoz ulanishni buzsa, rozetka ro'yxatdan o'chiriladi va yordamida yopiladi close().

Funktsiyani ko'rsatish 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);
    }
}

Voqealar boshqaruvchisi on_send() OS voqea yaratgandan keyin chaqiriladi EPOLLOUT, ya'ni ulanish qayd etilgan on_recv(), ma'lumotlarni yuborishga tayyor. Ushbu funktsiya mijozga HTML-ni o'z ichiga olgan HTTP javobini tasvir bilan yuboradi va keyin voqea ishlovchisini qayta o'zgartiradi on_recv().

on_send() funktsiyasini ko'rsatish

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

Va nihoyat, faylda http_server.c, funktsiyada main() yordamida kiritish-chiqarish reaktorini yaratamiz reactor_new(), server soketini yarating va uni ro'yxatdan o'tkazing, reaktorni ishga tushiring reactor_run() aniq bir daqiqa, keyin biz resurslarni bo'shatib, dasturdan chiqamiz.

http_server.c ni ko'rsatish

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

Keling, hamma narsa kutilganidek ishlayotganini tekshirib ko'raylik. kompilyatsiya qilish (chmod a+x compile.sh && ./compile.sh loyiha ildizida) va o'z-o'zidan yozilgan serverni ishga tushiring, oching http://127.0.0.1:18470 brauzerda va biz kutgan narsani ko'ring:

To'liq xususiyatli yalang'och C I/O reaktori

Samaradorlikni o'lchash

Avtomobilimning texnik xususiyatlarini ko'rsating

$ 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

Keling, bitta tarmoqli serverning ishlashini o'lchaymiz. Keling, ikkita terminalni ochamiz: birida biz ishga tushamiz ./http_server, boshqacha - buzmoq. Bir daqiqadan so'ng, ikkinchi terminalda quyidagi statistika ko'rsatiladi:

$ 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

Bizning yagona tarmoqli serverimiz 11 ta ulanishdan kelib chiqadigan daqiqada 100 milliondan ortiq so'rovlarni qayta ishlashga muvaffaq bo'ldi. Yomon natija emas, lekin uni yaxshilash mumkinmi?

Ko'p tarmoqli server

Yuqorida aytib o'tilganidek, kiritish-chiqarish reaktori alohida torlarda yaratilishi mumkin va shu bilan barcha CPU yadrolaridan foydalanadi. Keling, ushbu yondashuvni amalda qo'llaymiz:

http_server_multithreaded.c ni ko'rsatish

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

Endi har bir mavzu o'ziga tegishli reaktor:

static Reactor *reactor;
#pragma omp threadprivate(reactor)

Funktsiya argumenti ekanligini unutmang new_server() himoyachilar true. Bu shuni anglatadiki, biz opsiyani server soketiga tayinlaymiz SO_REUSEPORTuni ko'p tarmoqli muhitda ishlatish uchun. Batafsil ma'lumotlarni o'qishingiz mumkin shu yerda.

Ikkinchi yugurish

Keling, ko'p tarmoqli serverning ishlashini o'lchaymiz:

$ 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 daqiqada ko'rib chiqilgan so'rovlar soni ~3.28 martaga oshdi! Ammo biz bu raqamga atigi ~ XNUMX million kam edik, shuning uchun buni tuzatishga harakat qilaylik.

Avval yaratilgan statistik ma'lumotlarni ko'rib chiqaylik mukammal:

$ 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 yaqinligidan foydalanish, bilan kompilyatsiya -march=native, PGO, xitlar sonining ortishi kesh, kattalashtirish; ko'paytirish MAX_EVENTS va foydalaning EPOLLET unumdorlikning sezilarli o'sishini ta'minlamadi. Ammo bir vaqtning o'zida ulanishlar sonini ko'paytirsangiz nima bo'ladi?

Bir vaqtning o'zida 352 ta ulanish statistikasi:

$ 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

Istalgan natija olindi va u bilan 1 daqiqada qayta ishlangan so'rovlar soni ulanishlar soniga bog'liqligini ko'rsatadigan qiziqarli grafik:

To'liq xususiyatli yalang'och C I/O reaktori

Bir necha yuzta ulanishdan so'ng, ikkala server uchun qayta ishlangan so'rovlar soni keskin kamayib borayotganini ko'ramiz (ko'p tarmoqli versiyada bu ko'proq sezilarli). Bu Linux TCP/IP stekini amalga oshirish bilan bog'liqmi? Izohlarda grafikning ushbu xatti-harakati va ko'p va bitta ipli variantlarni optimallashtirish haqida o'z taxminlaringizni yozing.

qanday deya ta'kidladi izohlarda, ushbu ishlash testi haqiqiy yuklanishlar ostida I/U reaktorining xatti-harakatlarini ko'rsatmaydi, chunki deyarli har doim server ma'lumotlar bazasi bilan o'zaro ta'sir qiladi, jurnallarni chiqaradi, kriptografiyadan foydalanadi. TLS va hokazo, buning natijasida yuk bir xil bo'lmagan (dinamik) bo'ladi. Sinovlar uchinchi tomon komponentlari bilan birgalikda kiritish-chiqarish proaktori haqidagi maqolada o'tkaziladi.

I/U reaktorining kamchiliklari

I/U reaktorining kamchiliklari yo'qligini tushunishingiz kerak, xususan:

  • Ko'p tarmoqli muhitda I/U reaktoridan foydalanish biroz qiyinroq, chunki siz oqimlarni qo'lda boshqarishingiz kerak bo'ladi.
  • Amaliyot shuni ko'rsatadiki, ko'p hollarda yuk bir xil bo'lmaydi, bu esa bir ipning jurnaliga olib kelishi mumkin, ikkinchisi esa ish bilan band.
  • Agar bitta hodisa ishlov beruvchisi ipni bloklasa, tizim selektorining o'zi ham bloklanadi, bu esa topish qiyin bo'lgan xatolarga olib kelishi mumkin.

Bu muammolarni hal qiladi I/O proaktori, bu ko'pincha yukni iplar hovuziga teng taqsimlaydigan rejalashtiruvchiga ega, shuningdek, yanada qulayroq APIga ega. Bu haqda keyinroq, boshqa maqolamda gaplashamiz.

xulosa

Bu erda nazariyadan to'g'ridan-to'g'ri profiler egzosiga bo'lgan sayohatimiz yakunlandi.

Siz bu haqda to'xtalmasligingiz kerak, chunki turli darajadagi qulaylik va tezlik bilan tarmoq dasturiy ta'minotini yozishning boshqa bir xil qiziqarli yondashuvlari mavjud. Qiziqarli, menimcha, havolalar quyida keltirilgan.

Keyingi safargacha!

Qiziqarli loyihalar

Yana nimani o'qishim kerak?

Manba: www.habr.com

a Izoh qo'shish