Tam özellikli çıplak C I/O reaktörü

Tam özellikli çıplak C I/O reaktörü

Giriş

G/Ç reaktörü (tek dişli olay döngüsü), birçok popüler çözümde kullanılan, yüksek yüklü yazılım yazmaya yönelik bir kalıptır:

Bu makalede, bir I/O reaktörünün tüm detaylarına ve nasıl çalıştığına bakacağız, 200 satırdan daha az kodla bir uygulama yazacağız ve 40 milyon istek/dakikanın üzerinde basit bir HTTP sunucusu işlemi gerçekleştireceğiz.

Önsöz

  • Makale, G/Ç reaktörünün işleyişini anlamaya ve dolayısıyla onu kullanırken ortaya çıkabilecek riskleri anlamaya yardımcı olmak için yazılmıştır.
  • Makaleyi anlamak için temel bilgileri bilmek gerekir. C dili ve ağ uygulaması geliştirme konusunda biraz deneyim.
  • Tüm kodlar kesinlikle C dilinde ('ye göre yazılmıştır)dikkat: uzun PDF) C11 standardına göre Linux için ve şu adreste mevcuttur: GitHub.

Neden mi?

İnternetin popülaritesinin artmasıyla birlikte, web sunucuları aynı anda çok sayıda bağlantıyı yönetme ihtiyacı duymaya başladı ve bu nedenle iki yaklaşım denendi: çok sayıda işletim sistemi iş parçacığında G/Ç'nin engellenmesi ve "sistem seçici" olarak da adlandırılan bir olay bildirim sistemi (epol/kuyruk/IOCP/vesaire).

İlk yaklaşım, gelen her bağlantı için yeni bir işletim sistemi iş parçacığı oluşturmayı içeriyordu. Dezavantajı zayıf ölçeklenebilirliktir: işletim sisteminin birçok uygulama yapması gerekecektir. bağlam geçişleri и sistem çağrıları. Bunlar pahalı işlemlerdir ve etkileyici sayıda bağlantıya sahip boş RAM eksikliğine yol açabilir.

Değiştirilmiş sürüm öne çıkıyor sabit sayıda iş parçacığı (iş parçacığı havuzu), böylece sistemin yürütmeyi durdurması önlenir, ancak aynı zamanda yeni bir sorun ortaya çıkar: bir iş parçacığı havuzu şu anda uzun okuma işlemleri nedeniyle engellenmişse, o zaman zaten veri alabilen diğer soketler bu işlemi gerçekleştiremeyecektir. böyle yap.

İkinci yaklaşım şunları kullanır: olay bildirim sistemi (sistem seçici) işletim sistemi tarafından sağlanır. Bu makalede, G/Ç işlemlerine hazır olma durumu yerine uyarılara (olaylar, bildirimler) dayalı olarak en yaygın sistem seçici türü anlatılmaktadır. tamamlandığına dair bildirimler. Kullanımının basitleştirilmiş bir örneği aşağıdaki blok diyagramla gösterilebilir:

Tam özellikli çıplak C I/O reaktörü

Bu yaklaşımlar arasındaki fark aşağıdaki gibidir:

  • G/Ç işlemlerini engelleme askıya almak kullanıcı akışı a kadarişletim sistemi düzgün bir şekilde kuruluncaya kadar birleştirmeler gelen IP paketleri bayt akışına (TCP, veri alıyor) veya dahili yazma arabelleklerinde sonraki gönderimler için yeterli alan olmayacak NIC (veri gönderiliyor).
  • Sistem seçici bir süre sonra programa işletim sisteminin bildirildiğini bildirir zaten birleştirilmiş IP paketleri (TCP, veri alımı) veya dahili yazma arabelleklerinde yeterli alan zaten mevcut (veri gönderiliyor).

Özetlemek gerekirse, her bir G/Ç için bir işletim sistemi iş parçacığı ayırmak, bilgi işlem gücünün israfıdır, çünkü gerçekte iş parçacıkları yararlı işler yapmaz (bu nedenle terim "yazılım kesintisi"). Sistem seçici bu sorunu çözerek kullanıcı programının CPU kaynaklarını çok daha ekonomik kullanmasına olanak tanır.

G/Ç reaktör modeli

G/Ç reaktörü, sistem seçici ile kullanıcı kodu arasında bir katman görevi görür. Çalışma prensibi aşağıdaki blok diyagramda açıklanmaktadır:

Tam özellikli çıplak C I/O reaktörü

  • Bir olayın, belirli bir soketin engellenmeyen bir G/Ç işlemi gerçekleştirebildiğine dair bir bildirim olduğunu hatırlatmama izin verin.
  • Olay işleyici, bir olay alındığında G/Ç reaktörü tarafından çağrılan ve daha sonra engellemesiz bir G/Ç işlemi gerçekleştiren bir işlevdir.

G/Ç reaktörünün tanım gereği tek iş parçacıklı olduğuna dikkat etmek önemlidir, ancak konseptin çok iş parçacıklı bir ortamda 1 iş parçacığı: 1 reaktör oranında kullanılmasını, dolayısıyla tüm CPU çekirdeklerinin geri dönüştürülmesini engelleyen hiçbir şey yoktur.

uygulama

Genel arayüzü bir dosyaya yerleştireceğiz reactor.hve uygulama - içinde reactor.c. reactor.h aşağıdaki duyurulardan oluşacaktır:

Bildirimleri reaktör.h'de göster

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

G/Ç reaktör yapısı aşağıdakilerden oluşur: dosya tanımlayıcı seçici epol и karma tabloları GHashTableher bir soketi eşleyen CallbackData (bir olay işleyicisinin yapısı ve bunun için bir kullanıcı argümanı).

Reaktör ve Geri Arama Verilerini Göster

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

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

Lütfen işleme yeteneğini etkinleştirdiğimizi unutmayın. tamamlanmamış tür indekse göre. İÇİNDE reactor.h yapıyı ilan ediyoruz reactorVe içinde reactor.c bunu tanımlarız, böylece kullanıcının alanlarını açıkça değiştirmesini engelleriz. Bu modellerden biri verileri gizlemeC anlambilimine kısa ve öz bir şekilde uyan.

fonksiyonlar reactor_register, reactor_deregister и reactor_reregister sistem seçicide ve karma tablosunda ilgilenilen yuvaların ve karşılık gelen olay işleyicilerinin listesini güncelleyin.

Kayıt işlevlerini göster

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

G/Ç reaktörü olayı tanımlayıcıyla yakaladıktan sonra fd, iletildiği ilgili olay işleyicisini çağırır fd, bit maskesi oluşturulan olaylar ve bir kullanıcı işaretçisi void.

reaktör_run() işlevini göster

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

Özetlemek gerekirse, kullanıcı kodundaki işlev çağrıları zinciri aşağıdaki formu alacaktır:

Tam özellikli çıplak C I/O reaktörü

Tek iş parçacıklı sunucu

I/O reaktörünü yüksek yük altında test etmek için herhangi bir isteğe bir görüntüyle yanıt veren basit bir HTTP web sunucusu yazacağız.

HTTP protokolüne hızlı bir referans

HTTP - protokol bu uygulama seviyesi, öncelikle sunucu-tarayıcı etkileşimi için kullanılır.

HTTP kolaylıkla kullanılabilir Ulaşım protokol TCP, belirtilen formatta mesaj gönderip almak Şartname.

Talep Formatı

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

  • CRLF iki karakterden oluşan bir dizidir: r и nisteğin ilk satırını, başlıkları ve verileri ayırarak.
  • <КОМАНДА> - biri CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Tarayıcı sunucumuza bir komut gönderecek GET, "Dosyanın içeriğini bana gönder" anlamına gelir.
  • <URI> - Tekdüzen Kaynak Tanımlayıcı. Örneğin, eğer URI = /index.html, ardından müşteri sitenin ana sayfasını ister.
  • <ВЕРСИЯ HTTP> — HTTP protokolünün formattaki versiyonu HTTP/X.Y. Günümüzde en sık kullanılan versiyon HTTP/1.1.
  • <ЗАГОЛОВОК N> biçiminde bir anahtar/değer çiftidir <КЛЮЧ>: <ЗНАЧЕНИЕ>, daha fazla analiz için sunucuya gönderildi.
  • <ДАННЫЕ> — işlemi gerçekleştirmek için sunucunun ihtiyaç duyduğu veriler. Çoğu zaman basittir JSON veya başka herhangi bir format.

Yanıt Formatı

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

  • <КОД СТАТУСА> işlemin sonucunu temsil eden bir sayıdır. Sunucumuz her zaman durum 200'ü (başarılı işlem) döndürecektir.
  • <ОПИСАНИЕ СТАТУСА> — durum kodunun dize gösterimi. Durum kodu 200 için bu OK.
  • <ЗАГОЛОВОК N> — istektekiyle aynı formatta başlık. Başlıkları iade edeceğiz Content-Length (dosya boyutu) ve Content-Type: text/html (veri türünü döndürür).
  • <ДАННЫЕ> — kullanıcı tarafından talep edilen veriler. Bizim durumumuzda bu, görüntünün yoludur. HTML.

Dosya http_server.c (tek iş parçacıklı sunucu) dosya içerir common.hAşağıdaki işlev prototiplerini içeren:

İşlev prototiplerini ortak olarak göster.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);

İşlevsel makro da açıklanmıştır SAFE_CALL() ve fonksiyon tanımlandı fail(). Makro, ifadenin değerini hatayla karşılaştırır ve koşul doğruysa işlevi çağırır. fail():

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

Fonksiyon fail() iletilen argümanları terminale yazdırır (gibi printf()) ve programı kodla sonlandırır 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);
}

Fonksiyon new_server() sistem çağrıları tarafından oluşturulan "sunucu" soketinin dosya tanımlayıcısını döndürür socket(), bind() и listen() ve gelen bağlantıları engellemeyen bir modda kabul etme yeteneğine sahiptir.

new_server() işlevini göster

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

  • Soketin başlangıçta engellemesiz modda bayrak kullanılarak oluşturulduğunu unutmayın. SOCK_NONBLOCKböylece fonksiyonda on_accept() (devamını oku) sistem çağrısı accept() iş parçacığının yürütülmesini durdurmadı.
  • Eğer reuse_port olduğunu true, o zaman bu işlev soketi seçenekle yapılandıracaktır. SO_REUSEPORT sayesinde setsockopt()aynı bağlantı noktasını çok iş parçacıklı bir ortamda kullanmak için ("Çok iş parçacıklı sunucu" bölümüne bakın).

Olay işleyicisi on_accept() işletim sistemi bir olay oluşturduktan sonra çağrılır EPOLLIN, bu durumda yeni bağlantının kabul edilebileceği anlamına gelir. on_accept() yeni bir bağlantıyı kabul eder, engellemesiz moda geçirir ve bir olay işleyicisine kaydolur on_recv() bir G/Ç reaktöründe.

on_accept() işlevini göster

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

Olay işleyicisi on_recv() işletim sistemi bir olay oluşturduktan sonra çağrılır EPOLLIN, bu durumda bağlantının kayıtlı olduğu anlamına gelir on_accept(), veri almaya hazır.

on_recv() HTTP isteği tamamen alınana kadar bağlantıdaki verileri okur, ardından bir işleyiciyi kaydeder on_send() Bir HTTP yanıtı göndermek için. İstemci bağlantıyı keserse, soketin kaydı silinir ve kullanılarak kapatılır. close().

on_recv() fonksiyonunu göster

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

Olay işleyicisi on_send() işletim sistemi bir olay oluşturduktan sonra çağrılır EPOLLOUT, bağlantının kayıtlı olduğu anlamına gelir on_recv(), veri göndermeye hazır. Bu işlev, istemciye bir görüntü içeren HTML içeren bir HTTP yanıtı gönderir ve ardından olay işleyicisini tekrar şu şekilde değiştirir: on_recv().

on_send() işlevini göster

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

Ve son olarak dosyada http_server.c, işlevde main() kullanarak bir G/Ç reaktörü oluşturuyoruz reactor_new(), bir sunucu soketi oluşturun ve kaydedin, kullanarak reaktörü başlatın reactor_run() tam olarak bir dakika, sonra kaynakları serbest bırakıp programdan çıkıyoruz.

http_server.c'yi göster

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

Her şeyin beklendiği gibi çalışıp çalışmadığını kontrol edelim. Derleniyor (chmod a+x compile.sh && ./compile.sh proje kökünde) ve kendi kendine yazılan sunucuyu başlatın, açın http://127.0.0.1:18470 tarayıcıda ne beklediğimizi görün:

Tam özellikli çıplak C I/O reaktörü

Performans ölçümü

Arabamın özelliklerini göster

$ 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

Tek iş parçacıklı bir sunucunun performansını ölçelim. İki terminal açalım: birinde koşacağız ./http_server, farklı bir şekilde - . Bir dakika sonra ikinci terminalde aşağıdaki istatistikler görüntülenecektir:

$ 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

Tek iş parçacıklı sunucumuz, 11 bağlantıdan kaynaklanan, dakikada 100 milyondan fazla isteği işleyebildi. Kötü bir sonuç değil ama geliştirilebilir mi?

Çok iş parçacıklı sunucu

Yukarıda bahsedildiği gibi, G/Ç reaktörü ayrı iş parçacıklarında oluşturulabilir, böylece tüm CPU çekirdekleri kullanılabilir. Bu yaklaşımı uygulamaya koyalım:

http_server_multithreaded.c'yi göster

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

Artık her konu kendi sahibi reaktör:

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

Lütfen fonksiyon argümanının new_server() eylemler true. Bu, seçeneği sunucu soketine atadığımız anlamına gelir SO_REUSEPORTçok iş parçacıklı bir ortamda kullanmak için. Daha fazla ayrıntı okuyabilirsiniz burada.

İkinci çalıştırma

Şimdi çok iş parçacıklı bir sunucunun performansını ölçelim:

$ 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 dakikada işlenen taleplerin sayısı ~3.28 kat arttı! Ancak tur sayısından yalnızca ~XNUMX milyon eksiğimiz vardı, o yüzden bunu düzeltmeye çalışalım.

Öncelikle oluşturulan istatistiklere bakalım 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

CPU Benzeşimini Kullanma, ile derleme -march=native, PGO, isabet sayısında artış nakit, arttırmak MAX_EVENTS ve kullan EPOLLET performansta ciddi bir artış sağlamadı. Peki eşzamanlı bağlantıların sayısını artırırsanız ne olur?

352 eşzamanlı bağlantının istatistikleri:

$ 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

İstenilen sonuç elde edildi ve bununla birlikte 1 dakika içinde işlenen talep sayısının bağlantı sayısına bağımlılığını gösteren ilginç bir grafik oluştu:

Tam özellikli çıplak C I/O reaktörü

Birkaç yüz bağlantıdan sonra, her iki sunucu için de işlenen istek sayısının keskin bir şekilde düştüğünü görüyoruz (çok iş parçacıklı sürümde bu daha belirgindir). Bu Linux TCP/IP yığın uygulamasıyla mı ilgili? Grafiğin bu davranışı ve çok iş parçacıklı ve tek iş parçacıklı seçeneklere yönelik optimizasyonlar hakkındaki varsayımlarınızı yorumlara yazmaktan çekinmeyin.

Gibi ünlü Yorumlarda, bu performans testi, G/Ç reaktörünün gerçek yükler altındaki davranışını göstermez çünkü neredeyse her zaman sunucu veritabanıyla etkileşime girer, günlükleri çıkarır ve kriptografiyi kullanır. TLS vb. bunun sonucunda yük düzensiz (dinamik) hale gelir. I/O proactor hakkındaki makalede üçüncü taraf bileşenlerle birlikte testler gerçekleştirilecektir.

G/Ç reaktörünün dezavantajları

G/Ç reaktörünün dezavantajlarının olduğunu anlamalısınız:

  • Çok iş parçacıklı bir ortamda bir G/Ç reaktörünün kullanılması biraz daha zordur çünkü akışları manuel olarak yönetmeniz gerekecektir.
  • Uygulama çoğu durumda yükün tek biçimli olmadığını göstermektedir; bu da bir iş parçacığının günlüğe kaydedilmesine neden olurken diğerinin işle meşgul olmasına neden olabilir.
  • Bir olay işleyicisi bir iş parçacığını engellerse, sistem seçicinin kendisi de engeller ve bu da bulunması zor hatalara yol açabilir.

Bu sorunları çözer G/Ç proaktörüGenellikle yükü bir iş parçacığı havuzuna eşit olarak dağıtan bir zamanlayıcıya ve ayrıca daha kullanışlı bir API'ye sahiptir. Bunu daha sonra diğer yazımda konuşacağız.

Sonuç

Teoriden doğrudan profil oluşturucu egzozuna olan yolculuğumuzun sona erdiği yer burasıdır.

Bunun üzerinde durmamalısınız çünkü ağ yazılımı yazmaya yönelik farklı düzeyde kolaylık ve hıza sahip, eşit derecede ilginç başka yaklaşımlar da vardır. Bana göre ilginç olan bağlantılar aşağıda verilmiştir.

Gelecek sefere kadar!

İlginç projeler

Başka ne okumalıyım?

Kaynak: habr.com

Yorum ekle