Razumevanje vhodno / izhodnega (I / O) modela vaše aplikacije lahko pomeni razliko med aplikacijo, ki obravnava obremenitev, ki ji je izpostavljena, in tisto, ki se zmečka v resničnih primerih uporabe. Morda je vaša aplikacija majhna in ne služi velikim obremenitvam, vendar je morda veliko manj pomembna. Ker pa se prometna obremenitev vaše aplikacije povečuje, vas lahko delo z napačnim V / I modelom pripelje v svet poškodb.
In kot v večini primerov, ko je možnih več pristopov, tudi tu ni samo vprašanje, kateri je boljši, temveč tudi razumevanje kompromisov. Sprehodimo se po V / I pokrajini in poglejmo, kaj lahko vohunimo.
V tem članku bomo primerjali Node, Java, Go in PHP z Apachejem, razpravljali bomo o tem, kako različni jeziki oblikujejo svoj I / O, prednosti in slabosti vsakega modela in zaključili z nekaterimi osnovnimi merili. Če vas skrbi delovanje V / I naslednje vaše spletne aplikacije, je ta članek namenjen vam.
Da bi razumeli dejavnike, povezane z V / I, moramo najprej pregledati koncepte na ravni operacijskega sistema. Čeprav je malo verjetno, da bi se morali s številnimi od teh konceptov ukvarjati neposredno, se z njimi ves čas ukvarjate posredno skozi okolje izvajanja aplikacije. In podrobnosti so pomembne.
Prvič, imamo sistemske klice, ki jih lahko opišemo na naslednji način:
Zdaj sem pravkar rekel, da syscalls blokirajo, in to v splošnem drži. Vendar so nekateri klici kategorizirani kot »neblokirajoči«, kar pomeni, da jedro sprejme vašo zahtevo, jo nekam postavi v čakalno vrsto ali medpomnilnik in se nato takoj vrne, ne da bi čakal, da se dejanski I / O zgodi. Tako se 'blokira' le za kratek čas, ravno toliko, da postavi vašo zahtevo v vrsto.
Nekaj primerov (sistemskih klicev za Linux) bi lahko pomagalo razjasniti: - read()
je blokirni klic - posredujete mu ročaj, v katerem pišete, katera datoteka in medpomnilnik kje naj dostavi podatke, ki jih prebere, in klic se vrne, ko so podatki tam. Upoštevajte, da ima to prednost, da je prijazen in preprost. - epoll_create()
, epoll_ctl()
in epoll_wait()
so klici, ki vam omogočajo, da ustvarite skupino ročajev, ki jih želite poslušati, dodate / odstranite upravljavce iz te skupine in nato blokirate, dokler ne pride do kakršne koli dejavnosti. To vam omogoča učinkovito upravljanje velikega števila V / I operacij z eno nitjo, vendar sem v prednosti. To je super, če potrebujete funkcionalnost, toda kot vidite, je zagotovo bolj zapletena za uporabo.
Tu je pomembno razumeti vrstni red razlike v času. Če jedro CPU deluje na 3 GHz, ne da bi prišlo do optimizacij, ki jih CPU lahko naredi, izvede 3 milijarde ciklov na sekundo (ali 3 cikle na nanosekundo). Neblokirajoči sistemski klic lahko traja približno 10 s ciklov - ali 'razmeroma nekaj nanosekund'. Klic, ki blokira prejemanje informacij po omrežju, lahko traja veliko dlje - recimo na primer 200 milisekund (1/5 sekunde). Recimo, da je na primer neblokirajoči klic trajal 20 nanosekund, blokirni klic pa 200 000 000 nanosekund. Vaš postopek je pravkar čakal 10 milijonov krat dlje na klic blokiranja.
Jedro ponuja način za blokiranje V / I (»beri iz te omrežne povezave in mi daj podatke«) in neblokiranja V / I (»povej mi, kdaj ima katera od teh omrežnih povezav nove podatke«). Kateri mehanizem je uporabljen, bo blokiral postopek klica za dramatično različno dolgo obdobje.
Tretja stvar, ki jo je nujno upoštevati, je, kaj se zgodi, ko imate veliko niti ali procesov, ki se začnejo blokirati.
Za naše namene med nitjo in postopkom ni velike razlike. V resničnem življenju je najbolj opazna razlika, povezana z zmogljivostjo, v tem, da si niti delijo isti pomnilnik in imajo procesi vsak svoj pomnilniški prostor, zato ločeni procesi zavzamejo veliko več pomnilnika. Ko pa govorimo o razporejanju, se v resnici izkaže seznam stvari (niti in procesov), ki jih mora vsak dobiti delček časa izvajanja na razpoložljivih jedrih procesorja. Če imate 300 zagnanih niti in 8 jeder, na katerih jih je treba zagnati, morate čas razdeliti tako, da bo vsak dobil svoj delež, pri čemer bo vsako jedro teklo kratek čas in nato prešlo na naslednjo nit. To se naredi s pomočjo »kontekstnega stikala«, s čimer se CPU preklopi iz izvajanja ene niti / procesa v drugo.
S temi kontekstnimi stikali so povezani stroški - trajajo nekaj časa. V nekaterih hitrih primerih lahko traja manj kot 100 nanosekund, vendar ni nič nenavadnega, da traja 1000 nanosekund ali več, odvisno od podrobnosti izvedbe, hitrosti procesorja / arhitekture, predpomnilnika CPU itd.
In več niti (ali procesov), več preklapljanja konteksta. Ko govorimo o tisočih nitih in stotinah nanosekund za vsako, lahko stvari postanejo zelo počasne.
Neblokirajoči klici v bistvu jedru sporočajo, da me 'pokliče le, če imate na kateri koli od teh povezav nove podatke ali dogodek.' Ti neblokirajoči klici so zasnovani tako, da učinkovito obvladujejo velike V / I obremenitve in zmanjšajo preklapljanje med konteksti.
Z mano do zdaj? Ker zdaj prihaja zabaven del: poglejmo, kaj nekateri priljubljeni jeziki počnejo s temi orodji, in poiščimo nekaj sklepov o kompromisih med enostavnostjo uporabe in zmogljivostjo ... ter drugimi zanimivimi pikami.
Opomba: primeri, prikazani v tem članku, so sicer nepomembni (in delni, prikazani so le ustrezni biti); dostop do baze podatkov, zunanji predpomnilniški sistemi (memcache itd. vse) in vse, kar zahteva V / I, bo na koncu izvedlo nekakšen V / I klic pod pokrovom, kar bo imelo enak učinek kot prikazani preprosti primeri. Tudi za scenarije, kjer je V / I opisan kot 'blokiranje' (PHP, Java), branje in zapisovanje HTTP zahtev in odzivov tudi sami blokirajo klice: Še več V / I-ja, skritega v sistemu s pripadajočimi težavami z zmogljivostjo vzeti v obzir.
Pri izbiri programskega jezika za projekt gre veliko dejavnikov. Dejavnikov je celo veliko, če upoštevate samo uspešnost. Če pa vas skrbi, da bo vaš program omejen predvsem z vhodno / izhodnimi operacijami, morate to vedeti, če je vhodno / izhodna izvedba za vaš projekt pomembna.
V devetdesetih letih je bilo veliko ljudi oblečenih Converse čevljev in pisanje CGI skriptov v Perl. Nato se je pojavil PHP in, kolikor ga nekateri radi krpajo, je poenostavil dinamične spletne strani.
razvijalec pokliče s težavo. poskušali so odpraviti napake
Model, ki ga uporablja PHP, je dokaj preprost. Obstaja nekaj različic, vendar je vaš povprečni PHP strežnik videti tako:
Zahteva HTTP prihaja iz uporabnikovega brskalnika in zadene vaš spletni strežnik Apache. Apache za vsako zahtevo ustvari ločen postopek z nekaj optimizacijami, da jih znova uporabi, da zmanjša število, ki jih mora opraviti (ustvarjanje procesov je, razmeroma rečeno, počasno). Apache pokliče PHP in mu pove, naj zažene ustrezen .php
datoteko na disku. Koda PHP izvaja in blokira V / I klice. Pokličete file_get_contents()
v PHP in pod pokrovom naredi read()
syscalls in čaka na rezultate.
In dejanska koda je preprosto vdelana neposredno na vašo stran in operacije blokirajo:
query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>
Glede tega, kako se to integrira s sistemom, je tako:
Precej preprosto: en postopek na zahtevo. V / I klici samo blokirajo. Prednost? Je preprosto in deluje. Pomanjkljivost? Hkrati ga udarite z 20.000 odjemalci in vaš strežnik bo zagorel. Ta pristop ni dobro prilagojen, ker se orodja, ki jih ponuja jedro za obdelavo vhodnih / izhodnih podatkov velike količine (epoll itd.), Ne uporabljajo. In da žalite škodo, zagon ločenega postopka za vsako zahtevo ponavadi uporablja veliko sistemskih virov, zlasti pomnilnika, kar je pogosto prva stvar, ki vam je v takem scenariju zmanjkalo.
Opomba: Pristop, uporabljen za Ruby, je zelo podoben pristopu PHP in na splošno, na splošno, valovito, jih lahko štejemo za enake za naše namene.
programsko določena radijska vadnica pdf
Torej, Java pride zraven, ravno takrat, ko ste kupili svoje prvo ime domene, in bilo je v redu, če smo naključno rekli 'pika com' po stavku. In Java ima v jezik vgrajeno večnitnost, ki je (še posebej takrat, ko je bila ustvarjena) precej super.
Večina spletnih strežnikov Java deluje tako, da začne vsako novo izvedbeno nit za vsako zahtevo, ki pride, nato pa v tej niti sčasoma pokliče funkcijo, ki ste jo napisali kot razvijalec aplikacije.
Izvajanje I / O v Java Servlet ponavadi izgleda nekako takole:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream('/path/to/file'); // blocking network I/O URLConnection urlConnection = (new URL('http://example.com/example-microservice')).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println('...'); }
Ker je naš doGet
zgornja metoda ustreza eni zahtevi in se izvaja v lastni niti, namesto ločenega postopka za vsako zahtevo, ki zahteva svoj pomnilnik, imamo ločeno nit. To ima nekaj lepih ugodnosti, na primer možnost skupne rabe stanja, predpomnjenih podatkov itd. Med nitmi, ker lahko dostopajo do pomnilnika drug drugega, vendar je vpliv na to, kako deluje z urnikom, še vedno skoraj enak tistemu, kar se počne v PHP primer prej. Vsaka zahteva dobi novo nit in različne bloke V / I operacij znotraj te niti, dokler z zahtevo v celoti ne ravnamo. Niti se združijo, da se zmanjšajo stroški njihovega ustvarjanja in uničenja, vendar vseeno tisoče povezav pomeni na tisoče niti, kar je slabo za razporejevalnik.
Pomemben mejnik je, da je v različici 1.4 Java (in znova pomembna nadgradnja v 1.7) dobila zmožnost neblokiranja I / O klicev. Večina aplikacij, spletnih in drugače, je ne uporablja, je pa vsaj na voljo. Nekateri spletni strežniki Java to skušajo izkoristiti na različne načine; vendar velika večina uvedenih aplikacij Java še vedno deluje, kot je opisano zgoraj.
Java nas približa in ima zagotovo nekaj dobrih funkcionalnosti za V / I, vendar še vedno v resnici ne reši problema, kaj se zgodi, ko imate močno vezano I / O aplikacijo, v katero se zaletava. tla z več tisoč blokirajočimi nitmi.
Priljubljeni otrok v bloku, ko gre za boljši I / O, je Node.js. Vsakomur, ki je imel celo najkrajši uvod v Node, so povedali, da ta 'ne blokira' in da učinkovito obvladuje V / I. In to v splošnem drži. Toda hudič je v podrobnostih in načinih, s katerimi je bilo to čarovništvo doseženo, pomembno pri izvedbi.
V bistvu je sprememba paradigme, ki jo izvaja Node, ta, da namesto da bi v bistvu rekli 'napiši svojo kodo tukaj, da bo obravnavala zahtevo', namesto tega rečejo 'napiši kodo tukaj, da začneš obravnavati zahtevo.' Vsakič, ko morate narediti nekaj, kar vključuje V / I, oddate zahtevo in daste funkcijo povratnega klica, ki jo bo Node poklical, ko bo končana.
Tipična koda vozlišča za izvajanje V / I operacije v zahtevi gre takole:
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
Kot lahko vidite, sta tu dve funkciji povratnega klica. Prva se pokliče, ko se zahteva začne, druga pa, ko so na voljo podatki o datoteki.
To v bistvu daje Node priložnost, da učinkovito obvladuje V / I med temi povratnimi klici. Scenarij, kjer bi bilo še bolj pomembno, je klic klicev baze podatkov v vozlišču, vendar se ne bom obremenjeval s primerom, ker gre za popolnoma enako načelo: zaženete klic baze podatkov in Nodu daste funkcijo povratnega klica, izvede vhodno / izhodne operacije ločeno z neblokirajočimi klici in nato prikliče vašo funkcijo povratnega klica, ko so na voljo zahtevani podatki. Ta mehanizem čakalne vrste V / I klicev in dovoljevanja vozlišču, da to obdeluje in nato pridobi povratni klic, se imenuje »zanka dogodkov«. In deluje precej dobro.
Vendar ima ta model ulov. Razlog za to je veliko bolj povezan s tem, kako je implementiran V8 JavaScript motor (Chromov JS motor, ki ga uporablja Node) eno kot karkoli drugega. Koda JS, ki jo napišete, se izvaja v eni niti. Za trenutek pomislite na to. To pomeni, da medtem ko se V / I izvaja z učinkovitimi tehnikami neblokiranja, lahko vaš JS, ki izvaja operacije, vezane na CPU, deluje v eni niti, pri čemer vsak del kode blokira naslednjega. Pogost primer, kje bi se to lahko pojavilo, je preiskovanje zapisov baze podatkov, da jih na nek način obdelamo, preden jih pošljemo odjemalcu. Tu je primer, ki prikazuje, kako to deluje:
var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i Čeprav Node učinkovito obvladuje V / I, to for
zanka v zgornjem primeru uporablja procesorske cikle znotraj ene in edine glavne niti. To pomeni, da če imate 10.000 povezav, lahko ta zanka privede do iskanja celotne aplikacije, odvisno od tega, kako dolgo traja. Vsaka zahteva mora deliti delček časa, enega za drugim, v vaši glavni niti.
Izhodišče, na katerem temelji celoten koncept, je, da so V / I operacije najpočasnejši del, zato je najpomembneje, da se z njimi ravna učinkovito, četudi to pomeni, da se druga obdelava izvaja serijsko. V nekaterih primerih to drži, v vseh pa ne.
Druga stvar je, da, čeprav je to le mnenje, je lahko pisanje kopice ugnezdenih povratnih klicev precej naporno in nekateri trdijo, da je kodi precej težje slediti. Nenavadni so povratni klici, ugnezdeni štiri, pet ali celo več ravni globoko znotraj kode Node.
Spet smo se vrnili k kompromisom. Model Node dobro deluje, če je vaša glavna težava z zmogljivostjo V / I. Njegova ahilova peta pa je ta, da lahko preklopite v funkcijo, ki obravnava zahtevo HTTP, in vstavite CPU intenzivno kodo ter vsako povezavo pripeljete do iskanja, če niste previdni.
Seveda ne blokira: Pojdi
Preden pridem v razdelek za Go, je primerno, da razkrijem, da sem ljubitelj Go. Uporabljal sem ga za številne projekte in odkrito zastopam prednosti produktivnosti in jih pri svojem delu vidim, ko ga uporabljam.
Kljub temu poglejmo, kako se ukvarja z V / I. Ena ključnih lastnosti jezika Go je ta, da vsebuje svoj lastni razporejevalnik. Namesto vsake niti izvajanja, ki ustreza posamezni niti OS, deluje s konceptom 'goroutine'. Izvajalno okolje Go lahko dodeli goroutino niti OS in jo izvede, ali jo začasno ustavi in ne poveže z nitjo OS, glede na to, kaj ta goroutina počne. Vsaka zahteva, ki jo prejme Go-ov strežnik HTTP, se obravnava v ločeni Goroutini.
Diagram, kako deluje načrtovalnik, je videti takole:

Pod pokrovom to izvajajo različne točke v času izvajanja Go, ki izvajajo vhodno / izhodni klic tako, da podajo zahtevo za pisanje / branje / povezovanje / itd., Uspavanje trenutne goroutine z informacijami za ponovno bujenje goroutine. nadaljnje ukrepe.
Dejansko izvajalno okolje Go počne nekaj, kar se ne razlikuje od tistega, kar počne Node, le da je mehanizem povratnega klica vgrajen v izvedbo V / I klica in samodejno komunicira z razporejevalnikom. Prav tako ne trpi zaradi omejitve, da bi se morala vsa koda vašega vodnika izvajati v isti niti, Go bo samodejno preslikal vaše Goroutine na toliko niti OS, za katere meni, da so primerne glede na logiko v njegovem načrtovalniku. Rezultat je takšna koda:
func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query('SELECT ...') for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }
Kot lahko vidite zgoraj, je osnovna struktura kode tega, kar počnemo, podobna strukturi bolj poenostavljenih pristopov in kljub temu doseže neblokirajoči V / I pod pokrovom.
V večini primerov je to na koncu 'najboljše iz obeh svetov'. Neblokirajoči V / I se uporablja za vse pomembne stvari, toda vaša koda je videti kot da blokira in je zato preprostejša za razumevanje in vzdrževanje. Preostanek ureja interakcija med načrtovalnikom Go in razporejevalnikom OS. To ni popolna magija in če zgradite velik sistem, je vredno vložiti čas, da boste podrobneje razumeli, kako deluje; hkrati pa okolje, ki ga dobite 'izven škatle', deluje in se zelo dobro prilagaja.
Go ima lahko svoje napake, toda na splošno način ravnanja z V / I ni med njimi.
Laži, preklete laži in merila uspešnosti
Težko je natančno določiti časovne okvire preklopa konteksta, povezanih s temi različnimi modeli. Lahko bi tudi trdil, da vam je manj koristen. Namesto tega vam bom dal nekaj osnovnih meril uspešnosti, ki primerjajo splošno delovanje strežnikov HTTP v teh strežniških okoljih. Upoštevajte, da pri izvedbi celotne poti / odziva HTTP od konca do konca sodeluje veliko dejavnikov, tukaj predstavljene številke pa so le nekateri vzorci, ki sem jih sestavil za osnovno primerjavo.
Za vsako od teh okolij sem napisal ustrezno kodo za branje v 64k datoteki z naključnimi bajti, na njej N-krat izvedel razpršitev SHA-256 (N je določeno v iskalnem nizu URL-ja, npr. .../test.php?n=100
) in nastalo razpršitev natisnite v šestnajstiško. To sem izbral, ker gre za zelo preprost način za izvajanje istih meril z nekaterimi doslednimi V / I in nadzorovan način za povečanje uporabe CPU.
Glej teh referenčnih opomb za malo več podrobnosti o uporabljenih okoljih.
Najprej si oglejmo nekaj primerov z nizko sočasnostjo. Izvajanje 2000 ponovitev s 300 sočasnimi zahtevami in samo enim zgoščevanjem na zahtevo (N = 1) nam da to:

Časi so povprečno število milisekund za dokončanje zahteve za vse sočasne zahteve. Nižje je boljše.Iz samo tega grafa je težko sklepati, toda zdi se mi, da pri tej količini povezav in izračunavanja opažamo čase, ki so bolj povezani s splošno izvedbo jezikov samih, še toliko bolj, da V / I. Upoštevajte, da se jeziki, ki veljajo za »skriptne jezike« (ohlapno tipkanje, dinamična interpretacija), izvajajo najpočasneje.
Kaj pa se zgodi, če povečamo N na 1000, še vedno s 300 sočasnimi zahtevami - enaka obremenitev, vendar 100-krat več ponovitev zgoščevanja (bistveno več obremenitve procesorja):

Časi so povprečno število milisekund za dokončanje zahteve za vse sočasne zahteve. Nižje je boljše.Naenkrat se zmogljivost vozlišča bistveno zmanjša, ker se CPU-intenzivne operacije v vsaki zahtevi medsebojno blokirajo. In kar je zanimivo, zmogljivost PHP se izboljša (v primerjavi z drugimi) in v tem testu premaga Javo. (Omeniti velja, da je v PHP izvedba SHA-256 zapisana v jeziku C in izvedbena pot porabi veliko več časa v tej zanki, saj zdaj izvajamo 1000 ponovitev razprševanja).
ustvarjanje bloga z angularjs
Zdaj poskusimo s 5000 sočasnimi povezavami (z N = 1) - ali čim bližje temu, kolikor sem lahko prišel. Na žalost v večini teh okolij stopnja napak ni bila nepomembna. Za ta grafikon si bomo ogledali skupno število zahtev na sekundo. Višje kot bolje :

Skupno število zahtev na sekundo. Višje je bolje.In slika je videti povsem drugače. To je ugibanje, vendar se zdi, da pri visoki glasnosti povezave režijski stroški na povezavo, povezani z ustvarjanjem novih procesov, in dodatni pomnilnik, povezan z njim v PHP + Apache, postanejo prevladujoči dejavnik in zmanjšujejo učinkovitost PHP. Jasno je, da je tukaj zmagovalec Go, sledijo mu Java, Node in na koncu PHP.
Medtem ko je dejavnikov, povezanih z vašo splošno pretočnostjo, veliko in se med različnimi aplikacijami zelo razlikujejo, bolj ko boste razumeli drobovje dogajanja pod pokrovom in vpletene kompromise, boljše vam bo.
V povzetku
Ob vsem zgoraj navedenem je povsem jasno, da so se z razvojem jezikov z njim razvile tudi rešitve za obsežne aplikacije, ki izvajajo veliko I / O.
Po pravici povedano imata PHP in Java kljub opisom v tem članku izvedb od neblokirajoči V / I na voljo za uporabo v spletne aplikacije . Toda ti niso tako pogosti kot zgoraj opisani pristopi, zato bi bilo treba upoštevati tudi operativne režijske stroške vzdrževanja strežnikov s takimi pristopi. Da ne omenjam, da mora biti vaša koda strukturirana tako, da deluje v takšnih okoljih; vaša »običajna« spletna aplikacija PHP ali Java običajno ne bo delovala brez večjih sprememb v takem okolju.
Za primerjavo, če upoštevamo nekaj pomembnih dejavnikov, ki vplivajo na zmogljivost in enostavnost uporabe, dobimo to:
Jezik Niti v primerjavi s procesi Neblokirajoči V / I Enostavnost uporabe PHP Procesi Ne Java Niti Na voljo Zahteva povratne klice Node.js Niti Da Zahteva povratne klice Pojdi Niti (Goroutine) Da Noben povratni klic ni potreben
Niti bodo na splošno veliko bolj pomnilniško učinkovitejši od procesov, saj si delijo isti pomnilniški prostor, medtem ko procesi ne. Če to združimo z dejavniki, povezanimi z neblokiranjem V / I, lahko ugotovimo, da vsaj z zgoraj navedenimi dejavniki, ko se spodaj premikamo po seznamu, se splošna nastavitev, ko se nanaša na V / I, izboljša. Torej, če bi moral izbrati zmagovalca na zgornjem tekmovanju, bi bil to zagotovo Go.
Kljub temu je v praksi izbira okolja, v katerem boste sestavili svojo aplikacijo, tesno povezana s poznavanjem vaše ekipe z omenjenim okoljem in splošno produktivnostjo, ki jo lahko z njim dosežete. Zato morda ni smiselno, da se vsaka ekipa le potopi in začne razvijati spletne aplikacije in storitve v Node ali Go. Dejansko je pogosto glavni razlog za neuporabo drugega jezika in / ali okolja iskanje razvijalcev ali poznavanje lastne ekipe. Se pravi, da so se časi v zadnjih petnajstih letih ali tako zelo spremenili.
Upamo, da vam zgornje pomaga dati jasnejšo sliko dogajanja pod pokrovom in vam daje nekaj idej, kako ravnati z razširljivostjo aplikacije v resničnem svetu. Vesel vnos in izhod!