Gerieflike argitektoniese patrone

Haai Habr!

In die lig van huidige gebeure as gevolg van die koronavirus, het 'n aantal internetdienste 'n groter las begin kry. Byvoorbeeld, een van die kleinhandelkettings in die Verenigde Koninkryk het sopas die webwerf met aanlynbestellings gestopomdat daar nie genoeg kapasiteit was nie. En dit is ver van altyd moontlik om die bediener te bespoedig deur bloot kragtiger toerusting by te voeg, maar kliënteversoeke moet verwerk word (of dit sal na mededingers gaan).

In hierdie artikel sal ek kortliks praat oor gewilde praktyke wat jou in staat sal stel om 'n vinnige en foutverdraagsame diens te lewer. Uit die moontlike ontwikkelingskemas het ek egter net dié gekies wat nou is maklik om te gebruik. Vir elke item het jy óf klaargemaakte biblioteke, óf jy het die geleentheid om die probleem op te los met behulp van 'n wolkplatform.

Horisontale skaal

Die eenvoudigste en mees bekende punt. Konvensioneel is die twee mees algemene vragverspreidingskemas horisontale en vertikale skaal. In die eerste geval jy laat dienste toe om parallel te loop en sodoende die las tussen hulle te versprei. In die tweede jy bestel kragtiger bedieners of optimaliseer die kode.

Ek sal byvoorbeeld 'n abstrakte wolkberging van lêers neem, dit wil sê 'n analoog van OwnCloud, OneDrive, ensovoorts.

'n Standaardfoto van so 'n stroombaan is hieronder, maar dit demonstreer net die kompleksiteit van die stelsel. Ons moet immers dienste op een of ander manier sinchroniseer. Wat gebeur as 'n gebruiker 'n lêer vanaf 'n tablet stoor en dit dan van 'n foon af wil bekyk?

Gerieflike argitektoniese patrone
Die verskil tussen die benaderings: in vertikale skaal is ons gereed om die kapasiteit van nodusse te verhoog, en in horisontale skalering is ons gereed om nuwe nodusse by te voeg om die las te versprei.

CQRS

Bevelnavraag Verantwoordelikheidsskeiding 'n taamlik belangrike patroon, aangesien dit verskillende kliënte toelaat om nie net aan verskillende dienste te koppel nie, maar ook om dieselfde gebeurtenisstrome te ontvang. Die bonusse is nie so voor die hand liggend vir 'n eenvoudige toepassing nie, maar dit is uiters belangrik (en eenvoudig) vir 'n gelaaide diens. Die essensie daarvan: inkomende en uitgaande datastrome moet nie kruis nie. Dit wil sê, jy kan nie 'n versoek stuur en 'n reaksie verwag nie, in plaas daarvan stuur jy 'n versoek na diens A, maar ontvang 'n antwoord in diens B.

Die eerste bonus van hierdie benadering is die vermoë om die verbinding (in die breë sin van die woord) te verbreek in die proses om 'n lang navraag uit te voer. Kom ons neem byvoorbeeld 'n min of meer standaard volgorde:

  1. Die kliënt het 'n versoek na die bediener gestuur.
  2. Die bediener het 'n lang verwerking begin.
  3. Die bediener het op die kliënt gereageer met 'n resultaat.

Stel jou voor dat daar in punt 2 'n verbindingsbreuk was (of die netwerk het weer gekoppel, of die gebruiker het na 'n ander bladsy gegaan en die verbinding verbreek). In hierdie geval sal dit moeilik wees vir die bediener om 'n antwoord aan die gebruiker te stuur met inligting oor wat presies verwerk is. Deur CQRS te gebruik, sal die volgorde effens anders wees:

  1. Die kliënt het ingeteken op opdaterings.
  2. Die kliënt het 'n versoek na die bediener gestuur.
  3. Die bediener het geantwoord "versoek aanvaar".
  4. Die bediener het gereageer met 'n resultaat deur die kanaal vanaf punt "1".

Gerieflike argitektoniese patrone

Soos u kan sien, is die skema 'n bietjie meer ingewikkeld. Boonop ontbreek die intuïtiewe versoek-reaksie-benadering hier. Soos u egter kan sien, sal die verbreking van die verbinding tydens die verwerking van die versoek nie tot 'n fout lei nie. Verder, as die gebruiker in werklikheid vanaf verskeie toestelle (byvoorbeeld vanaf 'n selfoon en van 'n tablet) aan die diens gekoppel is, kan jy seker maak dat die reaksie na beide toestelle kom.

Interessant genoeg word die kode vir die verwerking van inkomende boodskappe dieselfde (nie 100%) vir gebeure wat deur die kliënt self beïnvloed is nie, en vir ander gebeurtenisse, insluitend dié van ander kliënte.

In werklikheid kry ons egter bykomende bonusse as gevolg van die feit dat 'n eenrigtingstroom in 'n funksionele styl verwerk kan word (met behulp van RX en analoë). En dit is reeds 'n ernstige pluspunt, aangesien die toepassing in werklikheid heeltemal reaktief gemaak kan word, en selfs met die gebruik van 'n funksionele benadering. Vir vetprogramme kan dit ontwikkeling en ondersteuningsbronne aansienlik bespaar.

As ons hierdie benadering met horisontale skaal kombineer, kry ons as 'n bonus die vermoë om versoeke na een bediener te stuur en antwoorde van 'n ander te ontvang. Die kliënt kan dus die diens kies wat vir hom gerieflik is, en die stelsel binne sal steeds gebeure korrek kan verwerk.

Gebeurtenis verkryging

Soos u weet, is een van die hoofkenmerke van 'n verspreide stelsel die afwesigheid van 'n gemeenskaplike tyd, 'n gemeenskaplike kritieke afdeling. Vir een proses kan jy 'n sinchronisasie maak (op dieselfde mutexes), waarin jy seker is dat niemand anders hierdie kode uitvoer nie. Vir 'n verspreide stelsel is dit egter gevaarlik, aangesien dit oorhoofse koste sal verg, en al die sjarme van skalering sal doodgemaak word - eweneens, alle komponente sal vir een wag.

Van hier af kry ons 'n belangrike feit - 'n vinnig verspreide stelsel kan nie gesinchroniseer word nie, want dan sal ons prestasie verminder. Aan die ander kant het ons dikwels 'n sekere konsekwentheid van komponente nodig. En hiervoor kan jy die benadering gebruik met uiteindelike konsekwentheid, waar dit gewaarborg word dat in die afwesigheid van dataveranderings, 'n ruk na die laaste opdatering ("uiteindelik"), alle navrae die laaste opgedateerde waarde sal terugstuur.

Dit is belangrik om te verstaan ​​dat dit baie gereeld vir klassieke databasisse gebruik word streng konsekwentheid, waar elke nodus dieselfde inligting het (dit word dikwels bereik in die geval wanneer die transaksie as gevestig beskou word eers na die reaksie van die tweede bediener). Daar is 'n paar loslatings hier as gevolg van isolasievlakke, maar die algemene essensie bly dieselfde - jy kan in 'n ten volle konsekwente wêreld leef.

Maar terug na die oorspronklike probleem. As deel van die stelsel gebou kan word met uiteindelike konsekwentheid, dan kan die volgende diagram saamgestel word.

Gerieflike argitektoniese patrone

Belangrike kenmerke van hierdie benadering:

  • Elke inkomende versoek word in een tou geplaas.
  • Terwyl 'n versoek verwerk word, kan die diens ook take op ander toue plaas.
  • Elke inkomende gebeurtenis het 'n ID (wat vir deduplisering vereis word).
  • Die tou werk ideologies volgens die "byvoeg slegs"-skema. Jy kan nie elemente daaruit skrap of herrangskik nie.
  • Die tou werk volgens die EIEU-skema (jammer vir die tautologie). As jy parallelle uitvoering moet doen, moet jy voorwerpe na verskillende rye in een van die stadiums skuif.

Laat ek u daaraan herinner dat ons die geval van aanlyn lêerberging oorweeg. In hierdie geval sal die stelsel iets soos volg lyk:

Gerieflike argitektoniese patrone

Dit is belangrik dat die dienste in die diagram nie noodwendig 'n aparte bediener beteken nie. Selfs die proses kan dieselfde wees. Nog iets is belangrik: ideologies is hierdie dinge so geskei dat horisontale skaal maklik toegepas kan word.

En vir twee gebruikers sal die skema so lyk (dienste wat vir verskillende gebruikers bedoel is, word in verskillende kleure aangedui):

Gerieflike argitektoniese patrone

Bonusse uit so 'n kombinasie:

  • Inligtingverwerkingsdienste word geskei. Die toue is ook geskei. As ons die deurset van die stelsel moet verhoog, moet ons net meer dienste op meer bedieners laat loop.
  • Wanneer ons inligting van 'n gebruiker ontvang, hoef ons nie te wag totdat die data ten volle gestoor is nie. Inteendeel, dit is genoeg dat ons “ok” antwoord en dan geleidelik begin werk. Terselfdertyd maak die tou pieke glad, aangesien die byvoeging van 'n nuwe voorwerp vinnig is, en die gebruiker hoef nie te wag vir 'n volle pas deur die hele siklus nie.
  • Ek het byvoorbeeld 'n dedupliseringsdiens bygevoeg wat probeer om identiese lêers saam te voeg. As dit in 1% van die gevalle lank werk, sal die kliënt dit feitlik nie agterkom nie (sien hierbo), wat 'n groot pluspunt is, aangesien ons nie meer XNUMX% spoed en betroubaarheid vereis nie.

Die nadele is egter onmiddellik sigbaar:

  • Ons stelsel het streng konsekwentheid verloor. Dit beteken dat as u byvoorbeeld op verskillende dienste inteken, u teoreties 'n ander toestand kan kry (aangesien een van die dienste dalk nie tyd het om 'n kennisgewing van die interne tou te ontvang nie). As 'n ander gevolg het die stelsel nou geen gemeenskaplike tyd nie. Dit wil sê, dit is byvoorbeeld onmoontlik om alle gebeure bloot volgens die tyd van aankoms te sorteer, aangesien die horlosies tussen bedieners dalk nie sinchronies is nie (bowendien is dieselfde tyd op twee bedieners 'n utopie).
  • Geen gebeurtenisse kan nou eenvoudig teruggerol word nie (soos 'n mens met 'n databasis kan doen). In plaas daarvan moet 'n nuwe gebeurtenis bygevoeg word − vergoeding gebeurtenis, wat die laaste toestand na die vereiste een sal verander. As 'n voorbeeld uit 'n soortgelyke area: sonder om die geskiedenis te herskryf (wat in sommige gevalle sleg is), kan jy nie 'n commit in git terugrol nie, maar jy kan 'n spesiale doen terugrol pleeg, wat in wese net die ou staat sal teruggee. Beide die foutiewe commit en die terugrol sal egter in die geskiedenis bly.
  • Die dataskema kan van vrystelling tot vrystelling verander, maar ou gebeurtenisse kan nie meer opgedateer word na die nuwe standaard nie (aangesien gebeurtenisse in beginsel nie verander kan word nie).

Soos u kan sien, kom Event Sourcing goed oor die weg met CQRS. Boonop is dit reeds moeilik om 'n stelsel met doeltreffende en gerieflike toue te implementeer, maar sonder datavloeiskeiding, want u sal sinchronisasiepunte moet byvoeg wat die hele positiewe effek van toue sal neutraliseer. Deur beide benaderings gelyktydig toe te pas, is dit nodig om die kode van die program effens aan te pas. In ons geval, wanneer 'n lêer na die bediener gestuur word, kom slegs "ok" in die antwoord, wat slegs beteken dat "die bewerking van die byvoeging van 'n lêer gestoor is". Formeel beteken dit nie dat die data reeds op ander toestelle beskikbaar is nie (die dedupliseringsdiens kan byvoorbeeld die indeks herbou). Na 'n rukkie sal die kliënt egter 'n kennisgewing ontvang in die styl van "lêer X gestoor".

As gevolg daarvan:

  • Die aantal lêeroplaaistatusse neem toe: in plaas van die klassieke "lêer gestuur", kry ons twee: "lêer bygevoeg aan tou op die bediener" en "lêer gestoor in berging". Laasgenoemde beteken dat ander toestelle reeds die lêer kan begin ontvang (aangepas vir die feit dat die toue teen verskillende spoed werk).
  • Weens die feit dat die indieninginligting nou deur verskillende kanale kom, moet ons met oplossings vorendag kom om die lêerverwerkingstatus te kry. As gevolg hiervan: anders as die klassieke versoek-reaksie, kan die kliënt herbegin word terwyl die lêer verwerk word, maar die status van hierdie verwerking self sal korrek wees. En hierdie item werk, in werklikheid, uit die boks. As gevolg hiervan: ons is nou meer verdraagsaam teenoor mislukkings.

Afskuur

Soos hierbo beskryf, is daar geen sterk konsekwentheid in gebeurtenisverkrygingstelsels nie. Dit beteken dat ons verskeie bergings kan gebruik sonder enige sinchronisasie tussen hulle. Wanneer ons ons taak nader, kan ons:

  • Skei lêers volgens tipe. Prente/video's kan byvoorbeeld gedekodeer word en 'n meer doeltreffende formaat kan gekies word.
  • Skei rekeninge volgens land. As gevolg van baie wette kan dit vereis word, maar hierdie argitektoniese skema bied so 'n moontlikheid outomaties.

Gerieflike argitektoniese patrone

As u data van een berging na 'n ander wil oordra, is standaardgereedskap hier onontbeerlik. Ongelukkig moet u in hierdie geval die tou stop, die migrasie maak en dit dan laat loop. In die algemene geval kan data nie dadelik oorgedra word nie, maar as die gebeurteniswaglys heeltemal gestoor is, en jy het kiekies van die vorige stoortoestande, kan ons die gebeure soos volg herspeel:

  • In die gebeurtenisbron het elke gebeurtenis sy eie identifiseerder (ideaal gesproke, nie afneemend nie). Dit beteken dat ons 'n veld by die stoor kan voeg - die ID van die laaste verwerkte element.
  • Ons dupliseer die tou sodat alle gebeurtenisse vir verskeie onafhanklike bergings verwerk kan word (die eerste is die een waarin data reeds gestoor is, en die tweede is nuut, maar vir eers leeg). Die tweede beurt is natuurlik nog nie verwerk nie.
  • Ons begin die tweede tou (dit wil sê, ons begin gebeurtenisse herspeel).
  • Wanneer die nuwe tou relatief leeg is (dit wil sê die gemiddelde tydsverskil tussen die byvoeging van 'n element en die onttrekking daarvan is aanvaarbaar), kan jy begin om lesers na die nuwe winkel oor te skakel.

Soos u kan sien, het ons nie, en het nog steeds nie, streng konsekwentheid in die stelsel. Daar is slegs uiteindelike konsekwentheid, dit wil sê 'n waarborg dat gebeure in dieselfde volgorde verwerk word (maar moontlik met 'n ander vertraging). En deur dit te gebruik, kan ons data relatief maklik oordra sonder om die stelsel na die ander kant van die aardbol te stop.

As ons dus ons voorbeeld van aanlynberging vir lêers voortsit, gee so 'n argitektuur ons reeds 'n aantal bonusse:

  • Ons kan voorwerpe nader aan gebruikers skuif, en op 'n dinamiese manier. Die kwaliteit van diens kan dus verbeter word.
  • Ons kan sommige data binne maatskappye stoor. Byvoorbeeld, Enterprise-gebruikers vereis dikwels dat hul data in beheerde datasentrums gestoor word (om datalekkasies te vermy). Met versnippering kan ons dit maklik ondersteun. En die taak is selfs makliker as die kliënt 'n versoenbare wolk het (byvoorbeeld, Azure word self aangebied).
  • En die belangrikste, ons kan dit nie doen nie. Inderdaad, vir 'n begin, sou ons redelik tevrede wees met een stoorplek vir alle rekeninge (om vinnig te begin werk). En die belangrikste kenmerk van hierdie stelsel is dat alhoewel dit uitbreidbaar is, dit in die aanvanklike stadium redelik eenvoudig is. Jy hoef net nie dadelik kode te skryf wat werk met 'n miljoen aparte onafhanklike rye, ens. Indien nodig, kan dit in die toekoms gedoen word.

Hosting vir statiese inhoud

Hierdie punt kan redelik voor die hand liggend lyk, maar dit is steeds nodig vir 'n min of meer standaard gelaaide toepassing. Die essensie daarvan is eenvoudig: alle statiese inhoud word nie versprei vanaf dieselfde bediener waar die toepassing geleë is nie, maar van spesiale inhoud wat spesifiek vir hierdie besigheid toegewys is. As gevolg hiervan is hierdie bewerkings vinniger (die voorwaardelike nginx bedien lêers vinniger en goedkoper as die Java-bediener). Plus die CDN-argitektuur (Inhoud lewer Netwerk) stel ons in staat om ons lêers nader aan eindgebruikers te plaas, wat 'n positiewe uitwerking het op die gerief om met die diens te werk.

Die eenvoudigste en mees standaard voorbeeld van statiese inhoud is 'n stel skrifte en beelde vir 'n webwerf. Alles is eenvoudig met hulle - hulle is vooraf bekend, dan word die argief na CDN-bedieners opgelaai, vanwaar dit aan eindgebruikers versprei word.

In werklikheid, vir statiese inhoud, kan jy egter 'n benadering toepas wat ietwat soortgelyk is aan die lambda-argitektuur. Kom ons keer terug na ons taak (aanlyn lêerberging), waarin ons lêers aan gebruikers moet versprei. Die eenvoudigste oplossing in die voorkop is om 'n diens te maak wat vir elke gebruikerversoek al die nodige kontroles doen (magtiging, ens.), en dan die lêer direk vanaf ons berging aflaai. Die grootste nadeel van hierdie benadering is dat statiese inhoud (en 'n lêer met 'n sekere hersiening, in werklikheid, statiese inhoud is) versprei word deur dieselfde bediener wat die besigheidslogika bevat. In plaas daarvan kan jy die volgende skema maak:

  • Die bediener reik 'n aflaai-URL uit. Dit kan van die vorm file_id + sleutel wees, waar sleutel 'n mini-digitale handtekening is wat die reg gee om toegang tot die hulpbron vir die volgende dag te verkry.
  • Die verspreiding van die lêer word hanteer deur 'n eenvoudige nginx met die volgende opsies:
    • Inhoudkas. Aangesien hierdie diens op 'n aparte bediener geleë kan wees, het ons vir onsself 'n reserwe vir die toekoms gelaat met die vermoë om al die nuutste afgelaaide lêers op skyf te stoor.
    • Kontroleer die sleutel ten tyde van die skep van verbinding
  • Opsioneel: stroominhoudverwerking. Byvoorbeeld, as ons alle lêers in die diens saampers, kan ons die uitpak direk in hierdie module doen. As gevolg hiervan: IO-operasies word gedoen waar dit hoort. Die argiefhouer in Java sal maklik baie ekstra geheue toewys, maar die herskryf van die diens met besigheidslogika na voorwaardelike Rust / C ++ kan ook ondoeltreffend wees. In ons geval word verskillende prosesse (of selfs dienste) gebruik, en daarom is dit moontlik om besigheidslogika en IO-bedrywighede redelik effektief te skei.

Gerieflike argitektoniese patrone

So 'n skema stem nie baie ooreen met die verspreiding van statiese inhoud nie (aangesien ons nie die hele pakket statika iewers aflaai nie), maar in werklikheid gaan hierdie benadering juis oor die verspreiding van onveranderlike data. Boonop kan hierdie skema veralgemeen word na ander gevalle waar die inhoud nie net staties is nie, maar aangebied kan word as 'n stel onveranderlike en nie-verwyderbare blokke (alhoewel hulle bygevoeg kan word).

As nog 'n voorbeeld (vir konsolidasie): as jy met Jenkins / TeamCity gewerk het, dan weet jy dat beide oplossings in Java geskryf is. Albei is Java-prosesse wat beide bou-orkestrasie en inhoudbestuur hanteer. Hulle het veral take soos "dra 'n lêer/lêer vanaf die bediener oor". As voorbeeld: die uitreiking van artefakte, die oordrag van bronkode (wanneer die agent nie die kode direk vanaf die bewaarplek aflaai nie, maar die bediener dit vir hom doen), toegang tot logs. Al hierdie take verskil in die las op IO. Dit wil sê, dit blyk dat die bediener wat verantwoordelik is vir komplekse besigheidslogika, terselfdertyd in staat moet wees om groot datastrome effektief deur homself te druk. En wat die interessantste is, so 'n bewerking kan volgens presies dieselfde skema na dieselfde nginx gedelegeer word (behalwe dat die datasleutel by die versoek gevoeg moet word).

As ons egter terugkeer na ons stelsel, kry ons 'n soortgelyke skema:

Gerieflike argitektoniese patrone

Soos u kan sien, is die stelsel radikaal meer ingewikkeld. Nou is dit nie net 'n mini-proses wat lêers plaaslik stoor nie. Nou het jy nie die maklikste ondersteuning, API-weergawe, ens nodig nie. Daarom, nadat al die diagramme geteken is, is dit die beste om in detail te evalueer of die uitbreidbaarheid die koste werd is. As jy egter die stelsel wil kan uitbrei (insluitend om met nog meer gebruikers te werk), dan sal jy vir soortgelyke oplossings moet gaan. Maar as gevolg hiervan is die argitektoniese stelsel gereed om die las te verhoog (byna elke komponent kan vir horisontale skaal gekloon word). Die stelsel kan opgedateer word sonder om dit te stop (eenvoudig, sommige bewerkings sal effens vertraag).

Soos ek heel aan die begin gesê het, het 'n aantal internetdienste nou 'n groter las begin kry. En sommige van hulle het net opgehou werk behoorlik. Trouens, die stelsels het misluk op die presiese oomblik wanneer die besigheid geld behoort te maak. Dit wil sê, in plaas daarvan om aflewering te vertraag, in plaas daarvan om aan kliënte voor te stel "beplan vir aflewering in die komende maande," het die stelsel eenvoudig gesê, "gaan na die kompetisie." Eintlik is dit die prys van lae produktiwiteit: verliese sal plaasvind presies wanneer die wins die hoogste sou wees.

Gevolgtrekking

Al hierdie benaderings was voorheen bekend. Dieselfde VK gebruik al lank die idee van Static Content Hosting om prente te vertoon. 'n Klomp aanlynspeletjies gebruik die Sharding-skema om spelers volgens streek te skei of om speletjie-liggings te skei (as die wêreld self een is). Gebeurtenisverkrygingbenadering word aktief in e-pos gebruik. Die meeste van die toepassings van handelaars waar data voortdurend inkom, is eintlik gebou op die CQRS-benadering om die ontvangde data te kan filter. Wel, horisontale skaal word al lank in baie dienste gebruik.

Die belangrikste is egter dat al hierdie patrone baie maklik geword het om in moderne toepassings toe te pas (as dit natuurlik toepaslik is). Wolke bied Sharding en horisontale skaal gelyktydig, wat baie makliker is as om verskillende toegewyde bedieners in verskillende datasentrums op jou eie te bestel. CQRS het baie makliker geword, al is dit net as gevolg van die ontwikkeling van biblioteke soos RX. Ongeveer 10 jaar gelede kon 'n seldsame webwerf dit ondersteun. Event Sourcing is ook ongelooflik maklik om op te stel danksy klaargemaakte houers met Apache Kafka. 10 jaar gelede sou dit 'n innovasie gewees het, nou is dit alledaags. Net so, met Static Content Hosting: as gevolg van geriefliker tegnologieë (insluitend omdat daar gedetailleerde dokumentasie en 'n groot databasis van antwoorde is), het hierdie benadering selfs eenvoudiger geword.

Gevolglik het die implementering van 'n aantal taamlik komplekse argitektoniese patrone nou baie makliker geword, wat beteken dit is beter om dit vooraf van nader te bekyk. As in 'n tien jaar oue toepassing een van die oplossings hierbo laat vaar is as gevolg van die hoë koste van implementering en bedryf, kan u nou, in 'n nuwe toepassing, of na herfaktorering, 'n diens maak wat reeds argitektonies beide uitbreidbaar sal wees ( in terme van prestasie) en gereed vir nuwe versoeke van kliënte (byvoorbeeld om persoonlike data te lokaliseer).

En die belangrikste, moet asseblief nie hierdie benaderings gebruik as jy 'n eenvoudige toepassing het nie. Ja, hulle is pragtig en interessant, maar vir 'n terrein met 'n piekbesoek van 100 mense, kan jy dikwels klaarkom met 'n klassieke monoliet (ten minste van buite, binne kan alles in modules verdeel word, ens.).

Bron: will.com

Voeg 'n opmerking