Princípy a vzory návrhu pre vysoko súčasné aplikácie

1. Prehľad

V tomto výučbe sa budeme zaoberať niektorými princípmi a vzormi návrhu, ktoré sa časom vytvorili za účelom vytvárania veľmi súbežných aplikácií.

Je však vhodné poznamenať, že navrhovanie súbežnej aplikácie je rozsiahla a zložitá téma, a preto žiadny tutoriál nemôže tvrdiť, že je pri jeho spracovaní vyčerpávajúci. Tu sa budeme venovať niektorým populárnym trikom, ktoré sa často používajú!

2. Základy súbežnosti

Než budeme pokračovať, venujme trochu času pochopeniu základných vecí. Najprv si musíme ujasniť, čo chápeme ako súbežný program. Máme na mysli súbežný program ak sa deje viac výpočtov súčasne.

Teraz si všimnite, že sme spomenuli výpočty prebiehajúce súčasne - to znamená, že prebiehajú súčasne. Môžu, ale nemusia vykonávať súčasne. Je dôležité pochopiť rozdiel ako súčasne vykonávajúce výpočty sa označujú ako paralelné.

2.1. Ako vytvoriť súbežné moduly?

Je dôležité pochopiť, ako môžeme vytvárať súbežné moduly. Existuje veľa možností, ale tu sa zameriame na dve populárne možnosti:

  • Proces: Proces je inštancia bežiaceho programu, ktorá je izolovaná od iných procesov v rovnakom stroji. Každý proces na stroji má svoj izolovaný čas a priestor. Z tohto dôvodu nie je zvyčajne možné zdieľať pamäť medzi procesmi a musia komunikovať odovzdávaním správ.
  • Závit: Niť je naopak iba časť procesu. V programe môže byť viac vlákien zdieľajúcich rovnaký pamäťový priestor. Každé vlákno má však jedinečný zásobník a prioritu. Vlákno môže byť natívne (natívne naplánované operačným systémom) alebo zelené (naplánované runtime knižnicou).

2.2. Ako pôsobia súbežné moduly?

Je to úplne ideálne, ak súbežné moduly nemusia komunikovať, ale často to tak nie je. Z toho vznikajú dva modely súbežného programovania:

  • Zdieľaná pamäť: V tomto modeli, súbežné moduly interagujú čítaním a zápisom zdieľaných objektov do pamäte. To často vedie k prekladaniu súbežných výpočtov, čo spôsobuje podmienky rasy. Môže teda nedeterministicky viesť k nesprávnym stavom.
  • Prebieha odosielanie správ: V tomto modeli, súbežné moduly interagujú odovzdávaním správ navzájom cez komunikačný kanál. Tu každý modul postupne spracováva prichádzajúce správy. Pretože tu nie je zdieľaný stav, programovanie je pomerne jednoduchšie, ale stále to nie je bez závodných podmienok!

2.3. Ako prebiehajú súbežné moduly?

Je to už nejaký čas, čo Moorov zákon narazil na múr vzhľadom na taktovanie procesora. Namiesto toho, pretože musíme rásť, sme začali baliť viac procesorov na jeden čip, ktorý sa často nazýva viacjadrové procesory. Ale aj tak nie je bežné počuť o procesoroch, ktoré majú viac ako 32 jadier.

Teraz vieme, že jedno jadro môže vykonávať naraz iba jedno vlákno alebo sadu pokynov. Počet procesov a vlákien však môže byť v stovkách, respektíve tisícoch. Ako to teda v skutočnosti funguje? Toto je kde operačný systém pre nás simuluje súbežnosť. Operačný systém to dosahuje krájanie času - čo v skutočnosti znamená, že procesor prepína medzi vláknami často, nepredvídateľne a nedeterministicky.

3. Problémy so súčasným programovaním

Keď ideme diskutovať o princípoch a vzoroch pre návrh súbežnej aplikácie, bolo by rozumné najskôr pochopiť, aké sú typické problémy.

Z veľkej časti zahŕňajú naše skúsenosti so súčasným programovaním pomocou natívnych vlákien so zdieľanou pamäťou. Preto sa zameriame na niektoré bežné problémy, ktoré z toho vyplývajú:

  • Vzájomné vylúčenie (synchronizačné primitívy): Vkladanie vlákna musia mať výlučný prístup do zdieľaného stavu alebo pamäte, aby sa zabezpečila správnosť programov. Synchronizácia zdieľaných zdrojov je populárna metóda na dosiahnutie vzájomného vylúčenia. K dispozícii je niekoľko synchronizačných primitívov, napríklad zámok, monitor, semafor alebo mutex. Programovanie vzájomného vylúčenia je však náchylné na chyby a často môže viesť k problémom s výkonom. S týmto súvisí niekoľko dobre diskutovaných problémov, ako je uviaznutie a živé zamykanie.
  • Prepínanie kontextu (ťažké vlákna): Každý operačný systém má natívnu, aj keď rôznorodú, podporu pre súbežné moduly ako proces a vlákno. Ako už bolo spomenuté, jednou zo základných služieb, ktoré operačný systém poskytuje, je plánovanie vlákien, ktoré sa majú vykonávať na obmedzenom počte procesorov prostredníctvom časového rozdelenia. Toto to v skutočnosti znamená vlákna sa často prepínajú medzi rôznymi stavmi. Pritom je potrebné uložiť a obnoviť ich súčasný stav. Toto je časovo náročná činnosť, ktorá priamo ovplyvňuje celkovú priepustnosť.

4. Návrhové vzory pre vysokú súbežnosť

Teraz, keď rozumieme základom súbežného programovania a bežným problémom v nich uvedeným, je čas porozumieť niektorým bežným vzorcom, ako sa týmto problémom vyhnúť. Musíme zopakovať, že súbežné programovanie je náročná úloha, ktorá si vyžaduje veľa skúseností. Nasledovanie niektorých zaužívaných vzorcov teda môže uľahčiť vykonanie úlohy.

4.1. Hercovská súbežnosť

Prvý návrh, o ktorom budeme diskutovať s ohľadom na súčasné programovanie, sa nazýva Actor Model. Toto je matematický model súbežných výpočtov, ktorý v zásade zaobchádza so všetkým ako s hercom. Herci si môžu navzájom odovzdávať správy a na ich základe môžu prijímať miestne rozhodnutia. Prvýkrát to navrhol Carl Hewitt a inšpirovalo ho množstvo programovacích jazykov.

Scalovým primárnym konštruktom pre súčasné programovanie sú herci. Herci sú bežné objekty v Scale, ktoré môžeme vytvoriť pomocou inštancie Herec trieda. Knižnica Scala Actors ďalej poskytuje mnoho užitočných operácií herca:

class myActor extends Actor {def act () {while (true) {receive {// Vykonať nejakú akciu}}}}

Vo vyššie uvedenom príklade je volanie na číslo prijímať metóda vnútri nekonečnej slučky pozastaví herca, kým nepríde správa. Po príchode je správa odstránená z hercovej poštovej schránky a sú podniknuté potrebné kroky.

Herecký model eliminuje jeden zo základných problémov súčasného programovania - zdieľanú pamäť. Herci komunikujú prostredníctvom správ a každý účastník spracováva správy zo svojich exkluzívnych poštových schránok postupne. Hercov však popravujeme prostredníctvom spoločného vlákna. A videli sme, že natívne vlákna môžu byť ťažké a preto môžu byť obmedzené počtom.

Tu nám samozrejme môžu pomôcť aj iné vzorce - tým sa budeme venovať neskôr!

4.2. Súbežnosť založená na udalostiach

Dizajnové riešenia založené na udalostiach výslovne riešia problém, že natívne vlákna sú nákladné na vytváranie a prevádzku. Jedným z návrhov založených na udalostiach je slučka udalostí. Smyčka udalostí funguje s poskytovateľom udalosti a sadou obslužných rutín udalostí. V tomto nastavení slučka udalostí blokuje poskytovateľa udalosti a po príchode odošle udalosť obsluhe udalosti.

Smyčka udalostí v podstate nie je nič iné ako dispečer udalostí! Samotná slučka udalostí môže bežať iba na jednom natívnom vlákne. Čo sa teda skutočne deje v slučke udalostí? Pozrime sa na príklad pseudokódu skutočne jednoduchej slučky udalostí:

while (true) {events = getEvents (); pre (e v udalostiach) processEvent (e); }

V podstate všetko, čo naša slučka udalostí robí, je neustále vyhľadávať udalosti a keď sa udalosti nájdu, spracovať ich. Prístup je skutočne jednoduchý, ale prináša výhody dizajnu založeného na udalostiach.

Budovanie súbežných aplikácií pomocou tohto dizajnu dáva aplikácii väčšiu kontrolu. Eliminuje tiež niektoré typické problémy viacvláknových aplikácií - napríklad zablokovanie.

JavaScript implementuje slučku udalostí, aby ponúkol asynchrónne programovanie. Udržuje zásobník hovorov na sledovanie všetkých vykonaných funkcií. Udržuje tiež front udalostí na odosielanie nových funkcií na spracovanie. Smyčka udalostí neustále kontroluje zásobník hovorov a pridáva nové funkcie z frontu udalostí. Všetky asynchronné volania sa odosielajú do webových rozhraní API, ktoré zvyčajne poskytuje prehliadač.

Samotná slučka udalostí môže bežať mimo jedného vlákna, ale webové rozhrania API poskytujú samostatné vlákna.

4.3. Neblokujúce algoritmy

V neblokujúcich algoritmoch pozastavenie jedného vlákna nevedie k pozastaveniu ďalších vlákien. Videli sme, že v našej aplikácii môžeme mať iba obmedzený počet natívnych vlákien. Teraz, algoritmus, ktorý blokuje vlákno, zjavne výrazne znižuje priepustnosť a bráni nám vo vytváraní veľmi súbežných aplikácií.

Neblokujúce algoritmy vždy využiť atómový primitív porovnania a výmeny, ktorý poskytuje základný hardvér. To znamená, že hardvér porovná obsah pamäťového miesta s danou hodnotou, a iba ak sú rovnaké, aktualizuje hodnotu na novú zadanú hodnotu. Môže to vyzerať jednoducho, ale efektívne nám poskytuje atómovú operáciu, ktorá by inak vyžadovala synchronizáciu.

To znamená, že musíme napísať nové dátové štruktúry a knižnice, ktoré využívajú túto atómovú operáciu. Toto nám dalo obrovskú sadu implementácií bez čakania a bez blokovania vo viacerých jazykoch. Java má niekoľko neblokujúcich dátových štruktúr ako AtomicBoolean, AtomicInteger, AtomicLonga Atómová referencia.

Zvážte aplikáciu, kde sa viac vlákien pokúša získať prístup k rovnakému kódu:

boolean open = false; if (! open) {// Urob niečo otvorené = false; }

Je zrejmé, že vyššie uvedený kód nie je bezpečný pre vlákna a jeho správanie v prostredí s viacerými vláknami môže byť nepredvídateľné. Naše možnosti sú buď synchronizácia tejto časti kódu so zámkom, alebo použitie atómovej operácie:

AtomicBoolean open = nový AtomicBoolean (false); if (open.compareAndSet (false, true) {// Urob niečo)

Ako vidíme, použitie neblokujúcej dátovej štruktúry ako AtomicBoolean pomáha nám písať kód bezpečný pre vlákna bez toho, aby sme sa zbavovali nevýhod zámkov!

5. Podpora v programovacích jazykoch

Videli sme, že existuje niekoľko spôsobov, ako môžeme zostaviť súbežný modul. Programovací jazyk síce robí zmeny, ale hlavne to, ako tento koncept podporuje základný operačný systém. Avšak ako súbežnosť založená na vláknach podporovaná natívnymi vláknami naráža na nové steny čo sa týka škálovateľnosti, vždy potrebujeme nové možnosti.

Implementácia niektorých dizajnérskych postupov, o ktorých sme hovorili v poslednej časti, sa ukazuje ako efektívna. Musíme si však uvedomiť, že programovanie ako také komplikuje. To, čo skutočne potrebujeme, je niečo, čo poskytuje silu súbežnosti založenej na vláknach bez nežiaducich účinkov, ktoré prináša.

Jedným z riešení, ktoré máme k dispozícii, sú zelené vlákna. Zelené vlákna sú vlákna, ktoré sú naplánované runtime knižnicou namiesto toho, aby boli natívne naplánované základným operačným systémom. Aj keď sa tým nezbavíte všetkých problémov v súbežnosti založenej na vláknach, určite nám to v niektorých prípadoch môže poskytnúť lepší výkon.

Teraz nie je nič ľahké používať zelené vlákna, pokiaľ to programovací jazyk, ktorý sme sa rozhodli použiť, nepodporuje. Nie každý programovací jazyk má túto zabudovanú podporu. Tiež to, čo voľne nazývame zelené vlákna, je možné implementovať veľmi jedinečnými spôsobmi v rôznych programovacích jazykoch. Pozrime sa na niektoré z týchto možností, ktoré máme k dispozícii.

5.1. Goroutiny v Go

Goroutiny v programovacom jazyku Go sú ľahké vlákna. Ponúkajú funkcie alebo metódy, ktoré môžu bežať súčasne s inými funkciami alebo metódami. Goroutíny sú extrémne lacné, pretože zaberajú iba niekoľko kilobajtov veľkosti zásobníka.

Najdôležitejšie je, že goroutiny sú multiplexované s menším počtom natívnych vlákien. Goroutíny navyše navzájom komunikujú pomocou kanálov, čím sa zabraňujú prístupu k zdieľanej pamäti. Dostávame skoro všetko, čo potrebujeme, a hádajte, čo - bez toho, aby ste niečo robili!

5.2. Procesy v Erlangu

V Erlangu sa každé vlákno vykonávania nazýva proces. Nie je to však celkom ako proces, o ktorom sme doteraz hovorili! Erlang procesy sú sú ľahké a majú malú pamäťovú stopu a rýchlo sa vytvárajú a likvidujú s nízkou réžiou plánovania.

Pod kapotou nie sú procesy Erlang nič iné ako funkcie, pre ktoré runtime spracováva plánovanie. Procesy Erlang navyše nezdieľajú žiadne údaje a navzájom komunikujú odovzdávaním správ. To je dôvod, prečo tieto procesy nazývame na prvom mieste!

5.3. Vlákna v Jave (návrh)

Príbeh súbežnosti s Javou bol nepretržitým vývojom. Java mala spočiatku podporu pre zelené vlákna, aspoň pre operačné systémy Solaris. To však bolo prerušené kvôli prekážkam presahujúcim rámec tohto tutoriálu.

Odvtedy je súbežnosť v Jave predovšetkým o natívnych vláknach a o tom, ako s nimi pracovať inteligentne! Ale zo zrejmých dôvodov môžeme v Jave čoskoro mať novú abstrakciu súbežnosti, ktorá sa bude nazývať vlákno. Projekt Loom navrhuje zaviesť pokračovania spolu s vláknami, čo môže zmeniť spôsob, akým píšeme súbežné aplikácie v Jave!

Toto je iba krátka informácia o tom, čo je k dispozícii v rôznych programovacích jazykoch. Existuje oveľa zaujímavejších spôsobov, ako sa iné programovacie jazyky pokúsili vyrovnať sa so súbežnosťou.

Okrem toho stojí za zmienku, že kombinácia návrhových vzorov diskutovaných v poslednej časti spolu s podporou programovacieho jazyka pre abstrakciu podobnú zeleným vláknam môže byť pri navrhovaní veľmi súbežných aplikácií mimoriadne silná.

6. Aplikácie s vysokou súbežnosťou

Aplikácia v reálnom svete má často viac komponentov, ktoré vzájomne komunikujú po drôte. Spravidla k nemu pristupujeme cez internet a skladá sa z viacerých služieb, ako sú proxy služba, brána, webová služba, databáza, adresárová služba a súborové systémy.

Ako zabezpečíme v týchto situáciách vysokú súbežnosť? Poďme preskúmať niektoré z týchto vrstiev a možnosti, ktoré máme pri vytváraní vysoko súbežnej aplikácie.

Ako sme videli v predchádzajúcej časti, kľúčom k vytváraniu aplikácií s vysokou súbežnosťou je použitie niektorých tu diskutovaných koncepcií návrhu. Musíme zvoliť správny softvér pre prácu, ktorý už niektoré z týchto postupov obsahuje.

6.1. Webová vrstva

Web je zvyčajne prvou vrstvou, kam prichádzajú požiadavky používateľov, a zabezpečenie vysokej súbežnosti je tu nevyhnutné. Pozrime sa, aké sú niektoré z možností:

  • Uzol (nazýva sa tiež NodeJS alebo Node.js) je runtime JavaScript s otvoreným zdrojovým kódom pre rôzne platformy postavený na stroji JavaScript V8 prehliadača Chrome. Uzol funguje celkom dobre pri spracovaní asynchrónnych I / O operácií. Dôvod, prečo to Uzol robí tak dobre, je ten, že implementuje slučku udalostí do jedného vlákna. Smyčka udalostí pomocou spätných volaní spracováva všetky blokovacie operácie ako I / O asynchrónne.
  • nginx je webový server s otvoreným zdrojovým kódom, ktorý bežne používame ako reverzný proxy server medzi jeho ďalšie použitie. Dôvod, prečo nginx poskytuje vysokú súbežnosť, je ten, že používa asynchrónny prístup založený na udalostiach. nginx pracuje s hlavným procesom v jednom vlákne. Hlavný proces udržiava pracovné procesy, ktoré vykonávajú skutočné spracovanie. Pracovné procesy teda spracúvajú každú požiadavku súčasne.

6.2. Aplikačná vrstva

Pri navrhovaní aplikácie existuje niekoľko nástrojov, ktoré nám pomáhajú budovať vysokú súbežnosť. Pozrime sa na niekoľko z týchto knižníc a rámcov, ktoré máme k dispozícii:

  • Akka je sada nástrojov napísaná v Scale na vytváranie vysoko súbežných a distribuovaných aplikácií na JVM. Akkov prístup k riešeniu súbežnosti je založený na hereckom modeli, o ktorom sme už hovorili. Akka vytvára vrstvu medzi aktérmi a základnými systémami. Rámec sa zaoberá zložitosťou vytvárania a plánovania vlákien, prijímania a odosielania správ.
  • Projektový reaktor je reaktívna knižnica na vytváranie neblokujúcich aplikácií na JVM. Je založený na špecifikácii Reactive Streams a zameriava sa na efektívne odovzdávanie správ a správu požiadaviek (protitlak). Prevádzkovatelia reaktorov a plánovači môžu udržiavať vysokú priepustnosť správ. Niekoľko populárnych rámcov poskytuje implementácie reaktorov, vrátane Spring WebFlux a RSocket.
  • Netty je asynchrónny sieťový aplikačný rámec založený na udalostiach. Môžeme použiť Netty na vývoj vysoko súbežných protokolových serverov a klientov. Netty využíva NIO, čo je kolekcia Java API, ktorá ponúka asynchrónny prenos dát cez vyrovnávaciu pamäť a kanály. Ponúka nám niekoľko výhod, ako je lepšia priepustnosť, nižšia latencia, menšia spotreba zdrojov a minimalizácia zbytočného kopírovania pamäte.

6.3. Dátová vrstva

Nakoniec nie je žiadna aplikácia úplná bez jej údajov a údaje pochádzajú z trvalého úložiska. Keď hovoríme o vysokej súbežnosti s ohľadom na databázy, väčšina pozornosti sa sústredí na rodinu NoSQL. Je to primárne kvôli lineárnej škálovateľnosti, ktorú môžu databázy NoSQL ponúkať, ale je ťažké ju dosiahnuť v relačných variantoch. Pozrime sa na dva populárne nástroje pre dátovú vrstvu:

  • Cassandra je bezplatná distribuovaná databáza NoSQL s otvoreným zdrojovým kódom ktorá poskytuje vysokú dostupnosť, vysokú škálovateľnosť a odolnosť voči chybám na komoditnom hardvéri. Cassandra však neposkytuje ACID transakcie pokrývajúce viac tabuliek. Takže ak naša aplikácia nevyžaduje silnú konzistenciu a transakcie, môžeme ťažiť z operácií Cassandry s nízkou latenciou.
  • Kafka je distribuovaná streamovacia platforma. Kafka ukladá prúd záznamov do kategórií zvaných témy. Môže poskytnúť lineárnu horizontálnu škálovateľnosť tak pre výrobcov, ako aj pre spotrebiteľov záznamov, a zároveň poskytuje vysokú spoľahlivosť a trvanlivosť. Oddiely, repliky a sprostredkovatelia sú niektoré zo základných konceptov, na ktorých poskytuje hromadne distribuovanú súbežnosť.

6.4. Cache Layer

Žiadna webová aplikácia v modernom svete, ktorá sa zameriava na vysokú súbežnosť, si nemôže dovoliť zakaždým naraziť do databázy. To nám zostáva na výber medzipamäte - najlepšie medzipamäte v pamäti, ktorá podporuje naše vysoko súčasné aplikácie:

  • Hazelcast je distribuovaný, cloudovo vhodný obchod v pamäti a výpočtový stroj, ktorý podporuje širokú škálu dátových štruktúr ako napr Mapa, Nastaviť, Zoznam, MultiMap, RingBuffera HyperLogLog. Má zabudovanú replikáciu a ponúka vysokú dostupnosť a automatické rozdelenie na oddiely.
  • Redis je úložisko dátovej štruktúry v pamäti, ktoré primárne používame ako vyrovnávacia pamäť. Poskytuje databázu kľúčov a hodnôt v pamäti s voliteľnou odolnosťou.Medzi podporované dátové štruktúry patria reťazce, hodnoty hash, zoznamy a množiny. Redis má zabudovanú replikáciu a ponúka vysokú dostupnosť a automatické rozdelenie na oddiely. V prípade, že nepotrebujeme vytrvalosť, Redis nám môže ponúknuť sieťovo vyrovnanú medzipamäť bohatú na funkcie s vynikajúcim výkonom.

Samozrejme, sotva sme poškriabali povrch toho, čo máme k dispozícii v našej snahe vybudovať vysoko súbežnú aplikáciu. Je dôležité poznamenať, že pri vytváraní vhodného dizajnu by nás mala viesť viac ako dostupný softvér. Niektoré z týchto možností môžu byť vhodné, zatiaľ čo iné nemusia byť vhodné.

A nezabúdajme, že existuje veľa ďalších možností, ktoré môžu lepšie vyhovovať našim požiadavkám.

7. Záver

V tomto článku sme diskutovali o základoch súbežného programovania. Pochopili sme niektoré základné aspekty súbežnosti a problémy, ku ktorým môže viesť. Ďalej sme prešli niektorými návrhovými vzormi, ktoré nám môžu pomôcť vyhnúť sa typickým problémom pri súbežnom programovaní.

Nakoniec sme prešli cez niektoré z rámcov, knižníc a softvéru, ktoré máme k dispozícii na vytvorenie vysoko súbežnej aplikácie typu end-to-end.