Pohodlné architektonické vzory

Čau Habr!

Ve světle aktuálního dění v souvislosti s koronavirem se řada internetových služeb začala více zatěžovat. Například, Jeden z maloobchodních řetězců ve Spojeném království jednoduše zastavil své online objednávkové stránky., protože nebyla dostatečná kapacita. A ne vždy je možné zrychlit server pouhým přidáním výkonnějšího zařízení, ale požadavky klientů musí být zpracovány (nebo půjdou ke konkurenci).

V tomto článku stručně pohovořím o oblíbených postupech, které vám umožní vytvořit rychlou a bezchybnou službu. Z možných vývojových schémat jsem však vybral pouze ta, která aktuálně jsou snadné použití. Pro každou položku máte buď hotové knihovny, nebo máte možnost problém vyřešit pomocí cloudové platformy.

Horizontální škálování

Nejjednodušší a nejznámější bod. Obvykle jsou nejběžnější dvě schémata rozložení zatížení horizontální a vertikální. V prvním případě umožňujete paralelnímu běhu služeb, čímž mezi ně rozdělujete zátěž. Ve druhé objednáváte výkonnější servery nebo optimalizujete kód.

Vezmu například abstraktní cloudové úložiště souborů, to znamená nějaký analog OwnCloud, OneDrive a tak dále.

Standardní obrázek takového obvodu je níže, ale pouze demonstruje složitost systému. Potřebujeme přece nějak synchronizovat služby. Co se stane, když uživatel uloží soubor z tabletu a poté si jej bude chtít prohlédnout z telefonu?

Pohodlné architektonické vzory
Rozdíl mezi přístupy: ve vertikálním škálování jsme připraveni zvýšit výkon uzlů a v horizontálním škálování jsme připraveni přidat nové uzly pro rozložení zátěže.

CQRS

Oddělení odpovědnosti za příkazový dotaz Poměrně důležitý vzorec, protože umožňuje různým klientům nejen se připojit k různým službám, ale také přijímat stejné toky událostí. Jeho výhody nejsou u jednoduché aplikace tak zřejmé, ale u vytížené služby je nesmírně důležité (a jednoduché). Jeho podstata: příchozí a odchozí datové toky by se neměly křížit. To znamená, že nemůžete odeslat požadavek a očekávat odpověď; místo toho odešlete požadavek službě A, ale obdržíte odpověď od služby B.

Prvním bonusem tohoto přístupu je možnost přerušit spojení (v širokém slova smyslu) při provádění dlouhého požadavku. Vezměme si například víceméně standardní sekvenci:

  1. Klient odeslal požadavek na server.
  2. Server spustil dlouhou dobu zpracování.
  3. Server odpověděl klientovi s výsledkem.

Představme si, že v bodě 2 bylo spojení přerušeno (nebo se síť znovu připojila, nebo uživatel přešel na jinou stránku a spojení přerušil). V tomto případě bude pro server obtížné odeslat uživateli odpověď s informací o tom, co přesně bylo zpracováno. Při použití CQRS se bude sekvence mírně lišit:

  1. Klient se přihlásil k odběru aktualizací.
  2. Klient odeslal požadavek na server.
  3. Server odpověděl „žádost přijata“.
  4. Server odpověděl s výsledkem přes kanál z bodu „1“.

Pohodlné architektonické vzory

Jak vidíte, schéma je trochu složitější. Navíc zde chybí intuitivní přístup žádost-odpověď. Jak však vidíte, přerušení připojení během zpracování požadavku nevede k chybě. Navíc, pokud je uživatel skutečně připojen ke službě z více zařízení (například z mobilního telefonu a z tabletu), můžete zajistit, aby odpověď přišla na obě zařízení.

Zajímavé je, že kód pro zpracování příchozích zpráv se stává stejný (nikoli 100%) jak pro události, které byly ovlivněny samotným klientem, tak pro další události, včetně těch od jiných klientů.

Ve skutečnosti však získáme další bonus díky tomu, že jednosměrné proudění lze zvládnout funkčním stylem (pomocí RX a podobně). A to je již vážné plus, protože v podstatě může být aplikace zcela reaktivní a také pomocí funkčního přístupu. U tukových programů to může výrazně ušetřit rozvojové a podpůrné zdroje.

Pokud tento přístup zkombinujeme s horizontálním škálováním, pak jako bonus získáme možnost posílat požadavky na jeden server a přijímat odpovědi od jiného. Klient si tak může vybrat službu, která mu vyhovuje, a systém uvnitř bude stále schopen události správně zpracovávat.

Sourcing událostí

Jak víte, jedním z hlavních rysů distribuovaného systému je absence společného času, společného kritického úseku. Pro jeden proces můžete provést synchronizaci (na stejných mutexech), v rámci které máte jistotu, že tento kód nikdo jiný nespouští. To je však pro distribuovaný systém nebezpečné, protože to bude vyžadovat režii a také zabije všechnu krásu škálování - všechny komponenty budou stále čekat na jednu.

Odtud dostáváme důležitý fakt – rychlý distribuovaný systém nelze synchronizovat, protože pak snížíme výkon. Na druhou stranu často potřebujeme určitou konzistenci mezi komponentami. A k tomu můžete použít přístup s případná konzistence, kde je zaručeno, že pokud nedojde po určitou dobu od poslední aktualizace ke změnám dat („nakonec“), všechny dotazy vrátí poslední aktualizovanou hodnotu.

Je důležité pochopit, že pro klasické databáze se poměrně často používá silná konzistence, kde má každý uzel stejné informace (toho je často dosaženo v případě, kdy je transakce považována za zavedenou až poté, co odpoví druhý server). Jsou zde určité relaxace díky úrovním izolace, ale obecná myšlenka zůstává stejná - můžete žít ve zcela harmonizovaném světě.

Vraťme se však k původnímu úkolu. Pokud lze sestavit část systému případná konzistence, pak můžeme sestavit následující diagram.

Pohodlné architektonické vzory

Důležité vlastnosti tohoto přístupu:

  • Každý příchozí požadavek je umístěn do jedné fronty.
  • Při zpracování požadavku může služba také umístit úkoly do jiných front.
  • Každá příchozí událost má identifikátor (který je nezbytný pro deduplikaci).
  • Fronta ideologicky funguje podle schématu „pouze připojit“. Nemůžete z něj odebírat prvky ani je přeskupovat.
  • Fronta funguje podle schématu FIFO (omlouvám se za tautologii). Pokud potřebujete provádět paralelní provádění, měli byste v jedné fázi přesunout objekty do různých front.

Připomínám, že zvažujeme případ online ukládání souborů. V tomto případě bude systém vypadat nějak takto:

Pohodlné architektonické vzory

Je důležité, že služby v diagramu nemusí nutně znamenat samostatný server. Dokonce i proces může být stejný. Další věc je důležitá: ideologicky jsou tyto věci odděleny tak, že horizontální škálování lze snadno aplikovat.

A pro dva uživatele bude diagram vypadat takto (služby určené pro různé uživatele jsou označeny různými barvami):

Pohodlné architektonické vzory

Bonusy z takové kombinace:

  • Služby zpracování informací jsou odděleny. Odděleny jsou i fronty. Pokud potřebujeme zvýšit propustnost systému, pak stačí spustit více služeb na více serverech.
  • Když obdržíme informace od uživatele, nemusíme čekat, až se data úplně uloží. Naopak, stačí odpovědět „ok“ a pak postupně začít pracovat. Fronta zároveň vyhlazuje špičky, protože přidávání nového objektu probíhá rychle a uživatel nemusí čekat na úplný průchod celým cyklem.
  • Jako příklad jsem přidal deduplikační službu, která se pokouší sloučit identické soubory. Pokud v 1 % případů funguje dlouhodobě, klient si toho téměř nevšimne (viz výše), což je velké plus, protože již není vyžadována XNUMX% rychlost a spolehlivost.

Nevýhody jsou však okamžitě viditelné:

  • Náš systém ztratil svou přísnou konzistenci. To znamená, že pokud se například přihlásíte k odběru různých služeb, teoreticky můžete získat jiný stav (protože jedna ze služeb nemusí mít čas na přijetí upozornění z interní fronty). Dalším důsledkem je, že systém nyní nemá společný čas. To znamená, že například není možné seřadit všechny události jednoduše podle času příchodu, protože hodiny mezi servery nemusí být synchronní (navíc stejný čas na dvou serverech je utopie).
  • Žádné události nyní nelze jednoduše vrátit zpět (jak by to bylo možné udělat s databází). Místo toho musíte přidat novou událost − kompenzační akce, který změní poslední stav na požadovaný. Jako příklad z podobné oblasti: bez přepisování historie (což je v některých případech špatné), nemůžete vrátit commit v git, ale můžete vytvořit speciální rollback commit, který v podstatě jen vrací starý stav. Jak chybný commit, tak i rollback však zůstanou v historii.
  • Datové schéma se může od vydání k vydání měnit, ale staré události již nebude možné aktualizovat na nový standard (protože události v zásadě nelze změnit).

Jak můžete vidět, Event Sourcing funguje dobře s CQRS. Navíc implementace systému s efektivními a pohodlnými frontami, ale bez oddělení datových toků, je již sama o sobě obtížná, protože budete muset přidat synchronizační body, které celý pozitivní efekt front neutralizují. Při aplikaci obou přístupů najednou je nutné mírně upravit programový kód. V našem případě při odesílání souboru na server přijde odpověď pouze „ok“, což znamená pouze „operace přidání souboru byla uložena“. Formálně to neznamená, že data jsou již dostupná na jiných zařízeních (například služba deduplikace může index znovu sestavit). Po nějaké době však klient obdrží upozornění ve stylu „soubor X byl uložen“.

Jako výsledek:

  • Počet stavů odesílání souborů se zvyšuje: místo klasického „soubor odeslán“ dostáváme dva: „soubor byl přidán do fronty na serveru“ a „soubor byl uložen do úložiště“. To druhé znamená, že ostatní zařízení již mohou začít přijímat soubor (upraveno s ohledem na skutečnost, že fronty pracují s různou rychlostí).
  • Vzhledem k tomu, že informace o odeslání nyní přicházejí různými kanály, musíme přijít s řešením, jak získat stav zpracování souboru. V důsledku toho: na rozdíl od klasického dotaz-odpověď může být klient během zpracování souboru restartován, ale stav tohoto zpracování sám o sobě bude správný. Navíc tato položka funguje v podstatě hned po vybalení. V důsledku toho jsme nyní tolerantnější k selháním.

Stříkání

Jak je popsáno výše, systémy sourcingu událostí postrádají přísnou konzistenci. To znamená, že můžeme používat několik úložišť bez jakékoli synchronizace mezi nimi. Přiblížením se k našemu problému můžeme:

  • Oddělte soubory podle typu. Lze například dekódovat obrázky/videa a vybrat efektivnější formát.
  • Oddělte účty podle země. Vzhledem k mnoha zákonům to může být vyžadováno, ale toto schéma architektury takovou příležitost poskytuje automaticky

Pohodlné architektonické vzory

Pokud chcete přenášet data z jednoho úložiště do druhého, pak standardní prostředky již nestačí. Bohužel v tomto případě musíte frontu zastavit, provést migraci a poté ji spustit. V obecném případě nelze data přenášet „za běhu“, pokud je však fronta událostí zcela uložena a máte snímky předchozích stavů úložiště, můžeme události přehrát následovně:

  • V Event Source má každá událost svůj vlastní identifikátor (ideálně neklesající). To znamená, že můžeme do úložiště přidat pole – id posledního zpracovaného prvku.
  • Frontu duplikujeme, aby bylo možné všechny události zpracovat pro několik nezávislých úložišť (první je to, ve kterém jsou již data uložena, a druhé je nové, ale stále prázdné). Druhá fronta se samozřejmě ještě nezpracovává.
  • Spustíme druhou frontu (to znamená, že začneme přehrávat události).
  • Když je nová fronta relativně prázdná (to znamená, že průměrný časový rozdíl mezi přidáním prvku a jeho načtením je přijatelný), můžete začít přepínat čtečky na nové úložiště.

Jak vidíte, v našem systému jsme neměli a stále nemáme přísnou konzistenci. Existuje pouze případná konzistence, tedy záruka, že události jsou zpracovávány ve stejném pořadí (ale možná s různým zpožděním). A pomocí toho můžeme relativně snadno přenášet data bez zastavení systému na druhou stranu zeměkoule.

Pokračujeme-li tedy v našem příkladu online úložiště souborů, taková architektura nám již poskytuje řadu bonusů:

  • Objekty můžeme přibližovat uživatelům dynamickým způsobem. Tímto způsobem můžete zlepšit kvalitu služeb.
  • Některá data můžeme ukládat v rámci společností. Enterprise uživatelé například často vyžadují, aby jejich data byla uložena v řízených datových centrech (aby nedocházelo k únikům dat). Prostřednictvím shardingu to můžeme snadno podpořit. A úkol je ještě jednodušší, pokud má zákazník kompatibilní cloud (např. Azure s vlastním hostitelem).
  • A nejdůležitější je, že to dělat nemusíme. Ostatně pro začátek bychom byli docela spokojeni s jedním úložištěm pro všechny účty (abychom mohli rychle začít pracovat). A klíčovou vlastností tohoto systému je, že ačkoli je rozšiřitelný, v počáteční fázi je docela jednoduchý. Nemusíte hned psát kód, který pracuje s milionem samostatných nezávislých front atd. V případě potřeby to lze provést v budoucnu.

Hosting statického obsahu

Tento bod se může zdát zcela samozřejmý, ale pro víceméně standardně načtenou aplikaci je stále nezbytný. Jeho podstata je jednoduchá: veškerý statický obsah není distribuován ze stejného serveru, kde se nachází aplikace, ale ze speciálních serverů určených speciálně pro tento úkol. V důsledku toho se tyto operace provádějí rychleji (podmíněný nginx obsluhuje soubory rychleji a méně nákladně než Java server). Plus architektura CDN (Content Delivery Network) nám umožňuje umístit naše soubory blíže koncovým uživatelům, což má pozitivní vliv na pohodlí práce se službou.

Nejjednodušším a nejstandardnějším příkladem statického obsahu je sada skriptů a obrázků pro web. S nimi je vše jednoduché - jsou předem známy, poté je archiv nahrán na CDN servery, odkud jsou distribuovány koncovým uživatelům.

Ve skutečnosti však pro statický obsah můžete použít přístup poněkud podobný architektuře lambda. Vraťme se k našemu úkolu (online úložiště souborů), ve kterém potřebujeme distribuovat soubory uživatelům. Nejjednodušším řešením je vytvořit službu, která na každý požadavek uživatele provede všechny potřebné kontroly (autorizace atd.) a následně stáhne soubor přímo z našeho úložiště. Hlavní nevýhodou tohoto přístupu je, že statický obsah (a soubor s určitou revizí je ve skutečnosti statický obsah) je distribuován stejným serverem, který obsahuje obchodní logiku. Místo toho můžete vytvořit následující diagram:

  • Server poskytuje adresu URL ke stažení. Může mít tvar file_id + klíč, kde klíč je mini-digitální podpis, který dává právo přístupu ke zdroji na následujících XNUMX hodin.
  • Soubor je distribuován jednoduchým nginx s následujícími možnostmi:
    • Ukládání obsahu do mezipaměti. Vzhledem k tomu, že tato služba může být umístěna na samostatném serveru, nechali jsme si rezervu do budoucna s možností ukládat všechny nejnovější stažené soubory na disk.
    • Kontrola klíče v době vytváření připojení
  • Volitelné: zpracování streamovaného obsahu. Pokud například zkomprimujeme všechny soubory ve službě, pak můžeme provést rozbalení přímo v tomto modulu. V důsledku toho: IO operace se provádějí tam, kam patří. Archivátor v Javě snadno alokuje spoustu paměti navíc, ale přepsání služby s obchodní logikou do podmínek Rust/C++ může být také neúčinné. V našem případě se používají různé procesy (nebo i služby), a proto dokážeme celkem efektivně oddělit business logiku a IO operace.

Pohodlné architektonické vzory

Toto schéma není příliš podobné distribuci statického obsahu (protože někam nenahrajeme celý statický balíček), ale ve skutečnosti se tento přístup týká právě distribuce neměnných dat. Toto schéma lze navíc zobecnit na další případy, kdy obsah není pouze statický, ale může být reprezentován jako sada neměnných a nesmazatelných bloků (ačkoli je lze přidat).

Jako další příklad (pro posílení): pokud jste pracovali s Jenkins/TeamCity, pak víte, že obě řešení jsou napsána v Javě. Oba jsou procesem Java, který se zabývá jak orchestrací sestavení, tak správou obsahu. Oba mají zejména úkoly jako „přenést soubor/složku ze serveru“. Jako příklad: vydávání artefaktů, přenos zdrojového kódu (kdy agent nestahuje kód přímo z úložiště, ale server to dělá za něj), přístup k logům. Všechny tyto úlohy se liší svým IO zatížením. To znamená, že se ukazuje, že server odpovědný za komplexní obchodní logiku musí být zároveň schopen efektivně protlačit velké toky dat sám přes sebe. A co je nejzajímavější, je, že taková operace může být delegována na stejný nginx podle přesně stejného schématu (kromě toho, že datový klíč by měl být přidán k požadavku).

Pokud se však vrátíme do našeho systému, dostaneme podobný diagram:

Pohodlné architektonické vzory

Jak vidíte, systém se stal radikálně složitějším. Nyní to není jen miniproces, který ukládá soubory lokálně. Nyní není vyžadována ta nejjednodušší podpora, kontrola verzí API atd. Po nakreslení všech diagramů je proto nejlepší podrobně zhodnotit, zda se rozšiřitelnost vyplatí. Pokud však chcete mít možnost systém rozšířit (včetně práce s ještě větším počtem uživatelů), pak budete muset sáhnout po podobných řešeních. Ale ve výsledku je systém architektonicky připraven na zvýšené zatížení (téměř každý komponent může být klonován pro horizontální škálování). Systém lze aktualizovat bez jeho zastavení (jednoduše se některé operace mírně zpomalí).

Jak jsem řekl na samém začátku, nyní se řada internetových služeb začala více zatěžovat. A některé z nich prostě přestaly správně fungovat. Systémy totiž selhaly právě ve chvíli, kdy měl podnik vydělávat peníze. To znamená, že namísto odloženého doručení, namísto toho, aby zákazníkům navrhoval „naplánujte si doručení na nadcházející měsíce“, systém jednoduše řekl: „jděte ke svým konkurentům“. Ve skutečnosti je to cena nízké produktivity: ztráty nastanou právě tehdy, když budou zisky nejvyšší.

Závěr

Všechny tyto přístupy byly známy již dříve. Stejný VK již dlouho používá myšlenku hostování statického obsahu k zobrazování obrázků. Mnoho online her používá schéma Sharding k rozdělení hráčů do regionů nebo k oddělení herních lokací (pokud je svět sám jedním). V e-mailu se aktivně používá přístup Event Sourcing. Většina obchodních aplikací, kde jsou data neustále přijímána, je ve skutečnosti postavena na přístupu CQRS, aby bylo možné přijatá data filtrovat. Horizontální škálování se v mnoha službách používá už docela dlouho.

Co je však nejdůležitější, všechny tyto vzory se staly velmi snadno aplikovatelné v moderních aplikacích (pokud jsou samozřejmě vhodné). Cloudy nabízejí Sharding a horizontální škálování hned, což je mnohem jednodušší, než si sami objednávat různé dedikované servery v různých datových centrech. CQRS se stalo mnohem jednodušším, už jen díky rozvoji knihoven, jako je RX. Asi před 10 lety to mohla podporovat vzácná webová stránka. Event Sourcing je také neuvěřitelně snadné nastavit díky připraveným kontejnerům s Apache Kafka. Před 10 lety by to byla inovace, nyní je to běžné. Stejné je to s hostováním statického obsahu: díky pohodlnějším technologiím (včetně skutečnosti, že existuje podrobná dokumentace a velká databáze odpovědí) se tento přístup ještě zjednodušil.

Výsledkem je, že implementace řady poměrně složitých architektonických vzorů se nyní stala mnohem jednodušší, což znamená, že je lepší se na ni předem blíže podívat. Pokud bylo v deset let staré aplikaci upuštěno od jednoho z výše uvedených řešení z důvodu vysokých nákladů na implementaci a provoz, můžete nyní v nové aplikaci nebo po refaktoringu vytvořit službu, která již bude architektonicky rozšiřitelná ( z hlediska výkonu) a připravené na nové požadavky klientů (například na lokalizaci osobních údajů).

A co je nejdůležitější: prosím nepoužívejte tyto přístupy, pokud máte jednoduchou aplikaci. Ano, jsou krásné a zajímavé, ale na stránky s vrcholnou návštěvností 100 lidí si často vystačíte s klasickým monolitem (alespoň zvenku, vše uvnitř lze rozdělit na moduly atd.).

Zdroj: www.habr.com

Přidat komentář