Yn yr erthygl hon, byddwn yn edrych ar i mewn a thu allan adweithydd I / O a sut mae'n gweithio, yn ysgrifennu gweithrediad mewn llai na 200 llinell o god, ac yn gwneud proses gweinydd HTTP syml dros 40 miliwn o geisiadau / mun.
Rhagair
Ysgrifennwyd yr erthygl i helpu i ddeall gweithrediad yr adweithydd I/O, ac felly i ddeall y risgiau wrth ei ddefnyddio.
Mae angen gwybodaeth o'r pethau sylfaenol i ddeall yr erthygl. C iaith a pheth profiad o ddatblygu cymwysiadau rhwydwaith.
Mae'r holl god wedi'i ysgrifennu yn iaith C yn unol â (rhybudd: PDF hir) i safon C11 ar gyfer Linux ac ar gael ar GitHub.
Pam mae angen hyn?
Gyda phoblogrwydd cynyddol y Rhyngrwyd, dechreuodd gweinyddwyr gwe fod angen trin nifer fawr o gysylltiadau ar yr un pryd, ac felly rhoddwyd cynnig ar ddau ddull: blocio I/O ar nifer fawr o edafedd OS a pheidio â rhwystro I/O ar y cyd â system hysbysu digwyddiad, a elwir hefyd yn "ddewisydd system" (epol/kciw/IOCP/etc).
Roedd y dull cyntaf yn cynnwys creu edefyn OS newydd ar gyfer pob cysylltiad sy'n dod i mewn. Ei anfantais yw scalability gwael: bydd yn rhaid i'r system weithredu weithredu llawer trawsnewidiadau cyd-destun и galwadau system. Maent yn weithrediadau drud a gallant arwain at ddiffyg RAM am ddim gyda nifer drawiadol o gysylltiadau.
Mae'r fersiwn addasedig yn amlygu nifer sefydlog o edafedd (pwll edau), a thrwy hynny atal y system rhag erthylu gweithredu, ond ar yr un pryd yn cyflwyno problem newydd: os yw pwll edau yn cael ei rwystro ar hyn o bryd gan weithrediadau darllen hir, yna ni fydd socedi eraill sydd eisoes yn gallu derbyn data yn gallu gwneud hynny.
Mae'r ail ddull yn defnyddio system hysbysu digwyddiad (detholwr system) a ddarperir gan yr OS. Mae'r erthygl hon yn trafod y math mwyaf cyffredin o ddewiswr system, yn seiliedig ar rybuddion (digwyddiadau, hysbysiadau) ynghylch parodrwydd ar gyfer gweithrediadau I/O, yn hytrach nag ar hysbysiadau am eu cwblhau. Gellir cynrychioli enghraifft symlach o'i ddefnydd gan y diagram bloc canlynol:
Mae'r gwahaniaeth rhwng y dulliau hyn fel a ganlyn:
Rhwystro gweithrediadau I/O atal llif defnyddiwr nesnes bod yr OS yn iawn defragments yn dod i mewn Pecynnau IP i ffrwd beit (TCP, derbyn data) neu ni fydd digon o le ar gael yn y byfferau ysgrifennu mewnol ar gyfer anfon ymlaen wedyn NIC (anfon data).
Dewisydd system dros amser yn hysbysu'r rhaglen bod yr OS eisoes pecynnau IP wedi'u darnio (TCP, derbyn data) neu ddigon o le mewn byfferau ysgrifennu mewnol eisoes ar gael (anfon data).
I grynhoi, mae cadw edefyn OS ar gyfer pob I/O yn wastraff o bŵer cyfrifiadurol, oherwydd mewn gwirionedd, nid yw'r edafedd yn gwneud gwaith defnyddiol (a dyna pam y mae'r term "toriad meddalwedd"). Mae'r dewisydd system yn datrys y broblem hon, gan ganiatáu i'r rhaglen ddefnyddwyr ddefnyddio adnoddau CPU yn llawer mwy darbodus.
Model adweithydd I/O
Mae'r adweithydd I/O yn gweithredu fel haen rhwng y dewisydd system a'r cod defnyddiwr. Disgrifir egwyddor ei weithrediad gan y diagram bloc canlynol:
Gadewch imi eich atgoffa bod digwyddiad yn hysbysiad bod soced penodol yn gallu cyflawni gweithrediad I/O nad yw'n rhwystro.
Mae triniwr digwyddiad yn swyddogaeth a elwir gan yr adweithydd I/O pan dderbynnir digwyddiad, sydd wedyn yn cyflawni gweithrediad I/O nad yw'n rhwystro.
Mae'n bwysig nodi bod yr adweithydd I/O trwy ddiffiniad yn un edau, ond nid oes dim yn atal y cysyniad rhag cael ei ddefnyddio mewn amgylchedd aml-edau ar gymhareb o adweithydd 1 edau: 1, a thrwy hynny ailgylchu holl greiddiau CPU.
Gweithredu
Byddwn yn gosod y rhyngwyneb cyhoeddus mewn ffeil reactor.h, a gweithredu - yn reactor.c. reactor.h bydd yn cynnwys y cyhoeddiadau a ganlyn:
Dangos datganiadau yn reactor.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);
Mae strwythur yr adweithydd I/O yn cynnwys disgrifydd ffeil detholwr epol и byrddau stwnshGHashTable, sy'n mapio pob soced i CallbackData (strwythur trafodwr digwyddiad a dadl defnyddiwr ar ei gyfer).
Sylwch ein bod wedi galluogi'r gallu i drin math anghyflawn yn ôl y mynegai. YN reactor.h rydym yn datgan y strwythur reactor, ac yn reactor.c rydym yn ei ddiffinio, gan atal y defnyddiwr rhag newid ei feysydd yn benodol. Dyma un o'r patrymau cuddio data, sy'n cyd-fynd yn gryno â semanteg C.
Swyddogaethau reactor_register, reactor_deregister и reactor_reregister diweddaru'r rhestr o socedi o ddiddordeb a thrinwyr digwyddiadau cyfatebol yn y dewisydd system a thabl stwnsh.
Ar ôl i'r adweithydd I/O ryng-gipio'r digwyddiad gyda'r disgrifydd fd, mae'n galw'r triniwr digwyddiad cyfatebol, y mae'n mynd iddo fd, mwgwd did digwyddiadau a gynhyrchir a phwyntydd defnyddiwr i void.
Dangos swyddogaeth 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;
}
I grynhoi, bydd y gadwyn o alwadau swyddogaeth yn y cod defnyddiwr ar y ffurf ganlynol:
Gweinydd edau sengl
Er mwyn profi'r adweithydd I / O dan lwyth uchel, byddwn yn ysgrifennu gweinydd gwe HTTP syml sy'n ymateb i unrhyw gais gyda delwedd.
Cyfeiriad cyflym at brotocol HTTP
HTTP - dyma'r protocol lefel cais, a ddefnyddir yn bennaf ar gyfer rhyngweithio gweinydd-porwr.
Gellir defnyddio HTTP yn hawdd drosodd trafnidiaeth protocol TCP, anfon a derbyn negeseuon mewn fformat penodol manyleb.
CRLF yn ddilyniant o ddau gymeriad: r и n, gan wahanu llinell gyntaf y cais, penawdau a data.
<КОМАНДА> - un o CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Bydd y porwr yn anfon gorchymyn i'n gweinydd GET, sy'n golygu "Anfon cynnwys y ffeil ataf."
<URI> - dynodwr adnoddau unffurf. Er enghraifft, os URI = /index.html, yna mae'r cleient yn gofyn am brif dudalen y wefan.
<ВЕРСИЯ HTTP> — fersiwn o'r protocol HTTP yn y fformat HTTP/X.Y. Y fersiwn a ddefnyddir amlaf heddiw yw HTTP/1.1.
<ЗАГОЛОВОК N> yn bâr gwerth allweddol yn y fformat <КЛЮЧ>: <ЗНАЧЕНИЕ>, wedi'i anfon at y gweinydd i'w ddadansoddi ymhellach.
<ДАННЫЕ> - data sydd ei angen ar y gweinydd i gyflawni'r llawdriniaeth. Yn aml mae'n syml JSON neu unrhyw fformat arall.
<КОД СТАТУСА> yw rhif sy'n cynrychioli canlyniad y llawdriniaeth. Bydd ein gweinydd bob amser yn dychwelyd statws 200 (gweithrediad llwyddiannus).
<ОПИСАНИЕ СТАТУСА> — cynrychioliad llinynnol o'r cod statws. Ar gyfer cod statws 200 mae hyn OK.
<ЗАГОЛОВОК N> — pennawd o'r un fformat ag yn y cais. Byddwn yn dychwelyd y teitlau Content-Length (maint ffeil) a Content-Type: text/html (math o ddata dychwelyd).
<ДАННЫЕ> — data y gofynnodd y defnyddiwr amdano. Yn ein hachos ni, dyma'r llwybr i'r ddelwedd i mewn HTML.
file http_server.c (gweinydd edefyn sengl) yn cynnwys ffeil common.h, sy'n cynnwys y prototeipiau swyddogaeth canlynol:
Dangos prototeipiau ffwythiant yn gyffredin.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);
Disgrifir y macro swyddogaethol hefyd SAFE_CALL() ac mae'r swyddogaeth wedi'i diffinio fail(). Mae'r macro yn cymharu gwerth y mynegiant gyda'r gwall, ac os yw'r cyflwr yn wir, mae'n galw'r ffwythiant fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
Swyddogaeth fail() yn argraffu'r dadleuon a basiwyd i'r derfynell (fel printf()) ac yn terfynu'r rhaglen gyda'r cod EXIT_FAILURE:
Swyddogaeth new_server() yn dychwelyd disgrifydd ffeil y soced "gweinydd" a grëwyd gan alwadau system socket(), bind() и listen() ac yn gallu derbyn cysylltiadau sy'n dod i mewn mewn modd nad yw'n rhwystro.
Sylwch fod y soced yn cael ei greu i ddechrau yn y modd di-flocio gan ddefnyddio'r faner SOCK_NONBLOCKfel bod yn y swyddogaeth on_accept() (darllen mwy) galwad system accept() ni ataliodd y gweithrediad edefyn.
Os reuse_port yn hafal true, yna bydd y swyddogaeth hon yn ffurfweddu'r soced gyda'r opsiwn SO_REUSEPORT trwodd setsockopt()i ddefnyddio'r un porthladd mewn amgylchedd aml-threaded (gweler yr adran “Gweinydd aml-edau”).
Triniwr Digwyddiad on_accept() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLIN, yn yr achos hwn yn golygu y gellir derbyn y cysylltiad newydd. on_accept() yn derbyn cysylltiad newydd, yn ei newid i fodd di-flocio ac yn cofrestru gyda thriniwr digwyddiad on_recv() mewn adweithydd I/O.
Triniwr Digwyddiad on_recv() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLIN, yn yr achos hwn yn golygu bod y cysylltiad cofrestredig on_accept(), yn barod i dderbyn data.
on_recv() yn darllen data o'r cysylltiad nes bod y cais HTTP wedi'i dderbyn yn llwyr, yna mae'n cofrestru triniwr on_send() i anfon ymateb HTTP. Os bydd y cleient yn torri'r cysylltiad, caiff y soced ei ddadgofrestru a'i gau gan ddefnyddio close().
Dangos ffwythiant 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);
}
}
Triniwr Digwyddiad on_send() a elwir ar ôl i'r OS gynhyrchu digwyddiad EPOLLOUT, sy'n golygu bod y cysylltiad wedi'i gofrestru on_recv(), yn barod i anfon data. Mae'r swyddogaeth hon yn anfon ymateb HTTP sy'n cynnwys HTML gyda delwedd i'r cleient ac yna'n newid y triniwr digwyddiad yn ôl i on_recv().
Ac yn olaf, yn y ffeil http_server.c, mewn swyddogaeth main() rydym yn creu adweithydd I/O gan ddefnyddio reactor_new(), creu soced gweinydd a'i gofrestru, dechreuwch yr adweithydd gan ddefnyddio reactor_run() am funud yn union, ac yna rydym yn rhyddhau adnoddau ac yn gadael y rhaglen.
Gadewch i ni wirio bod popeth yn gweithio yn ôl y disgwyl. Wrthi'n llunio (chmod a+x compile.sh && ./compile.sh yng ngwraidd y prosiect) a lansio'r gweinydd hunan-ysgrifenedig, agor http://127.0.0.1:18470 yn y porwr a gweld beth oeddem yn ei ddisgwyl:
Gadewch i ni fesur perfformiad gweinydd un edau. Gadewch i ni agor dwy derfynell: mewn un byddwn yn rhedeg ./http_server, mewn gwahanol - wrk. Ar ôl munud, bydd yr ystadegau canlynol yn cael eu harddangos yn yr ail derfynell:
Roedd ein gweinydd un edau yn gallu prosesu dros 11 miliwn o geisiadau y funud yn deillio o 100 o gysylltiadau. Ddim yn ganlyniad gwael, ond a ellir ei wella?
Gweinydd aml-threaded
Fel y soniwyd uchod, gellir creu'r adweithydd I / O mewn edafedd ar wahân, a thrwy hynny ddefnyddio'r holl greiddiau CPU. Gadewch i ni roi'r dull hwn ar waith:
Sylwch fod y ddadl swyddogaeth new_server() eiriolwyr true. Mae hyn yn golygu ein bod yn aseinio'r opsiwn i soced y gweinydd SO_REUSEPORTi'w ddefnyddio mewn amgylchedd aml-edau. Gallwch ddarllen mwy o fanylion yma.
Ail rediad
Nawr gadewch i ni fesur perfformiad gweinydd aml-edau:
Cynyddodd nifer y ceisiadau a broseswyd mewn 1 munud gan ~3.28 gwaith! Ond dim ond ~XNUMX filiwn oedden ni'n brin o'r rhif crwn, felly gadewch i ni geisio trwsio hynny.
Yn gyntaf, gadewch i ni edrych ar yr ystadegau a gynhyrchwyd perff:
Defnyddio CPU Affinity, crynhoad gyda -march=native, PBL, cynnydd yn nifer yr ymweliadau celc, cynyddu MAX_EVENTS a defnydd EPOLLET ni roddodd gynnydd sylweddol mewn perfformiad. Ond beth sy'n digwydd os ydych chi'n cynyddu nifer y cysylltiadau cydamserol?
Cafwyd y canlyniad dymunol, a chydag ef graff diddorol yn dangos dibyniaeth nifer y ceisiadau wedi'u prosesu mewn 1 munud ar nifer y cysylltiadau:
Gwelwn, ar ôl cwpl o gannoedd o gysylltiadau, fod nifer y ceisiadau wedi'u prosesu ar gyfer y ddau weinydd yn gostwng yn sydyn (yn y fersiwn aml-edau mae hyn yn fwy amlwg). A yw hyn yn gysylltiedig â gweithredu stack Linux TCP/IP? Mae croeso i chi ysgrifennu eich rhagdybiaethau am yr ymddygiad hwn o'r graff ac optimeiddiadau ar gyfer opsiynau aml-edau ac un edau yn y sylwadau.
Fel nodwyd yn y sylwadau, nid yw'r prawf perfformiad hwn yn dangos ymddygiad yr adweithydd I/O o dan lwythi real, oherwydd bron bob amser mae'r gweinydd yn rhyngweithio â'r gronfa ddata, logiau allbynnau, yn defnyddio cryptograffeg gyda TLS ac ati, ac o ganlyniad mae'r llwyth yn dod yn anwisg (deinamig). Bydd profion ynghyd â chydrannau trydydd parti yn cael eu cynnal yn yr erthygl am yr proactor I/O.
Anfanteision adweithydd I/O
Mae angen i chi ddeall nad yw'r adweithydd I/O heb ei anfanteision, sef:
Mae defnyddio adweithydd I/O mewn amgylchedd aml-edau braidd yn anoddach, oherwydd bydd yn rhaid i chi reoli'r llifau â llaw.
Mae ymarfer yn dangos nad yw'r llwyth yn unffurf yn y rhan fwyaf o achosion, a all arwain at logio un edefyn tra bod un arall yn brysur gyda gwaith.
Os bydd un triniwr digwyddiad yn blocio edefyn, bydd dewisydd y system ei hun hefyd yn blocio, a all arwain at fygiau anodd eu darganfod.
Yn datrys y problemau hyn Rhagweithredwr I/O, sydd yn aml â rhaglennydd sy'n dosbarthu'r llwyth yn gyfartal i gronfa o edafedd, ac mae ganddo hefyd API mwy cyfleus. Byddwn yn siarad amdano yn nes ymlaen, yn fy erthygl arall.
Casgliad
Dyma lle mae ein taith o theori yn syth i'r gwacáu proffiliwr wedi dod i ben.
Ni ddylech aros ar hyn, oherwydd mae yna lawer o ddulliau eraill yr un mor ddiddorol o ysgrifennu meddalwedd rhwydwaith gyda gwahanol lefelau o gyfleustra a chyflymder. Yn ddiddorol, yn fy marn i, rhoddir dolenni isod.