Di vê gotarê de, em ê li der û hundurên reaktorek I/O û çawa dixebite binihêrin, di kêmtirî 200 rêzikên kodê de pêkanînek binivîsin, û pêvajoyek servera HTTP-ya hêsan a li ser 40 mîlyon daxwaz / hûrdem çêkin.
Pêşniyar
Gotar hate nivîsandin ku ji bo fêmkirina fonksiyona reaktora I/O-yê alîkar be, û ji ber vê yekê xetereyên dema karanîna wê fêm bikin.
Ji bo fêmkirina gotarê zanîna bingehîn hewce ye. ziman C û hin ezmûn di pêşkeftina serîlêdana torê de.
Hemî kod bi zimanê C bi hişkî li gorî (hişyarî: PDF dirêj) standarda C11 ji bo Linux û li ser heye GitHub.
Çima ev hewce ye?
Bi mezinbûna populerbûna Înternetê re, pêşkêşkerên malperê dest pê kirin ku hewce ne ku bi hevdemî hejmareke mezin ji girêdanan re mijûl bibin, û ji ber vê yekê du nêzîkatî hatin ceribandin: astengkirina I/O li ser hejmareke mezin ji mijarên OS-ê û ne-astengkirina I/O bi hev re. pergalek agahdarkirina bûyerê, ku jê re "hilbijêrê pergalê" jî tê gotin (epoll/kqueue/IOCP/ hwd.).
Nêzîkatiya yekem ji bo her pêwendiya gihîştî diafirîne mijarek OS-ya nû. Kêmasiya wê pîvandina qels e: pergala xebitandinê dê neçar be ku gelekan bicîh bîne veguherînên çarçoveyê и bangên pergalê. Ew operasyonên biha ne û dikarin bibin sedema kêmbûna RAM-a belaş bi hejmareke balkêş a girêdanan.
Guhertoya guherbar ronî dike hejmara sabît ji mijarên (hewza Mijarê), bi vî rengî rê nade ku pergalê înfazê rawestîne, lê di heman demê de pirsgirêkek nû destnîşan dike: heke hewzek tîrêjê niha ji hêla operasyonên xwendina dirêj ve were asteng kirin, wê hingê soketên din ên ku berê dikarin daneyan bistînin dê nikaribin wisa bike.
Rêbaza duyemîn bikar tîne pergala ragihandina bûyerê (hilbijêrê pergalê) ku ji hêla OS-ê ve hatî peyda kirin. Ev gotar li ser bingeha hişyariyên (bûyer, agahdarî) di derbarê amadebûna ji bo operasyonên I/O de, li şûna li ser bingeha herî gelemperî celebê hilbijêra pergalê nîqaş dike. notifications li ser temamkirina wan. Nimûneyek hêsan a karanîna wê dikare bi diyagrama blokê ya jêrîn were destnîşan kirin:
Cûdahiya van rêbazan wiha ye:
Astengkirina operasyonên I/O dardekirin herikîna bikarhêner taheta ku OS rast e defragments hatin pakêtên IP ji bo byte stream (TCP, wergirtina daneyan) an dê di tamponên nivîsandina hundurîn de cîhek têr tune be ji bo şandina paşê bi rêya NOTHING (daneyên şandin).
Hilbijêra pergalê bi derbasbûna demê bernameya ku OS agahdar dike êdî pakêtên IP-ê yên defragmented (TCP, wergirtina daneyê) an cîhê têr di tamponên nivîsandina navxweyî de êdî heye (dane şandin).
Bi kurtî, veqetandina têlek OS-ê ji bo her I/O windakirina hêza hesabkirinê ye, ji ber ku di rastiyê de, xêzan karê kêrhatî nakin (ji ber vê yekê term "navbera nermalavê"). Hilbijêra pergalê vê pirsgirêkê çareser dike, dihêle ku bernameya bikarhêner çavkaniyên CPU-ê pir aborîtir bikar bîne.
I/O modela reaktorê
Reaktora I/O wekî qatek di navbera hilbijêra pergalê û koda bikarhêner de tevdigere. Prensîba xebata wê ji hêla diyagrama bloka jêrîn ve tête diyar kirin:
Bihêle ez ji we re bi bîr bînim ku bûyerek agahdariyek e ku hin soketek karibe karek I/O ya ne-astengker bike.
Rêvekera bûyerê fonksiyonek e ku dema bûyerek tê wergirtin ji hêla reaktora I/O ve tê gotin, ku dûv re karek I/O ya ne-astengkirî pêk tîne.
Girîng e ku were zanîn ku reaktora I/O ji hêla pênaseyê ve yek-têlek e, lê tiştek rê li ber vê yekê nagire ku têgîn di hawîrdorek pir-mijarî de bi rêjeya 1 Mijar: 1 reaktor were bikar anîn, bi vî rengî hemî navikên CPU-ê ji nû ve vezîvirîne.
Реализация
Em ê pêwendiya gelemperî di pelê de cîh bikin reactor.h, û pêkanîn - di reactor.c. reactor.h dê ji van daxuyaniyên jêrîn pêk were:
Daxuyaniyên di reaktorê de nîşan bide.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);
Avahiya reaktora I/O ji pêk tê ravekerê pelê hilbijêr epoll и maseyên hashGHashTable, ku nexşeya her soketê li ser dike CallbackData (avahiya hilgirê bûyerê û argûkek bikarhêner ji bo wê).
Ji kerema xwe bala xwe bidin ku me şiyana hilanînê çalak kiriye tîpa netemam li gor index. LI reactor.h em avaniyê radigihînin reactor, û bi reactor.c em wê pênase dikin, bi vî rengî rê nadin ku bikarhêner bi eşkere zeviyên xwe biguhezîne. Ev yek ji nimûneyan e daneyan vedişêre, ku bi kurtî di nav semantîka C de cih digire.
Karkerên reactor_register, reactor_deregister и reactor_reregister Di hilbijêra pergalê û tabloya haş de navnîşa soketên berjewendî û rêvebirên bûyerê yên têkildar nûve bikin.
Piştî ku reaktora I/O bûyerê bi ravekerê vegirt fd, ew gazî birêvebirê bûyerê yê têkildar dike, ku jê re derbas dibe fd, bit mask bûyerên çêkirî û nîşanek bikarhênerek void.
Fonksiyona reactor_run() nîşan bide
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;
}
Bi kurtasî, zincîra bangên fonksiyonê di koda bikarhêner de dê forma jêrîn bigire:
Pêşkêşkara yekane
Ji bo ceribandina reaktora I/O di bin barek zêde de, em ê web serverek HTTP-ya hêsan binivîsin ku bi wêneyek bersivê dide her daxwazê.
Referansek bilez a protokola HTTP
HTTP - ev protokol e asta serîlêdanê, di serî de ji bo têkiliya server-gerokê tê bikar anîn.
HTTP dikare bi hêsanî were bikar anîn neqilkirin protokol TCP, şandin û wergirtina peyaman di forma diyarkirî de specification.
CRLF rêzeka du tîpan e: r и n, rêza yekem a daxwazê, sernav û daneyan ji hev vediqetîne.
<КОМАНДА> - yek ji CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Gerok dê fermanek ji servera me re bişîne GET, tê wateya "Naveroka pelê ji min re bişîne."
<URI> - nasnameya çavkaniya yekgirtî. Mînakî, heke URI = /index.html, paşê xerîdar rûpela sereke ya malperê daxwaz dike.
<ВЕРСИЯ HTTP> - guhertoya protokola HTTP-ê di formatê de HTTP/X.Y. Guhertoya ku îro herî zêde tê bikar anîn ev e HTTP/1.1.
<ЗАГОЛОВОК N> di formatê de cotek key-nirx e <КЛЮЧ>: <ЗНАЧЕНИЕ>, ji bo analîzkirina bêtir ji serverê re şandin.
<ДАННЫЕ> - Daneyên ku ji hêla serverê ve hewce dike ku operasyonê pêk bîne. Gelek caran ew hêsan e JSON an her formatek din.
<КОД СТАТУСА> hejmareke ku encama operasyonê temsîl dike. Pêşkêşkara me dê her gav statûya 200 vegerîne (operasyona serketî).
<ОПИСАНИЕ СТАТУСА> - temsîla rêzê ya koda statûyê. Ji bo koda statûyê 200 ev e OK.
<ЗАГОЛОВОК N> - sernivîsa heman forma ku di daxwazê de ye. Em ê sernavan vegerînin Content-Length (mezinahiya pelê) û Content-Type: text/html (cureyê daneya vegere).
<ДАННЫЕ> - Daneyên ku ji hêla bikarhêner ve hatî xwestin. Di rewşa me de, ev riya wêneyê tê de ye HTML.
file http_server.c (Pêşkêşkera yekane) pelê vedihewîne common.h, ku prototîpên fonksiyonê yên jêrîn dihewîne:
Prototîpên fonksiyonê yên hevpar nîşan bidin.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);
Makroya fonksiyonel jî tête diyar kirin SAFE_CALL() û fonksiyonê tête diyar kirin fail(). Makro nirxa îfadeyê bi xeletiyê re berhev dike, û heke şert rast be, fonksiyonê vedixwîne fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
function fail() argumanên derbasbûyî li termînalê çap dike (wek printf()) û bernameyê bi kodê diqedîne EXIT_FAILURE:
function new_server() ravekera pelê ya soketa "server" ku ji hêla bangên pergalê ve hatî çêkirin vedigerîne socket(), bind() и listen() û karibe girêdanên hatinê di moda ne-astengkirinê de qebûl bike.
Têbînî ku soket di destpêkê de di moda ne-astengkirinê de bi karanîna ala ve hatî afirandin SOCK_NONBLOCKda ku di fonksiyonê de on_accept() (bixwîne) banga pergalê accept() înfaza mijarê nesekinî.
ger reuse_port wekhev e true, wê hingê ev fonksiyon dê soketê bi vebijarkê mîheng bike SO_REUSEPORT bi rêya setsockopt()ji bo ku heman portê di hawîrdorek pir-mijarî de bikar bînin (binihêrin beşa "Pêşkêşkera pir-têl").
Event Handler on_accept() piştî ku OS bûyerek çêdike tê gotin EPOLLIN, di vê rewşê de tê wateya ku girêdana nû dikare were pejirandin. on_accept() pêwendiyek nû qebûl dike, wê diguhezîne moda ne-astengkirinê û bi rêvekerek bûyerê re qeyd dike on_recv() di reaktoreke I/O de.
Event Handler on_recv() piştî ku OS bûyerek çêdike tê gotin EPOLLIN, di vê rewşê de tê wateya ku girêdana qeydkirî ye on_accept(), ji bo wergirtina daneyan amade ye.
on_recv() Daneyên ji pêwendiyê dixwîne heya ku daxwaza HTTP bi tevahî neyê wergirtin, dûv re ew hilberek tomar dike on_send() ku bersivek HTTP bişîne. Ger xerîdar pêwendiyê bişkîne, soket tê rakirin û bi kar tê girtin close().
Fonksiyonê nîşan bide 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);
}
}
Event Handler on_send() piştî ku OS bûyerek çêdike tê gotin EPOLLOUT, tê wateya ku pêwendiya qeydkirî ye on_recv(), ji bo şandina daneyan amade ye. Vê fonksiyonê bersivek HTTP ya ku HTML-ê dihewîne bi wêneyek ji xerîdar re dişîne û dûv re rêvebirê bûyerê vedigere on_recv().
Û di dawiyê de, di pelê de http_server.c, di fonksiyonê de main() em bi karanîna reaktorek I/O ava dikin reactor_new(), soketek serverê biafirînin û wê tomar bikin, reaktorê bikar bînin dest pê bikin reactor_run() tam yek deqîqe, û dûv re em çavkaniyan berdidin û ji bernameyê derdikevin.
Werin em kontrol bikin ku her tişt wekî ku tê hêvî kirin dixebite. Berhevkirin (chmod a+x compile.sh && ./compile.sh di koka projeyê de) û servera xwe-nivîsandî dest pê bikin, vekin http://127.0.0.1:18470 di gerokê de û bibînin ka em çi hêvî dikin:
Werin em performansa serverek yek-têkilî bipîvin. Ka em du termînalan vekin: di yekê de em ê birevin ./http_server, bi awayekî cuda - wink. Piştî deqeyek, statîstîkên jêrîn dê di termînala duyemîn de bêne xuyang kirin:
Pêşkêşkara meya yek-têkilî karîbû di her hûrdemê de zêdetirî 11 mîlyon daxwazên ku ji 100 girêdanan derketine pêvajo bike. Ne encamek xirab e, lê gelo ew dikare çêtir bibe?
Pêşkêşkara pir-threaded
Wekî ku li jor hatî behs kirin, reaktora I/O dikare di mijarên cûda de were afirandin, bi vî rengî hemî navikên CPU bikar bînin. Werin em vê nêzîkatiyê bixin pratîkê:
Ji kerema xwe not bikin ku argumana fonksiyonê new_server() parêzvanan true. Ev tê vê wateyê ku em vebijarkê ji bo soketa serverê veqetînin SO_REUSEPORTji bo ku wê di hawîrdorek pir-mijarî de bikar bînin. Hûn dikarin bêtir agahdarî bixwînin vir.
Rêza duyemîn
Naha werin em performansa serverek pir-mijarî bipîvin:
Hejmara daxwazên ku di 1 hûrdemê de hatine kirin ~ 3.28 carî zêde bû! Lê em tenê ~ XNUMX mîlyon ji jimareya dorpêçê kêm bûn, ji ber vê yekê em hewl bidin ku wê rast bikin.
Pêşî em li statîstîkên ku hatine çêkirin binêrin lhevderketî:
Bikaranîna CPU Affinity, berhevkirin bi -march=native, PGO, zêdebûna hejmara lêdan cache, zêde kirin MAX_EVENTS û bikar bînin EPOLLET di performansê de zêdebûnek girîng neda. Lê eger hûn hejmara girêdanên hevdem zêde bikin çi dibe?
Encama xwestî hate bidestxistin, û bi wê re grafiyek balkêş ku girêdayîbûna hejmara daxwazên pêvajoyî di 1 hûrdemê de li ser hejmara girêdanan nîşan dide:
Em dibînin ku piştî çend sed girêdanan, jimara daxwazên pêvajoyî yên ji bo her du serveran bi tundî dadikeve (di guhertoya pir-mijarî de ev bêtir xuyang e). Ma ev bi pêkanîna stackê ya Linux TCP/IP ve girêdayî ye? Di şîroveyan de hûn gumanên xwe yên di derbarê vê tevgera grafîkê û xweşbîniyên ji bo vebijarkên pir-mijal û yek-têlan de binivîsin.
çawa diyar kirin di şîroveyan de, ev ceribandina performansê tevgera reaktora I/O di bin barkirinên rastîn de nîşan nade, ji ber ku hema hema her gav server bi databasê re têkildar dibe, têketin derdixe, krîptografiyê bi kar tîne. TLS hwd., wekî encamek ku bark ne-yekhev (dînamîk) dibe. Dê di gotara li ser proaktora I/O de bi hev re bi pêkhateyên sêyemîn re ceribandin werin kirin.
Dezawantajên reaktora I/O
Pêdivî ye ku hûn fêhm bikin ku reaktora I/O ne bê kêmasiyên wê ye, yanî:
Bikaranîna reaktorek I/O di hawîrdorek pir-mijarî de hinekî dijwartir e, ji ber hûn ê neçar bibin ku bi destan herikînan birêve bibin.
Pratîk destnîşan dike ku di pir rewşan de bar ne-yekhev e, ku dikare bibe sedema têketinek yek di dema ku yekî din bi kar re mijûl e.
Ger yek rêvebirê bûyerê mijarek bloke bike, hilbijêra pergalê bixwe jî dê asteng bike, ku dikare bibe sedema xeletiyên ku dijwar têne dîtin.
Van pirsgirêkan çareser dike I/O proactor, ya ku bi gelemperî xwedan nexşeyek e ku bi rengek wekhev barkirinê li hewzek têlan belav dike, û di heman demê de xwedan API-yek hêsantir e. Em ê li ser wê paşê, di gotara xwe ya din de biaxivin.
encamê
Li vir rêwîtiya me ya ji teoriyê rasterast berbi eksê profîlerê bi dawî bûye.
Pêdivî ye ku hûn li ser vê yekê nesekinin, ji ber ku ji bo nivîsandina nermalava torê ya bi astên cûda yên rehetî û bilez gelek nêzîkatiyên din ên wekhev balkêş hene. Balkêş e, bi dîtina min, girêdan li jêr têne dayîn.