Výkon I/O na straně serveru: Výkonnost serveru: Node vs. PHP vs. Java vs. Go

Říj 22, 2021
admin

Pochopení vstupně-výstupního (I/O) modelu vaší aplikace může znamenat rozdíl mezi aplikací, která zvládá zátěž, jíž je vystavena, a aplikací, která se rozpadá tváří v tvář reálným případům použití. Možná, že dokud je vaše aplikace malá a neslouží k vysokému zatížení, může na tom záležet mnohem méně. Ale jakmile se zatížení vaší aplikace zvýší, může vás práce se špatným I/O modelem dostat do pěkné šlamastyky.

A stejně jako ve většině jiných situací, kde je možné použít více přístupů, nejde jen o to, který z nich je lepší, ale o pochopení kompromisů. Pojďme se projít po krajině I/O a podívat se, co můžeme vyzvídat.

V tomto článku porovnáme Node, Javu, Go a PHP s Apachem, probereme, jak jednotlivé jazyky modelují své I/O, výhody a nevýhody jednotlivých modelů, a na závěr uvedeme několik základních benchmarků. Pokud vás zajímá výkon I/O vaší příští webové aplikace, je tento článek určen právě vám.

Základy I/O: Abychom pochopili faktory související s I/O, musíme si nejprve zopakovat pojmy na úrovni operačního systému. I když je nepravděpodobné, že byste se s mnoha z těchto konceptů museli setkat přímo, nepřímo se s nimi setkáváte prostřednictvím běhového prostředí vaší aplikace neustále. A na detailech záleží.

Systémová volání

Nejprve tu máme systémová volání, která lze popsat takto:

  • Váš program (v „uživatelské zemi“, jak se říká) musí požádat jádro operačního systému, aby jeho jménem provedlo nějakou I/O operaci.
  • „Syscall“ je prostředek, kterým váš program požádá jádro, aby něco udělalo. Konkrétní způsoby implementace se v jednotlivých operačních systémech liší, ale základní koncept je stejný. Bude existovat nějaká specifická instrukce, která přenese řízení z vašeho programu na jádro (podobně jako volání funkce, ale s nějakou speciální omáčkou speciálně pro řešení této situace). Obecně řečeno, syscall jsou blokovací, což znamená, že váš program čeká, až se jádro vrátí zpět k vašemu kódu.
  • Jádro provede základní I/O operaci na daném fyzickém zařízení (disk, síťová karta atd.) a odpoví na syscall. V reálném světě může jádro pro splnění vašeho požadavku udělat řadu věcí, včetně čekání na připravenost zařízení, aktualizace svého vnitřního stavu atd. ale to vás jako vývojáře aplikace nezajímá. To je práce jádra.

Blokující vs. neblokující volání

Nyní jsem právě řekl, že syscall jsou blokující, a to je v obecném smyslu pravda. Některá volání jsou však klasifikována jako „neblokující“, což znamená, že jádro přijme váš požadavek, umístí jej někam do fronty nebo vyrovnávací paměti a pak se okamžitě vrátí, aniž by čekalo na skutečný vstup/výstup. „Blokuje“ tedy jen velmi krátkou dobu, jen tak dlouho, aby váš požadavek zařadilo do fronty.

Několik příkladů (linuxových syscallů) by mohlo pomoci s objasněním:- read() je blokující volání – předáte mu handle, který říká, který soubor a vyrovnávací paměť, kam má doručit načtená data, a volání se vrátí, jakmile jsou tam data. Všimněte si, že to má tu výhodu, že je to pěkné a jednoduché.- epoll_create(), epoll_ctl() a epoll_wait() jsou volání, která v tomto pořadí umožňují vytvořit skupinu handle, na které se bude naslouchat, přidávat/odebírat z této skupiny obslužné programy a pak blokovat, dokud se neobjeví nějaká aktivita. To umožňuje efektivně řídit velké množství I/O operací pomocí jediného vlákna, ale to už předbíhám. Je to skvělé, pokud tuto funkci potřebujete, ale jak vidíte, je to určitě složitější na používání.

Je důležité si uvědomit, že je zde řádový rozdíl v časování. Pokud jádro procesoru běží na frekvenci 3 GHz, aniž bychom se zabývali optimalizacemi, které procesor umí, provádí 3 miliardy cyklů za sekundu (nebo 3 cykly za nanosekundu). Neblokované systémové volání může trvat řádově desítky cyklů – neboli „relativně málo nanosekund“. Volání, které blokuje informace přijímané po síti, může trvat mnohem déle – řekněme například 200 milisekund (1/5 sekundy). A řekněme, že neblokující volání trvalo například 20 nanosekund a blokující volání trvalo 200 000 000 nanosekund. Váš proces právě čekal na blokující volání 10 milionkrát déle.

Jádro poskytuje prostředky pro provádění jak blokujícího I/O („přečti z tohoto síťového připojení a dej mi data“), tak neblokujícího I/O („řekni mi, kdy má některé z těchto síťových připojení nová data“). A to, který mechanismus se použije, zablokuje volající proces na dramaticky různě dlouhou dobu.

Rozvrhování

Třetí věc, kterou je důležité sledovat, je, co se stane, když máte mnoho vláken nebo procesů, které začnou blokovat.

Pro naše účely není velký rozdíl mezi vláknem a procesem. V reálném životě je nejvýraznějším rozdílem souvisejícím s výkonem to, že vzhledem k tomu, že vlákna sdílejí stejnou paměť a procesy mají každý svůj vlastní paměťový prostor, má vytváření samostatných procesů tendenci zabírat mnohem více paměti. Když ale mluvíme o plánování, ve skutečnosti se jedná o seznam věcí (jak vláken, tak procesů), z nichž každá potřebuje získat kousek času pro vykonávání na dostupných jádrech procesoru. Pokud máte spuštěno 300 vláken a 8 jader, na kterých mají běžet, musíte čas rozdělit tak, aby každé z nich dostalo svůj díl, přičemž každé jádro poběží krátkou dobu a pak se přesune na další vlákno. To se provádí pomocí „přepínání kontextu“, kdy procesor přepíná z běhu jednoho vlákna/procesu na další.

Tato přepínání kontextu jsou spojena s určitými náklady – zaberou nějaký čas. V některých rychlých případech to může být méně než 100 nanosekund, ale není neobvyklé, že to trvá 1000 nanosekund nebo déle v závislosti na implementačních detailech, rychlosti/architektuře procesoru, cache procesoru atd.

A čím více vláken (nebo procesů), tím více přepínání kontextu. Když se bavíme o tisících vláken a stovkách nanosekund pro každé z nich, může to být velmi pomalé.

Neblokující volání však v podstatě říkají jádru „volej mě jen tehdy, když máš nějaká nová data nebo událost na některém z těchto spojení“. Tato neblokující volání jsou navržena tak, aby efektivně zvládala velké I/O zátěže a omezila přepínání kontextu.

Jste zatím se mnou? Protože teď přichází ta zábavná část:

Poznamenejme, že příklady uvedené v tomto článku jsou triviální (a neúplné, zobrazeny jsou pouze relevantní části); přístup k databázi, externí cachovací systémy (memcache apod.) a cokoli, co vyžaduje I/O, bude nakonec provádět nějaký druh I/O volání pod kapotou, což bude mít stejný efekt jako uvedené jednoduché příklady. Také v případě scénářů, kde jsou I/O popsány jako „blokující“ (PHP, Java), jsou samotná čtení a zápisy požadavků a odpovědí HTTP blokujícími voláními:

Výběr programovacího jazyka pro projekt ovlivňuje mnoho faktorů. Je dokonce mnoho faktorů, když se bere v úvahu pouze výkon. Pokud se však obáváte, že váš program bude omezen především vstupem/výstupem, pokud výkon vstupu/výstupu rozhoduje o vašem projektu, jsou to věci, které byste měli vědět.

Přístup „Keep It Simple“: V 90. letech spousta lidí nosila boty Converse a psala skripty CGI v Perlu. Pak přišlo PHP, a i když na něj někteří lidé rádi nadávají, usnadnilo tvorbu dynamických webových stránek.

Model, který PHP používá, je poměrně jednoduchý. Existuje několik jeho variant, ale průměrný server PHP vypadá takto:

Z prohlížeče uživatele přijde požadavek HTTP a narazí na webový server Apache. Apache vytvoří pro každý požadavek samostatný proces s určitými optimalizacemi pro jejich opakované použití, aby minimalizoval jejich počet (vytváření procesů je relativně pomalé). apache zavolá PHP a řekne mu, aby spustil příslušný .php soubor na disku. kód PHP se spustí a provede blokující I/O volání. Zavoláte file_get_contents() v PHP a pod kapotou to provede read() syscall a čeká na výsledky.

A samozřejmě vlastní kód je jednoduše vložen přímo do vaší stránky a operace jsou blokující:

<?php// blocking file I/O$file_data = file_get_contents('/path/to/file.dat');// blocking network I/O$curl = curl_init('http://example.com/example-microservice');$result = curl_exec($curl);// some more blocking network I/O$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');?>

Z hlediska toho, jak se to integruje se systémem, je to takto:

Pěkně jednoduché: jeden proces na jeden požadavek. I/O volání se prostě zablokují. Výhoda? Je to jednoduché a funguje to. Nevýhoda? Když na to narazíte s 20 000 klienty současně, váš server vzplane. Tento přístup není dobře škálovatelný, protože se nevyužívají nástroje, které jádro poskytuje pro práci s velkým objemem I/O (epoll atd.). A aby toho nebylo málo, spouštění samostatného procesu pro každý požadavek má tendenci spotřebovávat spoustu systémových prostředků, zejména paměti, která v takovém případě často dojde jako první.

Poznámka: Přístup používaný v jazyce Ruby je velmi podobný přístupu v jazyce PHP a v širokém, obecném, rukopisném smyslu je lze pro naše účely považovat za stejné.

Multithreadový přístup: Java

Takže Java přichází, zhruba v době, kdy jste si koupili první doménové jméno a bylo cool za větou náhodně říkat „tečka com“. A Java má v sobě zabudovaný multithreading, což je (zejména na dobu, kdy vznikla) docela úžasné.

Většina webových serverů v Javě funguje tak, že pro každý příchozí požadavek se spustí nové prováděcí vlákno a v tomto vlákně se případně zavolá funkce, kterou jste jako vývojář aplikace napsali.

Provádění I/O v Java Servletu obvykle vypadá nějak takto:

public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{// blocking file I/OInputStream fileIs = new FileInputStream("/path/to/file");// blocking network I/OURLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();InputStream netIs = urlConnection.getInputStream();// some more blocking network I/Oout.println("...");}

Protože naše výše uvedená metoda doGet odpovídá jednomu požadavku a je spouštěna ve vlastním vlákně, místo samostatného procesu pro každý požadavek, který vyžaduje vlastní paměť, máme samostatné vlákno. To má některé příjemné výhody, například možnost sdílet stav, data v mezipaměti atd. mezi vlákny, protože mohou vzájemně přistupovat k paměti, ale dopad na způsob interakce s plánem je stále téměř totožný s tím, co se provádí v předchozím příkladu PHP. Každý požadavek dostane nové vlákno a různé I/O operace se blokují uvnitř tohoto vlákna, dokud není požadavek plně zpracován. Vlákna jsou sdružována, aby se minimalizovaly náklady na jejich vytváření a ničení, ale přesto tisíce spojení znamenají tisíce vláken, což je pro plánovač špatné.

Důležitým milníkem je, že ve verzi 1.4 (a opět výrazné vylepšení ve verzi 1.7) získala Java schopnost provádět neblokující I/O volání. Většina aplikací, webových i jiných, ji sice nepoužívá, ale alespoň je k dispozici. Některé webové servery v Javě se toho snaží různými způsoby využívat, nicméně drtivá většina nasazených aplikací v Javě stále funguje tak, jak je popsáno výše.

Java se nám přibližuje a rozhodně má dobrou out-of-the-box funkcionalitu pro I/O, ale stále neřeší problém, co se stane, když máte aplikaci silně vázanou na I/O, která je zatížena mnoha tisíci blokujícími vlákny.

Neblokující I/O jako občan první třídy: Node

Populárním dítětem v bloku, pokud jde o lepší I/O, je Node.js. Každému, kdo se s Node alespoň zběžně seznámil, bylo řečeno, že je „neblokující“ a že I/O zpracovává efektivně. A to je v obecném smyslu pravda. Ale ďábel se skrývá v detailech a pokud jde o výkon, záleží na prostředcích, kterými bylo tohoto kouzla dosaženo.

Změna paradigmatu, kterou Node implementuje, v podstatě spočívá v tom, že místo toho, aby v podstatě řekl „napište svůj kód sem, aby zpracoval požadavek“, místo toho říká „napište kód sem, aby začal zpracovávat požadavek“. Pokaždé, když potřebujete provést něco, co zahrnuje vstupně-výstupní operace, zadáte požadavek a funkci zpětného volání, kterou Node zavolá, až bude hotová.

Typický kód Node pro provedení vstupně-výstupní operace v požadavku vypadá takto:

http.createServer(function(request, response) {fs.readFile('/path/to/file', 'utf8', function(err, data) {response.end(data);});});

Jak vidíte, jsou zde dvě funkce zpětného volání. První se zavolá při spuštění požadavku a druhá se zavolá, když jsou k dispozici data souboru.

To v podstatě dává uzlu možnost efektivně zpracovávat I/O operace mezi těmito zpětnými voláními. Scénář, kdy by to bylo ještě relevantnější, je ten, kdy v Node provádíte volání databáze, ale nebudu se obtěžovat příkladem, protože jde o úplně stejný princip: Spustíte volání databáze a dáte Node funkci zpětného volání, ten provede I/O operace odděleně pomocí neblokujících volání a pak zavolá vaši funkci zpětného volání, když jsou k dispozici požadovaná data. Tento mechanismus řazení I/O volání do fronty a nechání uzlu, aby je zpracoval, a následného získání zpětného volání se nazývá „smyčka událostí“. A funguje docela dobře.

Tento model má však jeden háček. Pod kapotou má důvod mnohem více společného s tím, jak je implementován JavaScriptový engine V8 (JS engine Chrome, který používá Node) 1 než s čímkoli jiným. Kód JS, který píšete, běží v jednom vlákně. Chvíli o tom přemýšlejte. Znamená to, že zatímco I/O se provádí pomocí efektivních neblokujících technik, váš JS can, který provádí operace vázané na procesor, běží v jednom vlákně, přičemž každý kus kódu blokuje další. Běžným příkladem, kde se to může vyskytnout, je smyčka nad záznamy v databázi, která je nějakým způsobem zpracovává před jejich vypsáním klientovi. Zde je příklad, který ukazuje, jak to funguje:

var handler = function(request, response) {connection.query('SELECT ...', function (err, rows) {if (err) { throw err };for (var i = 0; i < rows.length; i++) {// do processing on each row}response.end(...); // write out the results})};

Ačkoli Node zpracovává I/O efektivně, tato smyčka for ve výše uvedeném příkladu využívá cykly CPU uvnitř vašeho jediného hlavního vlákna. To znamená, že pokud máte 10 000 připojení, tato smyčka by mohla celou vaši aplikaci přivést na buben, v závislosti na tom, jak dlouho trvá. Každý požadavek musí sdílet kousek času, jeden po druhém, ve vašem hlavním vlákně.

Předpoklad, na kterém je celý tento koncept založen, je, že I/O operace jsou nejpomalejší částí, a proto je nejdůležitější zpracovávat je efektivně, i kdyby to znamenalo provádět ostatní zpracování sériově. To je v některých případech pravda, ale ne ve všech.

Dalším bodem je, a i když je to jen názor, že může být docela únavné psát spoustu vnořených zpětných volání a někteří tvrdí, že to výrazně ztěžuje sledování kódu. Není neobvyklé vidět zpětná volání vnořená čtyři, pět nebo i více úrovní hluboko v kódu uzlu.

Znovu se vracíme ke kompromisům. Model Node funguje dobře, pokud je vaším hlavním výkonnostním problémem I/O. Jeho achillovou patou však je, že pokud si nedáte pozor, můžete vstoupit do funkce, která zpracovává požadavek HTTP, a vložit do ní kód náročný na výkon procesoru a přivést každé připojení k zátěži.

Přirozeně neblokující: Než se dostanu k části věnované Go, je vhodné, abych prozradil, že jsem Go fanboy. Používal jsem ho v mnoha projektech a jsem otevřeným zastáncem jeho výhod v oblasti produktivity a vidím je při své práci, když ho používám.

Podívejme se tedy na to, jak se vypořádává s vstupy a výstupy. Jednou z klíčových vlastností jazyka Go je, že obsahuje vlastní plánovač. Místo toho, aby každé vlákno provádění odpovídalo jednomu vláknu operačního systému, pracuje s konceptem „goroutines“. A runtime Go může přiřadit goroutine vláknu OS a nechat jej vykonávat, nebo jej pozastavit a nenechat jej spojit s vláknem OS, na základě toho, co tento goroutine dělá. Každý požadavek, který přichází z HTTP serveru Go, je zpracováván v samostatném goroutinu.

Schéma fungování plánovače vypadá takto:

Pod kapotou je to realizováno různými body v běhovém prostředí Go, které realizují volání I/O tím, že provedou požadavek na zápis/čtení/připojení atd, uspí aktuální goroutine s informací, že jej opět probudí, až bude možné provést další akci.

V podstatě runtime Go dělá něco, co se příliš neliší od toho, co dělá Node, s tím rozdílem, že mechanismus zpětného volání je zabudován do implementace I/O volání a automaticky spolupracuje s plánovačem. Netrpí také omezením, že by veškerý kód obsluhy musel běžet ve stejném vlákně, Go automaticky namapuje vaše Goroutines na tolik vláken operačního systému, kolik uzná za vhodné na základě logiky ve svém plánovači. Výsledkem je kód, jako je tento:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {// the underlying network call here is non-blockingrows, 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}

Jak vidíte výše, základní struktura kódu toho, co děláme, se podobá zjednodušeným přístupům, a přesto dosahuje neblokujícího I/O pod kapotou.

Ve většině případů je to nakonec „to nejlepší z obou světů“. Neblokovaný I/O se používá pro všechny důležité věci, ale váš kód vypadá, jako by byl blokovaný, a proto bývá jednodušší na pochopení a údržbu. O zbytek se postará interakce mezi plánovačem Go a plánovačem operačního systému. Není to úplná magie, a pokud vytváříte velký systém, vyplatí se věnovat čas pochopení podrobnějších informací o tom, jak to funguje; ale zároveň prostředí, které dostanete „out-of-the-box“, funguje a škáluje se docela dobře.

Go může mít své chyby, ale obecně řečeno, způsob, jakým zpracovává I/O, mezi ně nepatří.

Lži, zatracené lži a benchmarky

Je obtížné uvést přesné časy přepínání kontextu spojené s těmito různými modely. Také bych mohl tvrdit, že je to pro vás méně užitečné. Místo toho vám tedy poskytnu několik základních benchmarků, které porovnávají celkový výkon serveru HTTP těchto serverových prostředí. Mějte na paměti, že na výkonu celé cesty HTTP požadavek/odpověď od konce do konce se podílí mnoho faktorů, a čísla zde uvedená jsou jen některé ukázky, které jsem dal dohromady pro základní srovnání.

Pro každé z těchto prostředí jsem napsal příslušný kód, který načetl 64k soubor s náhodnými bajty, provedl na něm Nkrát hash SHA-256 (N je uvedeno v řetězci dotazu URL, např. .../test.php?n=100) a vypsal výsledný hash v hexadecimálním tvaru. Tento postup jsem zvolil proto, že je to velmi jednoduchý způsob, jak spustit stejné benchmarky s nějakým konzistentním vstupem/výstupem a řízeným způsobem, jak zvýšit využití procesoru.

Podívejte se na tyto poznámky k benchmarkům, kde najdete trochu podrobnější informace o použitých prostředích.

Nejprve se podívejme na některé příklady s nízkou souběžností. Spuštění 2000 iterací s 300 souběžnými požadavky a pouze jedním hashem na požadavek (N=1) nám dává toto:

Časy jsou průměrný počet milisekund k dokončení požadavku ve všech souběžných požadavcích. Nižší je lepší.

Je těžké vyvozovat závěry pouze z tohoto jednoho grafu, ale zdá se mi, že při tomto objemu připojení a výpočtů vidíme časy, které mnohem více souvisí s obecným prováděním samotných jazyků, než s I/O. Všimněte si, že jazyky, které jsou považovány za „skriptovací jazyky“ (volné psaní, dynamická interpretace), mají nejpomalejší výkon.

Ale co se stane, když zvýšíme N na 1000, stále s 300 souběžnými požadavky – stejná zátěž, ale 100x více iterací hashování (výrazně větší zatížení CPU):

Časy jsou průměrný počet milisekund k dokončení požadavku ve všech souběžných požadavcích. Nižší je lepší.

Najednou výkon uzlu výrazně klesne, protože operace náročné na procesor v jednotlivých požadavcích se navzájem blokují. A je zajímavé, že výkon PHP se výrazně zlepšuje (vzhledem k ostatním) a v tomto testu poráží Javu. (Stojí za zmínku, že v PHP je implementace SHA-256 napsána v jazyce C a cesta vykonávání tráví v této smyčce mnohem více času, protože nyní provádíme 1000 iterací hashe).

Nyní zkusíme 5000 souběžných spojení (s N=1) – nebo tak blízko, jak jsem se k tomu mohl dostat. Bohužel u většiny těchto prostředí byla míra selhání nezanedbatelná. Pro tento graf se podíváme na celkový počet požadavků za sekundu. Čím vyšší, tím lepší:

Celkový počet požadavků za sekundu. Vyšší je lepší.

A obrázek vypadá zcela jinak. Je to jen odhad, ale vypadá to, že při vysokém objemu připojení se režie na jedno připojení spojená se spawnováním nových procesů a s tím spojenou dodatečnou pamětí v PHP+Apache zřejmě stává dominantním faktorem a výkon PHP utlumuje. Je jasné, že zde vítězí Go, následuje Java, Node a nakonec PHP.

Ačkoli faktorů, které se podílejí na celkové propustnosti, je mnoho a také se značně liší aplikace od aplikace, čím více budete rozumět podstatě toho, co se děje pod kapotou, a souvisejícím kompromisům, tím lépe na tom budete.

Shrnutí

Z výše uvedeného je zcela zřejmé, že s vývojem jazyků se vyvíjela i řešení pro práci s rozsáhlými aplikacemi, které provádějí velké množství I/O.

Abychom byli spravedliví, PHP i Java mají navzdory popisu v tomto článku k dispozici implementace neblokujících I/O pro použití ve webových aplikacích. Ty však nejsou tak běžné jako výše popsané přístupy a bylo by třeba vzít v úvahu související provozní režii údržby serverů využívajících takové přístupy. Nemluvě o tom, že váš kód musí být strukturován tak, aby s takovým prostředím fungoval; vaše „normální“ webová aplikace v jazyce PHP nebo Java obvykle v takovém prostředí bez výrazných úprav nepoběží.

Pokud vezmeme v úvahu několik významných faktorů, které ovlivňují výkon i snadnost použití, dostaneme toto srovnání:

.

Jazyk Vlákna vs. Procesy Ne-blocking I/O Snadnost použití
PHP Procesy No
Java Vlákna Dostupné Vyžaduje zpětná volání
Node.js Vlákna Ano Požaduje zpětná volání
Přejít Vlákna (Goroutines) Ano No Callbacks Needed

Vlákna budou obecně mnohem paměťově úspornější než procesy, protože sdílejí stejný paměťový prostor, zatímco procesy nikoli. Když to zkombinujeme s faktory souvisejícími s neblokujícími I/O, vidíme, že přinejmenším u výše uvažovaných faktorů se s postupem dolů v seznamu obecné nastavení, pokud jde o I/O, zlepšuje. Pokud bych tedy měl ve výše uvedené soutěži vybrat vítěze, bylo by jím určitě Go.

V praxi však i tak výběr prostředí, ve kterém budete vytvářet své aplikace, úzce souvisí s tím, jak je váš tým s uvedeným prostředím obeznámen a jaké celkové produktivity s ním můžete dosáhnout. Proto nemusí mít pro každý tým smysl se do toho hned vrhnout a začít vyvíjet webové aplikace a služby v Node nebo Go. Jako hlavní důvod, proč nepoužít jiný jazyk a/nebo prostředí, se totiž často uvádí hledání vývojářů nebo obeznámenost vašeho interního týmu. Přesto se doba za posledních zhruba patnáct let hodně změnila.

Doufejme, že vám výše uvedené pomůže vykreslit jasnější obraz toho, co se děje pod kapotou, a poskytne vám několik nápadů, jak se vypořádat s reálnou škálovatelností vaší aplikace. Šťastné zadávání a výstupy!

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.