Reaktor I/O kosong-C berciri penuh

Reaktor I/O kosong-C berciri penuh

Pengenalan

reaktor I/O (benang tunggal gelung acara) ialah corak untuk menulis perisian beban tinggi, digunakan dalam banyak penyelesaian popular:

Dalam artikel ini, kita akan melihat selok-belok reaktor I/O dan cara ia berfungsi, menulis pelaksanaan dalam kurang daripada 200 baris kod dan membuat proses pelayan HTTP yang mudah melebihi 40 juta permintaan/min.

Perutusan

  • Artikel itu ditulis untuk membantu memahami fungsi reaktor I/O, dan oleh itu memahami risiko apabila menggunakannya.
  • Pengetahuan asas diperlukan untuk memahami artikel. bahasa C dan beberapa pengalaman dalam pembangunan aplikasi rangkaian.
  • Semua kod ditulis dalam bahasa C dengan ketat mengikut (awas: PDF panjang) kepada standard C11 untuk Linux dan tersedia pada GitHub.

Mengapa melakukannya?

Dengan populariti Internet yang semakin meningkat, pelayan web mula perlu mengendalikan sejumlah besar sambungan secara serentak, dan oleh itu dua pendekatan telah dicuba: menyekat I/O pada sejumlah besar utas OS dan I/O tidak menyekat dalam kombinasi dengan sistem pemberitahuan acara, juga dipanggil "pemilih sistem" (epoll/kqueue/IOCP/dan lain-lain).

Pendekatan pertama melibatkan mencipta benang OS baharu untuk setiap sambungan masuk. Kelemahannya ialah kebolehskalaan yang lemah: sistem pengendalian perlu melaksanakan banyak perkara peralihan konteks и panggilan sistem. Ia adalah operasi yang mahal dan boleh menyebabkan kekurangan RAM percuma dengan bilangan sambungan yang mengagumkan.

Versi yang diubah suai menyerlahkan bilangan benang tetap (kolam benang), dengan itu menghalang sistem daripada membatalkan pelaksanaan, tetapi pada masa yang sama memperkenalkan masalah baharu: jika kumpulan benang pada masa ini disekat oleh operasi baca lama, maka soket lain yang sudah dapat menerima data tidak akan dapat berbuat demikian.

Pendekatan kedua menggunakan sistem pemberitahuan acara (pemilih sistem) yang disediakan oleh OS. Artikel ini membincangkan jenis pemilih sistem yang paling biasa, berdasarkan makluman (peristiwa, pemberitahuan) tentang kesediaan untuk operasi I/O, bukannya pada pemberitahuan tentang penyiapan mereka. Contoh ringkas penggunaannya boleh diwakili oleh gambarajah blok berikut:

Reaktor I/O kosong-C berciri penuh

Perbezaan antara pendekatan ini adalah seperti berikut:

  • Menyekat operasi I/O menangguhkan aliran pengguna sehinggasehingga OS betul defragmen masuk paket IP kepada aliran bait (TCP, menerima data) atau tidak akan ada ruang yang mencukupi dalam penimbal tulis dalaman untuk penghantaran berikutnya melalui NIC (menghantar data).
  • Pemilih sistem lebih masa memberitahu program bahawa OS sudah paket IP defragmented (TCP, penerimaan data) atau ruang yang mencukupi dalam penimbal tulis dalaman sudah tersedia (menghantar data).

Kesimpulannya, menempah benang OS untuk setiap I/O adalah satu pembaziran kuasa pengkomputeran, kerana pada hakikatnya, benang tidak melakukan kerja yang berguna (di sinilah istilah itu berasal "gangguan perisian"). Pemilih sistem menyelesaikan masalah ini, membenarkan program pengguna menggunakan sumber CPU dengan lebih menjimatkan.

Model reaktor I/O

Reaktor I/O bertindak sebagai lapisan antara pemilih sistem dan kod pengguna. Prinsip operasinya diterangkan oleh gambarajah blok berikut:

Reaktor I/O kosong-C berciri penuh

  • Izinkan saya mengingatkan anda bahawa peristiwa ialah pemberitahuan bahawa soket tertentu dapat melakukan operasi I/O yang tidak menyekat.
  • Pengendali peristiwa ialah fungsi yang dipanggil oleh reaktor I/O apabila peristiwa diterima, yang kemudiannya melakukan operasi I/O tanpa menyekat.

Adalah penting untuk ambil perhatian bahawa reaktor I/O mengikut takrifan berbenang tunggal, tetapi tiada apa-apa yang menghalang konsep daripada digunakan dalam persekitaran berbilang benang pada nisbah 1 benang: 1 reaktor, dengan itu mengitar semula semua teras CPU.

Реализация

Kami akan meletakkan antara muka awam dalam fail reactor.h, dan pelaksanaan - dalam reactor.c. reactor.h akan terdiri daripada pengumuman berikut:

Tunjukkan pengisytiharan dalam reaktor.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);

Struktur reaktor I/O terdiri daripada deskriptor fail pemilih epoll и jadual hash GHashTable, yang memetakan setiap soket ke CallbackData (struktur pengendali acara dan hujah pengguna untuknya).

Tunjukkan Reaktor dan CallbackData

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

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

Sila ambil perhatian bahawa kami telah mendayakan keupayaan untuk mengendalikan jenis tidak lengkap mengikut indeks. DALAM reactor.h kami mengisytiharkan struktur reactor, dan dalam reactor.c kami mentakrifkannya, dengan itu menghalang pengguna daripada mengubah medannya secara eksplisit. Ini adalah salah satu corak menyembunyikan data, yang secara ringkas sesuai dengan semantik C.

Fungsi reactor_register, reactor_deregister и reactor_reregister kemas kini senarai soket minat dan pengendali acara yang sepadan dalam pemilih sistem dan jadual cincang.

Tunjukkan fungsi pendaftaran

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

Selepas reaktor I/O telah memintas peristiwa dengan deskriptor fd, ia memanggil pengendali acara yang sepadan, yang dilaluinya fd, sedikit topeng peristiwa yang dijana dan penunjuk pengguna kepada void.

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

Untuk meringkaskan, rangkaian panggilan fungsi dalam kod pengguna akan mengambil bentuk berikut:

Reaktor I/O kosong-C berciri penuh

Pelayan berulir tunggal

Untuk menguji reaktor I/O di bawah beban tinggi, kami akan menulis pelayan web HTTP mudah yang bertindak balas kepada sebarang permintaan dengan imej.

Rujukan pantas kepada protokol HTTP

HTTP - ini adalah protokol peringkat permohonan, digunakan terutamanya untuk interaksi pelayan-pelayar.

HTTP boleh digunakan dengan mudah pengangkutan protokol TCP, menghantar dan menerima mesej dalam format yang ditentukan spesifikasi.

Format Permintaan

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

  • CRLF ialah urutan dua aksara: r и n, memisahkan baris pertama permintaan, pengepala dan data.
  • <КОМАНДА> - satu daripada CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Penyemak imbas akan menghantar arahan kepada pelayan kami GET, yang bermaksud "Hantar saya kandungan fail."
  • <URI> - pengecam sumber seragam. Sebagai contoh, jika URI = /index.html, kemudian pelanggan meminta halaman utama tapak.
  • <ВЕРСИЯ HTTP> — versi protokol HTTP dalam format HTTP/X.Y. Versi yang paling biasa digunakan hari ini ialah HTTP/1.1.
  • <ЗАГОЛОВОК N> ialah pasangan nilai kunci dalam format <КЛЮЧ>: <ЗНАЧЕНИЕ>, dihantar ke pelayan untuk analisis lanjut.
  • <ДАННЫЕ> — data yang diperlukan oleh pelayan untuk melaksanakan operasi. Selalunya ia mudah JSON atau mana-mana format lain.

Format Respons

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

  • <КОД СТАТУСА> ialah nombor yang mewakili hasil operasi. Pelayan kami akan sentiasa mengembalikan status 200 (operasi yang berjaya).
  • <ОПИСАНИЕ СТАТУСА> — perwakilan rentetan kod status. Untuk kod status 200 ini adalah OK.
  • <ЗАГОЛОВОК N> — pengepala format yang sama seperti dalam permintaan. Kami akan mengembalikan tajuk Content-Length (saiz fail) dan Content-Type: text/html (kembali jenis data).
  • <ДАННЫЕ> — data yang diminta oleh pengguna. Dalam kes kami, ini ialah laluan ke imej dalam HTML.

fail http_server.c (pelayan berulir tunggal) termasuk fail common.h, yang mengandungi prototaip fungsi berikut:

Tunjukkan prototaip fungsi yang sama.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);

Makro berfungsi juga diterangkan SAFE_CALL() dan fungsi ditakrifkan fail(). Makro membandingkan nilai ungkapan dengan ralat, dan jika keadaan itu benar, memanggil fungsi tersebut fail():

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

Fungsi fail() mencetak argumen yang diluluskan ke terminal (seperti printf()) dan menamatkan program dengan kod 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);
}

Fungsi new_server() mengembalikan deskriptor fail soket "pelayan" yang dibuat oleh panggilan sistem socket(), bind() и listen() dan mampu menerima sambungan masuk dalam mod tidak menyekat.

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

  • Ambil perhatian bahawa soket pada mulanya dicipta dalam mod tidak menyekat menggunakan bendera SOCK_NONBLOCKsupaya dalam fungsi on_accept() (baca lebih lanjut) panggilan sistem accept() tidak menghentikan pelaksanaan benang.
  • Jika reuse_port sama dengan true, maka fungsi ini akan mengkonfigurasi soket dengan pilihan SO_REUSEPORT melalui setsockopt()untuk menggunakan port yang sama dalam persekitaran berbilang benang (lihat bahagian "Pelayan berbilang benang").

Pengurus acara on_accept() dipanggil selepas OS menjana acara EPOLLIN, dalam kes ini bermakna sambungan baharu boleh diterima. on_accept() menerima sambungan baharu, menukarnya kepada mod tidak menyekat dan mendaftar dengan pengendali acara on_recv() dalam reaktor I/O.

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

Pengurus acara on_recv() dipanggil selepas OS menjana acara EPOLLIN, dalam kes ini bermakna sambungan didaftarkan on_accept(), sedia menerima data.

on_recv() membaca data daripada sambungan sehingga permintaan HTTP diterima sepenuhnya, kemudian ia mendaftarkan pengendali on_send() untuk menghantar respons HTTP. Jika pelanggan memutuskan sambungan, soket dibatalkan pendaftaran dan ditutup menggunakan close().

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

Pengurus acara on_send() dipanggil selepas OS menjana acara EPOLLOUT, bermakna sambungan itu didaftarkan on_recv(), sedia untuk menghantar data. Fungsi ini menghantar respons HTTP yang mengandungi HTML dengan imej kepada klien dan kemudian menukar pengendali acara kembali kepada on_recv().

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

Dan akhirnya, dalam fail http_server.c, dalam fungsi main() kami mencipta reaktor I/O menggunakan reactor_new(), cipta soket pelayan dan daftarkannya, mulakan reaktor menggunakan reactor_run() tepat satu minit, dan kemudian kami mengeluarkan sumber dan keluar dari program.

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

Mari periksa sama ada semuanya berfungsi seperti yang diharapkan. Menyusun (chmod a+x compile.sh && ./compile.sh dalam akar projek) dan lancarkan pelayan yang ditulis sendiri, buka http://127.0.0.1:18470 dalam penyemak imbas dan lihat apa yang kami jangkakan:

Reaktor I/O kosong-C berciri penuh

Pengukuran prestasi

Tunjukkan spesifikasi kereta saya

$ 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

Mari kita ukur prestasi pelayan satu benang. Mari buka dua terminal: dalam satu kita akan jalankan ./http_server, dalam berbeza - celaka. Selepas seminit, statistik berikut akan dipaparkan dalam terminal kedua:

$ 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

Pelayan satu benang kami dapat memproses lebih 11 juta permintaan seminit yang berasal daripada 100 sambungan. Bukan hasil yang buruk, tetapi bolehkah ia diperbaiki?

Pelayan berbilang benang

Seperti yang dinyatakan di atas, reaktor I/O boleh dibuat dalam benang berasingan, dengan itu menggunakan semua teras CPU. Mari kita praktikkan pendekatan ini:

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

Sekarang setiap utas memiliki miliknya sendiri reaktor:

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

Sila ambil perhatian bahawa hujah fungsi new_server() peguam bela true. Ini bermakna kami memberikan pilihan kepada soket pelayan SO_REUSEPORTuntuk menggunakannya dalam persekitaran berbilang benang. Anda boleh membaca butiran lanjut di sini.

Larian kedua

Sekarang mari kita ukur prestasi pelayan berbilang benang:

$ 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

Bilangan permintaan yang diproses dalam 1 minit meningkat sebanyak ~3.28 kali! Tetapi kami hanya kurang ~XNUMX juta daripada nombor pusingan, jadi mari kita cuba membetulkannya.

Mula-mula mari kita lihat statistik yang dihasilkan sempurna:

$ 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

Menggunakan CPU Affinity, kompilasi dengan -march=native, PGO, peningkatan dalam bilangan hits cache, meningkat MAX_EVENTS dan gunakan EPOLLET tidak memberikan peningkatan yang ketara dalam prestasi. Tetapi apa yang berlaku jika anda meningkatkan bilangan sambungan serentak?

Statistik untuk 352 sambungan serentak:

$ 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

Hasil yang diingini diperolehi, dan dengannya graf menarik yang menunjukkan pergantungan bilangan permintaan yang diproses dalam 1 minit pada bilangan sambungan:

Reaktor I/O kosong-C berciri penuh

Kami melihat bahawa selepas beberapa ratus sambungan, bilangan permintaan yang diproses untuk kedua-dua pelayan menurun secara mendadak (dalam versi berbilang benang ini lebih ketara). Adakah ini berkaitan dengan pelaksanaan tindanan TCP/IP Linux? Jangan ragu untuk menulis andaian anda tentang tingkah laku graf ini dan pengoptimuman untuk pilihan berbilang benang dan satu benang dalam ulasan.

bagaimana tercatat dalam ulasan, ujian prestasi ini tidak menunjukkan kelakuan reaktor I/O di bawah beban sebenar, kerana hampir selalu pelayan berinteraksi dengan pangkalan data, mengeluarkan log, menggunakan kriptografi dengan TLS dan lain-lain, akibatnya beban menjadi tidak seragam (dinamik). Ujian bersama dengan komponen pihak ketiga akan dijalankan dalam artikel tentang proaktor I/O.

Kelemahan reaktor I/O

Anda perlu memahami bahawa reaktor I/O bukan tanpa kelemahannya, iaitu:

  • Menggunakan reaktor I/O dalam persekitaran berbilang benang agak lebih sukar, kerana anda perlu menguruskan aliran secara manual.
  • Amalan menunjukkan bahawa dalam kebanyakan kes beban adalah tidak seragam, yang boleh membawa kepada satu pembalakan benang manakala yang lain sibuk dengan kerja.
  • Jika satu pengendali acara menyekat urutan, pemilih sistem itu sendiri juga akan menyekat, yang boleh membawa kepada pepijat yang sukar ditemui.

Menyelesaikan masalah ini proaktor I/O, yang selalunya mempunyai penjadual yang mengagihkan beban secara sama rata kepada kumpulan benang, dan juga mempunyai API yang lebih mudah. Kami akan membincangkannya kemudian, dalam artikel saya yang lain.

Kesimpulan

Di sinilah perjalanan kami dari teori terus ke ekzos profiler telah berakhir.

Anda tidak sepatutnya memikirkan perkara ini, kerana terdapat banyak pendekatan lain yang sama menarik untuk menulis perisian rangkaian dengan tahap kemudahan dan kelajuan yang berbeza. Menarik, pada pendapat saya, pautan diberikan di bawah.

Sehingga masa depan!

Projek yang menarik

Apa lagi yang perlu saya baca?

Sumber: www.habr.com

Tambah komen