Reaktor I/O bare-C berfitur lengkap

Reaktor I/O bare-C berfitur lengkap

pengenalan

reaktor masukan/keluaran (ulir tunggal lingkaran peristiwa) adalah pola untuk menulis perangkat lunak beban tinggi, yang digunakan dalam banyak solusi populer:

Pada artikel ini, kita akan melihat seluk beluk reaktor I/O dan cara kerjanya, menulis implementasi dalam kurang dari 200 baris kode, dan membuat server HTTP sederhana memproses lebih dari 40 juta permintaan/menit.

kata pengantar

  • Artikel ini ditulis untuk membantu memahami fungsi reaktor I/O, dan oleh karena itu memahami risiko saat menggunakannya.
  • Pengetahuan tentang dasar-dasar diperlukan untuk memahami artikel. bahasa C dan beberapa pengalaman dalam pengembangan aplikasi jaringan.
  • Semua kode ditulis dalam bahasa C secara ketat sesuai dengan (hati-hati: PDF panjang) ke standar C11 untuk Linux dan tersedia di GitHub.

Mengapa melakukannya?

Dengan semakin populernya Internet, server web mulai perlu menangani sejumlah besar koneksi secara bersamaan, dan oleh karena itu ada dua pendekatan yang dicoba: memblokir I/O pada sejumlah besar thread OS dan I/O non-pemblokiran yang dikombinasikan dengan sistem pemberitahuan peristiwa, juga disebut "pemilih sistem" (epolling/antrian/IOCP/dll).

Pendekatan pertama melibatkan pembuatan thread OS baru untuk setiap koneksi masuk. Kerugiannya adalah skalabilitas yang buruk: sistem operasi harus mengimplementasikan banyak hal transisi konteks и panggilan sistem. Ini adalah operasi yang mahal dan dapat menyebabkan kurangnya RAM kosong dengan jumlah koneksi yang mengesankan.

Sorotan versi modifikasi jumlah thread yang tetap (kumpulan thread), sehingga mencegah sistem membatalkan eksekusi, tetapi pada saat yang sama menimbulkan masalah baru: jika kumpulan thread saat ini diblokir oleh operasi pembacaan yang lama, maka soket lain yang sudah dapat menerima data tidak akan dapat untuk lakukan itu.

Pendekatan kedua menggunakan sistem pemberitahuan acara (pemilih sistem) yang disediakan oleh OS. Artikel ini membahas jenis pemilih sistem yang paling umum, berdasarkan peringatan (peristiwa, pemberitahuan) tentang kesiapan untuk operasi I/O, bukan pada pemberitahuan tentang penyelesaiannya. Contoh sederhana penggunaannya dapat diwakili oleh diagram blok berikut:

Reaktor I/O bare-C berfitur lengkap

Perbedaan antara pendekatan-pendekatan ini adalah sebagai berikut:

  • Memblokir operasi I/O menskors aliran pengguna sampaisampai OS benar defragment masuk paket IP ke aliran byte (TCP, menerima data) atau tidak akan tersedia cukup ruang di buffer tulis internal untuk pengiriman selanjutnya melalui NIC (mengirimkan data).
  • Pemilih sistem lembur memberi tahu program bahwa OS sudah paket IP yang didefragmentasi (TCP, penerimaan data) atau ruang yang cukup di buffer tulis internal sudah tersedia (mengirim data).

Singkatnya, memesan thread OS untuk setiap I/O adalah pemborosan daya komputasi, karena pada kenyataannya, thread tersebut tidak melakukan pekerjaan yang berguna (dari sinilah istilah tersebut berasal. "interupsi perangkat lunak"). Pemilih sistem memecahkan masalah ini, memungkinkan program pengguna menggunakan sumber daya CPU dengan lebih hemat.

Model reaktor I/O

Reaktor I/O bertindak sebagai lapisan antara pemilih sistem dan kode pengguna. Prinsip pengoperasiannya dijelaskan oleh diagram blok berikut:

Reaktor I/O bare-C berfitur lengkap

  • Izinkan saya mengingatkan Anda bahwa suatu peristiwa adalah pemberitahuan bahwa soket tertentu mampu melakukan operasi I/O non-pemblokiran.
  • Pengendali peristiwa (event handler) adalah fungsi yang dipanggil oleh reaktor I/O ketika suatu peristiwa diterima, yang kemudian melakukan operasi I/O non-pemblokiran.

Penting untuk dicatat bahwa reaktor I/O menurut definisinya adalah reaktor berulir tunggal, namun tidak ada yang menghentikan konsep tersebut untuk digunakan dalam lingkungan multi-utas dengan rasio reaktor 1 utas: 1, sehingga mendaur ulang semua inti CPU.

Implementasi

Kami akan menempatkan antarmuka publik dalam sebuah file reactor.h, dan implementasi - masuk reactor.c. reactor.h akan terdiri dari pengumuman berikut:

Tampilkan deklarasi di 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 dari deskriptor file pemilih epolling и tabel hash GHashTable, yang memetakan setiap soket ke dalamnya CallbackData (struktur pengendali kejadian dan argumen pengguna untuk itu).

Tampilkan Reaktor dan CallbackData

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

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

Harap dicatat bahwa kami telah mengaktifkan kemampuan untuk menangani tipe tidak lengkap menurut indeks. DI DALAM reactor.h kami mendeklarasikan strukturnya reactor, dan dalam reactor.c kami mendefinisikannya, sehingga mencegah pengguna mengubah bidangnya secara eksplisit. Ini adalah salah satu polanya menyembunyikan data, yang secara ringkas cocok dengan semantik C.

Fungsi reactor_register, reactor_deregister и reactor_reregister perbarui daftar soket yang diminati dan event handler terkait di pemilih sistem dan tabel hash.

Tampilkan fungsi registrasi

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

Setelah reaktor I/O mencegat peristiwa dengan deskriptor fd, ia memanggil event handler terkait, yang diteruskannya fd, topeng kecil peristiwa yang dihasilkan dan penunjuk pengguna ke void.

Tampilkan fungsi reaktor_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;
}

Ringkasnya, rangkaian pemanggilan fungsi dalam kode pengguna akan mengambil bentuk berikut:

Reaktor I/O bare-C berfitur lengkap

Server berulir tunggal

Untuk menguji reaktor I/O pada beban tinggi, kami akan menulis server web HTTP sederhana yang merespons permintaan apa pun dengan gambar.

Referensi cepat ke protokol HTTP

HTTP - ini adalah protokolnya tingkat aplikasi, terutama digunakan untuk interaksi server-browser.

HTTP dapat dengan mudah digunakan mengangkut protokol TCP, mengirim dan menerima pesan dalam format yang ditentukan spesifikasi.

Format Permintaan

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

  • CRLF adalah urutan dua karakter: r и n, memisahkan baris pertama permintaan, header, dan data.
  • <КОМАНДА> - satu dari CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Browser akan mengirimkan perintah ke server kami GET, artinya "Kirimkan saya isi file tersebut."
  • <URI> - pengidentifikasi sumber daya yang seragam. Misalnya, jika URI = /index.html, lalu klien meminta halaman utama situs.
  • <ВЕРСИЯ HTTP> — versi protokol HTTP dalam format HTTP/X.Y. Versi yang paling umum digunakan saat ini adalah HTTP/1.1.
  • <ЗАГОЛОВОК N> adalah pasangan nilai kunci dalam format <КЛЮЧ>: <ЗНАЧЕНИЕ>, dikirim ke server untuk analisis lebih lanjut.
  • <ДАННЫЕ> — data yang dibutuhkan oleh server untuk melakukan operasi. Seringkali itu sederhana JSON atau format lainnya.

Format Respons

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

  • <КОД СТАТУСА> adalah angka yang mewakili hasil operasi. Server kami akan selalu mengembalikan status 200 (operasi berhasil).
  • <ОПИСАНИЕ СТАТУСА> — representasi string dari kode status. Untuk kode status 200 ini OK.
  • <ЗАГОЛОВОК N> — header dengan format yang sama seperti pada permintaan. Kami akan mengembalikan gelarnya Content-Length (ukuran file) dan Content-Type: text/html (mengembalikan tipe data).
  • <ДАННЫЕ> — data yang diminta oleh pengguna. Dalam kasus kami, ini adalah jalur menuju gambar masuk HTML.

berkas http_server.c (server berulir tunggal) termasuk file common.h, yang berisi prototipe fungsi berikut:

Tampilkan prototipe 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 fungsional juga dijelaskan SAFE_CALL() dan fungsinya ditentukan fail(). Makro membandingkan nilai ekspresi dengan kesalahan, dan jika kondisinya benar, memanggil fungsinya fail():

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

Fungsi fail() mencetak argumen yang diteruskan ke terminal (seperti printf()) dan mengakhiri program dengan kode 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 file dari soket "server" yang dibuat oleh panggilan sistem socket(), bind() и listen() dan mampu menerima koneksi masuk dalam mode non-pemblokiran.

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

  • Perhatikan bahwa soket awalnya dibuat dalam mode non-pemblokiran menggunakan flag SOCK_NONBLOCKsehingga dalam fungsinya on_accept() (baca lebih lanjut) panggilan sistem accept() tidak menghentikan eksekusi thread.
  • Jika reuse_port sama dengan true, maka fungsi ini akan mengkonfigurasi soket dengan opsi SO_REUSEPORT melalui setsockopt()untuk menggunakan port yang sama di lingkungan multi-thread (lihat bagian “Server multi-thread”).

Penangan Acara on_accept() dipanggil setelah OS menghasilkan suatu peristiwa EPOLLIN, dalam hal ini berarti koneksi baru dapat diterima. on_accept() menerima koneksi baru, mengalihkannya ke mode non-pemblokiran dan mendaftar dengan event handler on_recv() dalam reaktor I/O.

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

Penangan Acara on_recv() dipanggil setelah OS menghasilkan suatu peristiwa EPOLLIN, dalam hal ini berarti koneksi telah terdaftar on_accept(), siap menerima data.

on_recv() membaca data dari koneksi hingga permintaan HTTP diterima sepenuhnya, lalu mendaftarkan penangan on_send() untuk mengirim respons HTTP. Jika klien memutus koneksi, soket dibatalkan pendaftarannya dan ditutup menggunakan close().

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

Penangan Acara on_send() dipanggil setelah OS menghasilkan suatu peristiwa EPOLLOUT, artinya koneksi telah terdaftar on_recv(), siap mengirim data. Fungsi ini mengirimkan respon HTTP yang berisi HTML dengan gambar ke klien dan kemudian mengubah event handler kembali on_recv().

Tampilkan 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 terakhir, di file http_server.c, dalam fungsi main() kami membuat reaktor I/O menggunakan reactor_new(), buat soket server dan daftarkan, mulai reaktor menggunakan reactor_run() selama tepat satu menit, lalu kami melepaskan sumber daya dan keluar dari program.

Tampilkan 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 kita periksa apakah semuanya berfungsi seperti yang diharapkan. Kompilasi (chmod a+x compile.sh && ./compile.sh di root proyek) dan luncurkan server yang ditulis sendiri, buka http://127.0.0.1:18470 di browser dan lihat apa yang kami harapkan:

Reaktor I/O bare-C berfitur lengkap

Pengukuran kinerja

Tunjukkan spesifikasi mobil 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 mengukur kinerja server single-threaded. Mari kita buka dua terminal: di satu terminal kita akan menjalankannya ./http_server, di tempat yang berbeda - kerja. Semenit kemudian, statistik berikut akan ditampilkan di 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

Server single-threaded kami mampu memproses lebih dari 11 juta permintaan per menit yang berasal dari 100 koneksi. Bukan hasil yang buruk, tapi bisakah diperbaiki?

Server multithread

Seperti disebutkan di atas, reaktor I/O dapat dibuat dalam thread terpisah, sehingga memanfaatkan semua inti CPU. Mari kita praktikkan pendekatan ini:

Tampilkan 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)

Harap dicatat bahwa argumen fungsi new_server() pendukung true. Ini berarti kami menetapkan opsi ke soket server SO_REUSEPORTuntuk menggunakannya dalam lingkungan multi-thread. Anda dapat membaca lebih detailnya di sini.

Putaran kedua

Sekarang mari kita ukur kinerja server multi-thread:

$ 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

Jumlah permintaan yang diproses dalam 1 menit meningkat ~3.28 kali lipat! Tapi kita hanya kekurangan ~XNUMX juta untuk mencapai angka bulatnya, jadi mari kita coba memperbaikinya.

Pertama mari kita lihat statistik yang dihasilkan Perf:

$ 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 Afinitas CPU, kompilasi dengan -march=native, PGO, peningkatan jumlah hit cache, meningkatkan MAX_EVENTS dan gunakan EPOLLET tidak memberikan peningkatan kinerja yang signifikan. Namun apa jadinya jika Anda menambah jumlah koneksi simultan?

Statistik untuk 352 koneksi simultan:

$ 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 diinginkan diperoleh, dan dengan itu grafik menarik yang menunjukkan ketergantungan jumlah permintaan yang diproses dalam 1 menit pada jumlah koneksi:

Reaktor I/O bare-C berfitur lengkap

Kami melihat bahwa setelah beberapa ratus koneksi, jumlah permintaan yang diproses untuk kedua server turun tajam (dalam versi multi-utas hal ini lebih terlihat). Apakah ini terkait dengan implementasi tumpukan TCP/IP Linux? Jangan ragu untuk menulis asumsi Anda tentang perilaku grafik ini dan pengoptimalan untuk opsi multi-thread dan single-thread di komentar.

Как dicatat di komentar, uji kinerja ini tidak menunjukkan perilaku reaktor I/O di bawah beban nyata, karena hampir selalu server berinteraksi dengan database, mengeluarkan log, menggunakan kriptografi dengan TLS dll, akibatnya beban menjadi tidak seragam (dinamis). Pengujian bersama dengan komponen pihak ketiga akan dilakukan di artikel tentang proaktor I/O.

Kekurangan reaktor I/O

Perlu Anda pahami bahwa reaktor I/O bukannya tanpa kekurangan, yaitu:

  • Menggunakan reaktor I/O dalam lingkungan multi-thread agak lebih sulit, karena Anda harus mengelola arus secara manual.
  • Praktek menunjukkan bahwa dalam banyak kasus, muatannya tidak seragam, yang dapat menyebabkan satu thread logging sementara thread lainnya sibuk dengan pekerjaan.
  • Jika salah satu pengendali peristiwa memblokir sebuah thread, maka pemilih sistem itu sendiri juga akan memblokir, yang dapat menyebabkan bug yang sulit ditemukan.

Memecahkan masalah ini proaktor I/O, yang sering kali memiliki penjadwal yang mendistribusikan beban secara merata ke kumpulan thread, dan juga memiliki API yang lebih nyaman. Kita akan membicarakannya nanti, di artikel saya yang lain.

Kesimpulan

Di sinilah perjalanan kita dari teori langsung ke knalpot profiler berakhir.

Anda tidak boleh memikirkan hal ini, karena ada banyak pendekatan lain yang sama menariknya dalam menulis perangkat lunak jaringan dengan tingkat kenyamanan dan kecepatan berbeda. Menarik, menurut saya, tautannya diberikan di bawah ini.

о овых еч!

Proyek yang menarik

Apa lagi yang harus saya baca?

Sumber: www.habr.com

Tambah komentar