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:
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:
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 jadvallariGHashTable, bu har bir rozetkaga mos keladi CallbackData (hodisalar ishlovchisining tuzilishi va uning uchun foydalanuvchi argumenti).
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.
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:
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.
<КОД СТАТУСА> 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:
vazifa new_server() tizim chaqiruvlari orqali yaratilgan "server" soketining fayl deskriptorini qaytaradi socket(), bind() и listen() va bloklanmaydigan rejimda kiruvchi ulanishlarni qabul qila oladi.
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.
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().
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.
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:
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:
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:
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:
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:
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?
Istalgan natija olindi va u bilan 1 daqiqada qayta ishlangan so'rovlar soni ulanishlar soniga bog'liqligini ko'rsatadigan qiziqarli grafik:
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.