Čau Habr!
Vzhľadom na aktuálne udalosti spôsobené koronavírusom začalo viacero internetových služieb pociťovať zvýšenú záťaž. Napríklad , pretože nebola dostatočná kapacita. A nie vždy je možné zrýchliť server jednoduchým pridaním výkonnejšieho hardvéru, ale požiadavky zákazníkov musia byť spracované (inak pôjdu ku konkurencii).
V tomto článku stručne rozoberiem populárne postupy, ktoré vám pomôžu vybudovať rýchlu a chybám odolnú službu. Z možných vývojových schém som však vybral iba tie, ktoré sa v súčasnosti používajú. jednoduché použitiePre každú položku máte buď hotové knižnice, alebo môžete problém vyriešiť pomocou cloudovej platformy.
Horizontálne škálovanie
Toto je najjednoduchší a najznámejší bod. Vo všeobecnosti sú dve najbežnejšie schémy rozloženia zaťaženia horizontálne a vertikálne škálovanie. Umožníte službám bežať paralelne, čím medzi ne rozložíte záťaž. Objednáte si výkonnejšie servery alebo optimalizujete kód.
Ako príklad si vezmem abstraktné cloudové úložisko súborov, teda nejaký analóg OwnCloud, OneDrive a podobne.
Typický diagram takejto schémy je uvedený nižšie, ale len demonštruje zložitosť systému. Koniec koncov, musíme nejako synchronizovať služby. Čo sa stane, ak si používateľ uloží súbor z tabletu a potom si ho chce pozrieť na telefóne?

Rozdiel medzi týmito prístupmi spočíva v tom, že pri vertikálnom škálovaní sme pripravení zvýšiť kapacitu uzlov, zatiaľ čo pri horizontálnom škálovaní sme pripravení pridať nové uzly na rozloženie záťaže.
CQRS
Toto je pomerne dôležitý vzorec, pretože umožňuje rôznym klientom nielen pripojiť sa k rôznym službám, ale aj prijímať rovnaké streamy udalostí. Jeho výhody nie sú také zrejmé pre jednoduchú aplikáciu, ale je mimoriadne dôležitý (a jednoduchý) pre vyťaženú službu. Jeho podstatou je, že prichádzajúce a odchádzajúce dátové toky by sa nemali pretínať. To znamená, že nemôžete odoslať požiadavku a očakávať odpoveď; namiesto toho odošlete požiadavku službe A, ale dostanete odpoveď od služby B.
Prvou výhodou tohto prístupu je schopnosť prerušiť spojenia (v širšom zmysle slova) počas dlhého dotazu. Vezmime si napríklad viac-menej štandardnú postupnosť:
- Klient odoslal požiadavku na server.
- Server začal dlhé spracovanie.
- Server odpovedal klientovi s výsledkom.
Predstavme si, že v kroku 2 bolo pripojenie prerušené (buď sa sieť znova pripojila, alebo používateľ prešiel na inú stránku, čím sa pripojenie prerušilo). V tomto prípade by bolo pre server ťažké odoslať používateľovi odpoveď s informáciou o tom, čo presne bolo spracované. Pri použití CQRS by bola postupnosť mierne odlišná:
- Klient sa prihlásil na odber aktualizácií.
- Klient odoslal požiadavku na server.
- Server odpovedal „žiadosť prijatá“.
- Server odpovedal s výsledkom cez kanál z bodu „1“.

Ako vidíte, schéma je o niečo zložitejšia. Okrem toho tu chýba intuitívny prístup typu požiadavka-odpoveď. Ako však vidíte, prerušenie pripojenia počas spracovania požiadavky nespôsobí chybu. Okrem toho, ak je používateľ skutočne pripojený k službe z viacerých zariadení (napríklad z mobilného telefónu a tabletu), je možné nastaviť, aby sa odpoveď odoslala na obe zariadenia.
Je zaujímavé, že kód na spracovanie prichádzajúcich správ sa stáva rovnakým (nie 100 %) pre udalosti ovplyvnené samotným klientom aj pre iné udalosti vrátane udalostí od iných klientov.
V skutočnosti však získavame ďalšie výhody zo skutočnosti, že jednosmerný tok je možné spracovať funkčne (pomocou RX a podobných metód). To je významná výhoda, pretože aplikáciu je možné v podstate urobiť plne reaktívnou, a to aj pomocou funkčného prístupu. Pri rozsiahlych programoch to môže výrazne znížiť zdroje na vývoj a podporu.
Kombinácia tohto prístupu s horizontálnym škálovaním poskytuje ďalšiu výhodu v podobe možnosti odosielať požiadavky na jeden server a prijímať odpovede z iného. To umožňuje klientovi vybrať si preferovanú službu, zatiaľ čo základný systém dokáže stále správne spracovávať udalosti.
Event Sourcing
Ako viete, jednou z kľúčových vlastností distribuovaného systému je absencia zdieľaného času alebo zdieľanej kritickej sekcie. Pre jeden proces môžete implementovať synchronizáciu (pomocou mutexov), čím zabezpečíte, že nikto iný nevykonáva rovnaký kód. Pre distribuovaný systém je to však nebezpečné, pretože to vyžaduje réžiu a marí účel škálovateľnosti – všetky komponenty budú stále čakať na ten istý.
To nás vedie k dôležitému faktu: rýchly distribuovaný systém nemožno synchronizovať, pretože by to znížilo výkon. Na druhej strane často potrebujeme určitý stupeň konzistencie medzi komponentmi. A na to môžeme použiť prístup s , kde je zaručené, že ak nedôjde k žiadnym zmenám údajov v určitom časovom období od poslednej aktualizácie („prípadne“), všetky dotazy vrátia poslednú aktualizovanú hodnotu.
Je dôležité pochopiť, že pre klasické databázy sa pomerne často používa , kde každý uzol má rovnaké informácie (toto sa často dosiahne, keď sa transakcia považuje za vykonanú až po odpovedi druhého servera). Existujú tu určité úľavy kvôli úrovniam izolácie, ale všeobecná myšlienka zostáva rovnaká – môžete žiť v úplne konzistentnom svete.
Ale vráťme sa k pôvodnému problému. Ak je možné časť systému zostaviť s , potom je možné zostrojiť nasledujúci diagram.

Dôležité vlastnosti tohto prístupu:
- Každá prichádzajúca požiadavka je umiestnená do jedného frontu.
- Počas spracovania požiadavky môže služba umiestniť úlohy aj do iných frontov.
- Každá prichádzajúca udalosť má ID (ktoré je potrebné na deduplikáciu).
- Front funguje na princípe „iba pridávanie“. Prvky nie je možné odstrániť ani zmeniť ich poradie.
- Front funguje na princípe FIFO (prvý dnu, prvý von). Ak je potrebné paralelné vykonávanie, objekty by sa mali presúvať do rôznych frontov naraz.
Dovoľte mi pripomenúť, že zvažujeme online ukladanie súborov. V tomto prípade by systém vyzeral asi takto:

Je dôležité poznamenať, že služby v diagrame nemusia nevyhnutne predstavovať samostatné servery. Môžu dokonca zdieľať rovnaký proces. Dôležité je, že tieto veci sú ideologicky oddelené takým spôsobom, že horizontálne škálovanie je možné ľahko implementovať.
A pre dvoch používateľov bude diagram vyzerať takto (služby určené pre rôznych používateľov sú označené rôznymi farbami):

Bonusy z takejto kombinácie:
- Služby spracovania informácií sú oddelené. Fronty sú tiež oddelené. Ak potrebujeme zvýšiť priepustnosť systému, jednoducho musíme spustiť viac služieb na viacerých serveroch.
- Keď od používateľa dostaneme informácie, nemusíme čakať na úplné uloženie údajov. Namiesto toho jednoducho odpovieme „OK“ a potom postupne začneme spracovávať. Front tiež vyhladzuje špičky, pretože pridávanie nového objektu prebieha rýchlo a používateľ nemusí čakať na dokončenie celého cyklu.
- Ako príklad som pridal službu deduplikácie, ktorá sa pokúša zlúčiť identické súbory. Ak to v 1 % prípadov trvá dlho, klient si to sotva všimne (pozri vyššie), čo je veľké plus, pretože už nevyžadujeme 100 % rýchlosť a spoľahlivosť.
Nevýhody sú však okamžite zjavné:
- Náš systém už nemá striktnú konzistenciu. To znamená, že napríklad, ak sa prihlásite na odber rôznych služieb, teoreticky by ste mohli dostávať rôzne stavy (pretože jedna zo služieb nemusí mať čas prijať oznámenie z interného frontu). Ďalším dôsledkom je, že systém už nemá spoločný čas. To napríklad znamená, že nie je možné zoradiť všetky udalosti jednoducho podľa času príchodu, pretože hodiny medzi servermi nemusia byť synchronizované (v skutočnosti mať rovnaký čas na dvoch serveroch je utópia).
- Žiadne udalosti sa teraz nedajú jednoducho vrátiť späť (ako je to možné v prípade databázy). Namiesto toho sa musí pridať nová udalosť – , čo zmení posledný stav na požadovaný. Ako príklad z podobnej oblasti: bez prepisovania histórie (čo je v niektorých prípadoch zlé) nemôžete v Gite vrátiť späť commit, ale môžete vytvoriť špeciálny , čo v podstate len vráti starý stav. V histórii sa však zachovajú chybný commit aj vrátenie zmien.
- Schéma údajov sa môže z vydania na vydanie meniť, ale staré udalosti už nebude možné aktualizovať na nový štandard (pretože udalosti sa v princípe nedajú zmeniť).
Ako vidíte, Event Sourcing funguje dobre s CQRS. Navyše, implementácia systému s efektívnymi a pohodlnými frontami bez oddelenia tokov údajov je inherentne náročná, pretože by si vyžadovala pridanie synchronizačných bodov, ktoré by negovali pozitívne účinky frontov. Súčasné použitie oboch prístupov si vyžaduje menšie úpravy kódu programu. V našom prípade pri odoslaní súboru na server odpoveď vráti iba „OK“, čo jednoducho znamená „operácia pridania súboru bola uložená“. Technicky to neznamená, že údaje sú už dostupné na iných zariadeniach (napríklad služba deduplikácie môže znovu vytvoriť index). Po chvíli však klient dostane upozornenie s textom „Súbor X bol uložený“.
V dôsledku toho:
- Počet stavov odosielania súborov sa zvyšuje: namiesto klasického „súbor odoslaný“ teraz vidíme dva: „súbor pridaný do frontu servera“ a „súbor uložený do úložiska“. To druhé znamená, že súbor môžu začať prijímať aj iné zariadenia (s výhradou, že fronty fungujú rôznymi rýchlosťami).
- Keďže informácie o odoslaní teraz prichádzajú rôznymi kanálmi, musíme vyvinúť riešenia na získanie stavu spracovania súboru. V dôsledku toho, na rozdiel od klasickej metódy požiadavky a odpovede, je možné klienta reštartovať počas spracovania súboru, ale stav spracovania zostane správny. Navyše, táto funkcia v podstate funguje ihneď po vybalení z krabice. Vďaka tomu sme teraz tolerantnejší k zlyhaniam.
Črepovanie
Ako je popísané vyššie, systémom so zdrojovými prvkami udalostí chýba striktná konzistencia. To znamená, že môžeme používať viacero úložných zariadení bez akejkoľvek synchronizácie medzi nimi. Pri prístupe k našej úlohe môžeme:
- Oddeľte súbory podľa typu. Napríklad obrázky/videá je možné dekódovať a vybrať efektívnejší formát.
- Oddeľte účty podľa krajiny. Mnohé zákony to môžu vyžadovať, ale táto architektúra to umožňuje automaticky.

Ak chcete migrovať dáta z jedného úložiska do druhého, štandardné nástroje vám nepostačia. V tomto prípade však musíte front udalostí zastaviť, vykonať migráciu a potom ho reštartovať. Vo všeobecnosti nie je možné migrovať dáta „za chodu“. Ak je však front udalostí uložený v celom rozsahu a máte snímky predchádzajúcich stavov úložiska, môžete udalosti prehrať takto:
- V zdroji udalostí má každá udalosť svoj vlastný identifikátor (ideálne neklesajúci). To znamená, že do úložiska môžeme pridať pole – ID posledného spracovaného prvku.
- Rad duplikujeme, aby sa všetky udalosti mohli spracovať na viacerých nezávislých úložiskách (prvé je to, ktoré momentálne ukladá dáta, a druhé je nové, ale momentálne prázdne). Druhý rad sa, prirodzene, momentálne nespracováva.
- Spustíme druhý front (to znamená, že začneme prehrávať udalosti).
- Keď je nový front relatívne prázdny (t. j. priemerný časový rozdiel medzi pridaním prvku a jeho načítaním je prijateľný), čitatelia môžu začať prepínať do nového úložiska.
Ako vidíte, náš systém nikdy nemal a stále nemá striktnú konzistenciu. Existuje iba eventuálna konzistencia, ktorá zaručuje, že udalosti sú spracované v rovnakom poradí (hoci možno s rôznymi latenciami). Vďaka tomu môžeme relatívne ľahko preniesť dáta na druhú stranu sveta bez zastavenia systému.
Takže, pokračujúc v našom príklade online ukladania súborov, táto architektúra nám už poskytuje množstvo bonusov:
- Dokážeme dynamicky presúvať objekty bližšie k používateľom, a tým zlepšovať kvalitu služieb.
- Niektoré dáta vieme ukladať v rámci spoločností. Napríklad, firemní používatelia často vyžadujú, aby boli ich dáta uložené v kontrolovaných dátových centrách (aby sa predišlo únikom dát). Pomocou shardingu to vieme jednoducho zabezpečiť. Táto úloha sa ešte viac zjednoduší, ak má zákazník kompatibilný cloud (napr. ).
- A čo je najdôležitejšie, nemusíme to robiť. Veď na začiatok by bolo úplne v poriadku jedno úložisko pre všetky účty (pre rýchlejší štart). A kľúčovou vlastnosťou tohto systému je, že hoci je rozšíriteľný, je na začiatku celkom jednoduchý. Len nemusíte hneď písať kód, ktorý spracováva milión samostatných nezávislých frontov atď. V prípade potreby to môžeme urobiť neskôr.
Hosting statického obsahu
Tento bod sa môže zdať úplne zrejmý, ale stále je nevyhnutný pre viac-menej štandardnú aplikáciu s vysokou záťažou. Jeho podstata je jednoduchá: všetok statický obsah nie je poskytovaný z toho istého servera, kde sa nachádza aplikácia, ale zo špeciálnych serverov určených špeciálne na túto úlohu. Vďaka tomu sa tieto operácie vykonávajú rýchlejšie (napríklad Nginx poskytuje súbory rýchlejšie a za nižšie náklady ako server Java). Navyše je tu architektúra CDN () nám umožňuje umiestniť naše súbory bližšie ku koncovým používateľom, čo má pozitívny vplyv na jednoduchosť používania služby.
Najjednoduchším a najštandardnejším príkladom statického obsahu je kolekcia skriptov a obrázkov pre webovú stránku. Je to priamočiare – sú vopred známe a archív sa potom nahrá na CDN servery, odkiaľ sa poskytuje koncovým používateľom.
V praxi sa však pre statický obsah dá použiť prístup, ktorý je trochu podobný lambda architektúre. Vráťme sa k našej úlohe (online úložisko súborov), v ktorej potrebujeme distribuovať súbory používateľom. Najjednoduchším a priamočiarym riešením je vytvoriť službu, ktorá vykoná všetky potrebné kontroly (autorizáciu atď.) pre každú požiadavku používateľa a potom stiahne súbor priamo z nášho úložiska. Hlavnou nevýhodou tohto prístupu je, že statický obsah (a súbor s konkrétnou revíziou je v podstate statický obsah) je obsluhovaný tým istým serverom, ktorý obsahuje obchodnú logiku. Namiesto toho môžeme použiť nasledujúcu schému:
- Server poskytuje URL adresu na stiahnutie. Môže mať tvar file_id + key, kde key je miniatúrny digitálny podpis, ktorý udeľuje prístup k zdroju počas nasledujúcich 24 hodín.
- Distribúciu súborov zabezpečuje jednoduchý nginx s nasledujúcimi možnosťami:
- Ukladanie obsahu do vyrovnávacej pamäte. Keďže túto službu je možné hostiť na samostatnom serveri, zabezpečili sme budúcnosť uložením všetkých nedávno stiahnutých súborov na disk.
- Kontrola kľúča pri vytváraní pripojenia
- Voliteľné: spracovanie streamovaného obsahu. Napríklad, ak komprimujeme všetky súbory v službe, môžeme ich dekomprimovať priamo v tomto module. V dôsledku toho sa operácie IO vykonávajú tam, kde patria. Archivátor Java by ľahko alokoval veľa nepotrebnej pamäte, ale prepísanie služby s obchodnou logikou v konvenčnom Rust/C++ by mohlo byť tiež neefektívne. V našom prípade používame rôzne procesy (alebo dokonca služby), čo znamená, že môžeme efektívne oddeliť obchodnú logiku a operácie IO.

Táto schéma sa celkom nepodobá poskytovaniu statického obsahu (keďže niekam nenahrávame celý statický balík), ale v skutočnosti tento prístup presne poskytuje nemenné dáta. Navyše, túto schému možno zovšeobecniť na iné prípady, kde obsah nie je jednoducho statický, ale možno ho reprezentovať ako súbor nemenných a neodstrániteľných blokov (hoci ich možno pridať).
Tu je ďalší príklad (na upevnenie): ak ste pracovali s Jenkins alebo TeamCity, viete, že obe riešenia sú napísané v jazyku Java. Obe sú procesy Java, ktoré sa zaoberajú orchestráciou zostavovania aj správou obsahu. Konkrétne obe majú úlohy ako „prenos súboru/priečinka zo servera“. Medzi príklady patrí doručovanie artefaktov, prenos zdrojového kódu (keď agent nestiahne kód priamo z repozitára, ale server to urobí za neho) a prístup k protokolom. Všetky tieto úlohy majú rôzne zaťaženie I/O. To znamená, že server zodpovedný za komplexnú obchodnú logiku musí byť tiež schopný efektívne presúvať cez seba veľké dátové toky. A čo je najzaujímavejšie, túto operáciu je možné delegovať na Nginx pomocou úplne rovnakej schémy (okrem toho, že k požiadavke je potrebné pridať dátový kľúč).
Ak sa však vrátime k nášmu systému, dostaneme podobnú schému:

Ako vidíte, systém sa radikálne skomplikoval. Už to nie je len miniproces, ktorý lokálne ukladá súbory. Teraz si vyžaduje komplexnú podporu, správu verzií API atď. Preto je po nakreslení všetkých diagramov najlepšie dôkladne zhodnotiť, či sa investícia do škálovateľnosti oplatí. Ak však chcete systém rozšíriť (vrátane spracovania ešte väčšieho počtu používateľov), budete sa musieť uchýliť k takýmto riešeniam. Architektúra systému je však vďaka tomu pripravená na zvýšené zaťaženie (prakticky každý komponent je možné klonovať pre horizontálne škálovanie). Systém je možné aktualizovať bez jeho vypnutia (niektoré operácie budú jednoducho mierne pomalšie).
Ako som spomenul hneď na začiatku, množstvo online služieb momentálne zaznamenáva zvýšené zaťaženie. A niektoré z nich jednoducho prestali správne fungovať. V podstate sa systémy zrútili práve v momente, keď by firmy mali zarábať. Takže namiesto odkladu doručenia, namiesto toho, aby zákazníkom ponúkol „naplánujte si doručenie na najbližších pár mesiacov“, systém jednoducho povedal: „Choďte ku konkurencii.“ To je v skutočnosti cena za nízku produktivitu: straty vznikajú práve vtedy, keď by zisky boli najvyššie.
Záver
Všetky tieto prístupy existujú už nejaký čas. Napríklad VK už dlho využíva myšlienku statického hostingu obsahu na poskytovanie obrázkov. Mnoho online hier používa sharding na oddelenie hráčov podľa regiónu alebo na oddelenie herných lokácií (ak je samotný svet zjednotený). V e-mailoch sa aktívne používa systém event sourcing. Väčšina obchodných aplikácií, ktoré nepretržite prijímajú údaje, je v skutočnosti postavená na prístupe CQRS na filtrovanie prichádzajúcich údajov. A horizontálne škálovanie sa už dlho používa v mnohých službách.
Najdôležitejšie však je, že všetky tieto vzory sa stali veľmi ľahko aplikovateľnými v moderných aplikáciách (samozrejme, ak sú vhodné). Cloud computing ponúka sharding a horizontálne škálovanie ihneď po vybalení z krabice, čo je oveľa jednoduchšie ako objednávanie samostatných dedikovaných serverov v rôznych dátových centrách. CQRS sa stal oveľa jednoduchším, už len vďaka vývoju knižníc ako RX. Pred desiatimi rokmi by to len málo webových stránok dokázalo podporiť. Event Sourcing sa tiež neuveriteľne ľahko nastavuje vďaka hotovým kontajnerom Apache Kafka. Pred desiatimi rokmi by to bolo inovatívne; teraz je to bežné. Podobne aj pri Static Content Hostingu, užívateľsky prívetivejšie technológie (vrátane podrobnej dokumentácie a veľkej databázy odpovedí) tento prístup ešte viac zjednodušili.
V dôsledku toho sa implementácia viacerých pomerne zložitých architektonických vzorov stala oveľa jednoduchšou, čo znamená, že sa oplatí zvážiť ich včas. Zatiaľ čo desaťročná aplikácia mohla opustiť jedno z vyššie uvedených riešení kvôli vysokým implementačným a prevádzkovým nákladom, nová aplikácia alebo možno refaktoring teraz dokáže vytvoriť službu, ktorá je architektonicky rozšíriteľná (z hľadiska výkonu) a pripravená na nové požiadavky klientov (napríklad na lokalizáciu osobných údajov).
A čo je najdôležitejšie: prosím, nepoužívajte tieto prístupy, ak máte jednoduchú aplikáciu. Áno, sú krásne a zaujímavé, ale pre stránku s maximálnou návštevnosťou 100 ľudí môže často postačovať klasický monolit (aspoň zvonka; vnútorne sa dá všetko rozdeliť na moduly atď.).
Zdroj: hab.com
