Priročni arhitekturni vzorci

Pozdravljeni, Habr!

V luči aktualnega dogajanja zaradi koronavirusa so številne internetne storitve začele biti obremenjene. na primer Ena izmed britanskih trgovskih verig je preprosto ustavila svojo stran za spletno naročanje., ker ni bilo dovolj kapacitet. In ni vedno mogoče pospešiti strežnika s preprostim dodajanjem zmogljivejše opreme, vendar je treba zahteve odjemalcev obdelati (ali pa bodo šli k konkurentom).

V tem članku bom na kratko govoril o priljubljenih praksah, ki vam bodo omogočile ustvarjanje hitre storitve, odporne na napake. Vendar sem izmed možnih razvojnih shem izbral le tiste, ki so trenutno enostaven za uporabo. Za vsak element imate bodisi pripravljene knjižnice ali pa imate možnost, da težavo rešite s platformo v oblaku.

Horizontalno skaliranje

Najenostavnejša in najbolj znana točka. Običajno sta najpogostejši dve shemi porazdelitve obremenitve vodoravno in navpično skaliranje. V prvem primeru omogočite vzporedno delovanje storitev in s tem porazdelite obremenitev med njimi. V drugem naročite močnejše strežnike ali optimizirate kodo.

Za primer bom vzel abstraktno shranjevanje datotek v oblaku, to je nekaj analogov OwnCloud, OneDrive in tako naprej.

Standardna slika takega vezja je spodaj, vendar le prikazuje kompleksnost sistema. Konec koncev moramo nekako sinhronizirati storitve. Kaj se zgodi, če uporabnik shrani datoteko s tablice in si jo nato želi ogledati s telefona?

Priročni arhitekturni vzorci
Razlika med pristopi: pri vertikalnem skaliranju smo pripravljeni povečati moč vozlišč, pri horizontalnem skaliranju pa smo pripravljeni dodati nova vozlišča za porazdelitev obremenitve.

CQRS

Ukaz Poizvedba Ločevanje odgovornosti Precej pomemben vzorec, saj omogoča različnim odjemalcem ne le povezavo z različnimi storitvami, ampak tudi prejemanje istih tokov dogodkov. Njegove prednosti niso tako očitne za preprosto aplikacijo, vendar je izjemno pomemben (in preprost) za zaposleno storitev. Njegovo bistvo: dohodni in odhodni tokovi podatkov se ne smejo križati. To pomeni, da ne morete poslati zahteve in pričakovati odgovora; namesto tega pošljete zahtevo storitvi A, vendar prejmete odgovor storitve B.

Prvi bonus tega pristopa je zmožnost prekinitve povezave (v širšem pomenu besede) med izvajanjem dolge zahteve. Na primer, vzemimo bolj ali manj standardno zaporedje:

  1. Stranka je poslala zahtevo strežniku.
  2. Strežnik je začel dolgo obdelavo.
  3. Strežnik je odjemalcu odgovoril z rezultatom.

Predstavljajmo si, da je bila v točki 2 povezava prekinjena (ali se je omrežje znova povezalo ali pa je uporabnik odšel na drugo stran in prekinil povezavo). V tem primeru bo strežnik težko poslal uporabniku odgovor s podatki o tem, kaj točno je bilo obdelano. Z uporabo CQRS bo zaporedje nekoliko drugačno:

  1. Stranka je naročena na posodobitve.
  2. Stranka je poslala zahtevo strežniku.
  3. Strežnik je odgovoril "zahteva sprejeta."
  4. Strežnik je odgovoril z rezultatom preko kanala iz točke “1”.

Priročni arhitekturni vzorci

Kot lahko vidite, je shema nekoliko bolj zapletena. Poleg tega tukaj manjka intuitivni pristop zahteva-odgovor. Vendar, kot lahko vidite, prekinitev povezave med obdelavo zahteve ne bo povzročila napake. Poleg tega, če je uporabnik dejansko povezan s storitvijo iz več naprav (na primer iz mobilnega telefona in tabličnega računalnika), lahko poskrbite, da bo odgovor prišel na obe napravi.

Zanimivo je, da koda za obdelavo dohodnih sporočil postane enaka (ne 100%) tako za dogodke, na katere je vplival odjemalec sam, kot za druge dogodke, vključno s tistimi od drugih odjemalcev.

Vendar pa v resnici dobimo dodaten bonus zaradi dejstva, da lahko enosmerni tok obravnavamo v funkcionalnem slogu (z uporabo RX in podobno). In to je že resen plus, saj je v bistvu aplikacijo mogoče narediti popolnoma reaktivno in tudi s funkcionalnim pristopom. Pri mastnih programih lahko to znatno prihrani sredstva za razvoj in podporo.

Če ta pristop združimo s horizontalnim skaliranjem, potem kot bonus dobimo možnost pošiljanja zahtev enemu strežniku in sprejemanja odgovorov drugega. Tako lahko stranka izbere storitev, ki mu ustreza, sistem znotraj pa bo še vedno lahko pravilno obdelal dogodke.

Dogodek Sourcing

Kot veste, je ena od glavnih značilnosti porazdeljenega sistema odsotnost skupnega časa, skupnega kritičnega odseka. Za en proces lahko narediš sinhronizacijo (na istih muteksih), znotraj katere si prepričan, da nihče drug ne izvaja te kode. Vendar je to nevarno za porazdeljeni sistem, saj bo zahtevalo dodatne stroške in bo tudi uničilo vso lepoto skaliranja - vse komponente bodo še vedno čakale na eno.

Od tu izhaja pomembno dejstvo - hitrega porazdeljenega sistema ni mogoče sinhronizirati, ker bomo s tem zmanjšali zmogljivost. Po drugi strani pa pogosto potrebujemo določeno skladnost med komponentami. In za to lahko uporabite pristop z končna doslednost, kjer je zagotovljeno, da bodo vse poizvedbe vrnile zadnjo posodobljeno vrednost, če nekaj časa po zadnji posodobitvi (»čez čas«) ni sprememb podatkov.

Pomembno je razumeti, da se za klasične baze podatkov precej pogosto uporablja močna konsistenca, kjer ima vsako vozlišče enake informacije (to se pogosto doseže v primeru, ko se transakcija šteje za vzpostavljeno šele po odzivu drugega strežnika). Tukaj je nekaj sprostitev zaradi stopenj izolacije, vendar splošna ideja ostaja enaka - živite lahko v popolnoma harmoniziranem svetu.

Vendar se vrnimo k prvotni nalogi. Če je del sistema mogoče zgraditi z končna doslednost, potem lahko sestavimo naslednji diagram.

Priročni arhitekturni vzorci

Pomembne značilnosti tega pristopa:

  • Vsaka dohodna zahteva se postavi v eno čakalno vrsto.
  • Storitev lahko med obdelavo zahteve postavi opravila tudi v druge čakalne vrste.
  • Vsak dohodni dogodek ima identifikator (ki je potreben za deduplikacijo).
  • Čakalna vrsta ideološko deluje po shemi »samo dodajanje«. Iz njega ne morete odstraniti elementov ali jih preurediti.
  • Čakalna vrsta deluje po shemi FIFO (se opravičujem za tavtologijo). Če morate izvesti vzporedno izvajanje, morate na eni stopnji premakniti predmete v različne čakalne vrste.

Naj vas spomnim, da obravnavamo primer spletnega shranjevanja datotek. V tem primeru bo sistem videti nekako takole:

Priročni arhitekturni vzorci

Pomembno je, da storitve v diagramu ne pomenijo nujno ločenega strežnika. Celo postopek je lahko enak. Pomembno je še nekaj: ideološko so te stvari ločene tako, da se zlahka uporabi horizontalno skaliranje.

In za dva uporabnika bo diagram videti tako (storitve, namenjene različnim uporabnikom, so označene z različnimi barvami):

Priročni arhitekturni vzorci

Bonusi iz takšne kombinacije:

  • Storitve obdelave informacij so ločene. Tudi čakalne vrste so ločene. Če moramo povečati prepustnost sistema, moramo samo zagnati več storitev na več strežnikih.
  • Ko prejmemo informacije od uporabnika, nam ni treba čakati, da so podatki v celoti shranjeni. Nasprotno, samo odgovoriti moramo "v redu" in nato postopoma začeti delati. Hkrati čakalna vrsta zgladi vrhove, saj se dodajanje novega predmeta zgodi hitro in uporabniku ni treba čakati na popoln prehod skozi celoten cikel.
  • Kot primer sem dodal storitev deduplikacije, ki poskuša združiti enake datoteke. Če v 1% primerov deluje dlje časa, naročnik tega komajda opazi (glej zgoraj), kar je velik plus, saj od nas ne zahtevajo več XNUMX% hitrosti in zanesljivosti.

Vendar pa so slabosti vidne takoj:

  • Naš sistem je izgubil svojo strogo doslednost. To pomeni, da če se na primer naročite na različne storitve, potem teoretično lahko dobite drugačno stanje (ker ena od storitev morda nima časa prejeti obvestila iz notranje čakalne vrste). Druga posledica tega je, da sistem zdaj nima skupnega časa. To pomeni, da je na primer nemogoče vse dogodke razvrstiti preprosto po času prihoda, saj ure med strežniki morda niso sinhrone (še več, isti čas na dveh strežnikih je utopija).
  • Nobenega dogodka zdaj ni mogoče preprosto vrniti (kot bi lahko storili z zbirko podatkov). Namesto tega morate dodati nov dogodek − odškodninski dogodek, ki bo spremenil zadnje stanje v zahtevano. Kot primer s podobnega področja: brez ponovnega pisanja zgodovine (kar je v nekaterih primerih slabo) ne morete vrniti objave v git, lahko pa naredite poseben vrnitev nazaj, ki v bistvu samo vrne staro stanje. Vendar pa bosta tako napačna potrditev kot povrnitev ostala v zgodovini.
  • Podatkovna shema se lahko spreminja od izdaje do izdaje, vendar starih dogodkov ne bo več mogoče posodobiti na nov standard (saj dogodkov načeloma ni mogoče spreminjati).

Kot lahko vidite, Event Sourcing dobro deluje s CQRS. Poleg tega je implementacija sistema z učinkovitimi in priročnimi čakalnimi vrstami, a brez ločevanja podatkovnih tokov, že sama po sebi težka, saj boste morali dodati sinhronizacijske točke, ki bodo nevtralizirale celoten pozitivni učinek čakalnih vrst. Z uporabo obeh pristopov hkrati je potrebno nekoliko prilagoditi programsko kodo. V našem primeru pri pošiljanju datoteke na strežnik pride odgovor le »ok«, kar pomeni le, da je bila »operacija dodajanja datoteke shranjena«. Formalno to ne pomeni, da so podatki že na voljo na drugih napravah (storitev za deduplikacijo lahko na primer ponovno zgradi indeks). Čez nekaj časa pa bo stranka prejela obvestilo v slogu »datoteka X je bila shranjena«.

Kot rezultat:

  • Število statusov pošiljanja datoteke narašča: namesto klasičnega »datoteka poslana« dobimo dva: »datoteka je bila dodana v čakalno vrsto na strežniku« in »datoteka je bila shranjena v shrambi«. Slednje pomeni, da lahko druge naprave že začnejo prejemati datoteko (prilagojeno dejstvu, da čakalne vrste delujejo različno hitro).
  • Ker informacije o oddaji zdaj prihajajo prek različnih kanalov, moramo najti rešitve za prejemanje statusa obdelave datoteke. Posledica tega je: za razliko od klasične zahteve-odgovora se odjemalec med obdelavo datoteke lahko ponovno zažene, vendar bo sam status te obdelave pravilen. Poleg tega ta izdelek deluje v bistvu takoj po izdelavi. Posledično: zdaj smo bolj tolerantni do neuspehov.

Ostriženje

Kot je opisano zgoraj, sistemi za pridobivanje dogodkov nimajo stroge doslednosti. To pomeni, da lahko uporabljamo več shramb brez kakršne koli sinhronizacije med njimi. Če pristopimo k našemu problemu, lahko:

  • Datoteke ločite po vrsti. Na primer, slike/videoposnetke je mogoče dekodirati in izbrati učinkovitejši format.
  • Ločeni računi po državah. Zaradi številnih zakonov je to morda potrebno, vendar ta arhitekturna shema samodejno zagotavlja takšno možnost

Priročni arhitekturni vzorci

Če želite prenesti podatke iz enega pomnilnika v drugega, standardna sredstva niso več dovolj. Na žalost morate v tem primeru ustaviti čakalno vrsto, izvesti selitev in jo nato zagnati. V splošnem primeru podatkov ni mogoče prenašati "sproti", če pa je čakalna vrsta dogodkov v celoti shranjena in imate posnetke prejšnjih stanj shranjevanja, potem lahko dogodke ponovno predvajamo na naslednji način:

  • V viru dogodka ima vsak dogodek svoj identifikator (v idealnem primeru ne padajočega). To pomeni, da lahko v shrambo dodamo polje – ID zadnjega obdelanega elementa.
  • Čakalno vrsto podvojimo, da lahko vse dogodke obdelamo za več neodvisnih shramb (prva je tista, v kateri so podatki že shranjeni, druga pa je nova, a še prazna). Druga čakalna vrsta seveda še ni v obdelavi.
  • Zaženemo drugo čakalno vrsto (torej začnemo s ponovnim predvajanjem dogodkov).
  • Ko je nova čakalna vrsta razmeroma prazna (to pomeni, da je povprečna časovna razlika med dodajanjem elementa in njegovim pridobivanjem sprejemljiva), lahko začnete preklapljati bralnike v novo shrambo.

Kot lahko vidite, v našem sistemu nismo imeli in še vedno nimamo stroge doslednosti. Obstaja le morebitna doslednost, to je zagotovilo, da se dogodki obdelujejo v istem vrstnem redu (vendar z različnimi zakasnitvami). In z uporabo tega lahko razmeroma enostavno prenesemo podatke brez zaustavitve sistema na drugo stran sveta.

Če nadaljujemo naš primer spletnega shranjevanja datotek, nam takšna arhitektura že daje številne prednosti:

  • Objekte lahko na dinamičen način približamo uporabnikom. Tako lahko izboljšate kakovost storitev.
  • Nekatere podatke lahko shranimo znotraj podjetij. Na primer, uporabniki Enterprise pogosto zahtevajo, da se njihovi podatki shranijo v nadzorovanih podatkovnih centrih (da se izognejo uhajanju podatkov). S shadingom lahko to enostavno podpiramo. In naloga je še lažja, če ima stranka združljiv oblak (npr. Azure gostuje sam).
  • In najpomembneje je, da nam tega ni treba storiti. Konec koncev, za začetek bi bili kar zadovoljni z enim skladiščem za vse račune (da hitro začnemo delati). In ključna značilnost tega sistema je, da je, čeprav ga je mogoče razširiti, v začetni fazi precej preprost. Samo ni vam treba takoj napisati kode, ki deluje z milijoni ločenih neodvisnih čakalnih vrst itd. Če bo potrebno, bo to mogoče storiti v prihodnosti.

Gostovanje statične vsebine

Ta točka se morda zdi precej očitna, vendar je še vedno potrebna za bolj ali manj standardno naloženo aplikacijo. Njegovo bistvo je preprosto: vsa statična vsebina se ne distribuira iz istega strežnika, kjer se nahaja aplikacija, temveč iz posebnih, namenjenih posebej tej nalogi. Posledično se te operacije izvajajo hitreje (pogojni nginx streže datoteke hitreje in ceneje kot strežnik Java). Plus CDN arhitektura (Content Delivery omrežje) nam omogoča, da naše datoteke poiščemo bližje končnim uporabnikom, kar pozitivno vpliva na udobje dela s storitvijo.

Najenostavnejši in najbolj standardni primer statične vsebine je nabor skriptov in slik za spletno stran. Z njimi je vse preprosto - znani so vnaprej, nato se arhiv naloži na strežnike CDN, od koder se distribuirajo končnim uporabnikom.

Vendar pa lahko v resnici za statično vsebino uporabite pristop, ki je nekoliko podoben lambda arhitekturi. Vrnimo se k naši nalogi (spletna shramba datotek), pri kateri moramo datoteke distribuirati uporabnikom. Najenostavnejša rešitev je ustvariti storitev, ki za vsako zahtevo uporabnika opravi vsa potrebna preverjanja (avtorizacija ipd.), nato pa prenese datoteko direktno iz našega skladišča. Glavna pomanjkljivost tega pristopa je, da statično vsebino (in datoteka z določeno revizijo je pravzaprav statična vsebina) distribuira isti strežnik, ki vsebuje poslovno logiko. Namesto tega lahko naredite naslednji diagram:

  • Strežnik ponuja URL za prenos. Lahko je v obliki file_id + ključ, kjer je ključ mini digitalni podpis, ki daje pravico do dostopa do vira naslednjih XNUMX ur.
  • Datoteko distribuira preprost nginx z naslednjimi možnostmi:
    • Predpomnjenje vsebine. Ker se ta storitev lahko nahaja na ločenem strežniku, smo si za prihodnost pustili rezervo z možnostjo shranjevanja vseh najnovejših prenesenih datotek na disk.
    • Preverjanje ključa v času ustvarjanja povezave
  • Izbirno: obdelava pretočne vsebine. Na primer, če stisnemo vse datoteke v storitvi, lahko razpakiranje izvedemo neposredno v tem modulu. Posledično: IO operacije se izvajajo tam, kjer sodijo. Arhivar v Javi bo zlahka dodelil veliko dodatnega pomnilnika, vendar je lahko tudi prepis storitve s poslovno logiko v pogojnike Rust/C++ neučinkovit. V našem primeru se uporabljajo različni procesi (ali celo storitve), zato lahko precej učinkovito ločimo poslovno logiko in IO operacije.

Priročni arhitekturni vzorci

Ta shema ni zelo podobna distribuciji statične vsebine (saj celotnega statičnega paketa nekam ne naložimo), vendar se v resnici ta pristop ukvarja ravno z distribucijo nespremenljivih podatkov. Poleg tega je to shemo mogoče posplošiti na druge primere, kjer vsebina ni preprosto statična, ampak jo je mogoče predstaviti kot niz nespremenljivih in neizbrisljivih blokov (čeprav jih je mogoče dodati).

Kot drug primer (za okrepitev): če ste delali z Jenkins/TeamCity, potem veste, da sta obe rešitvi napisani v Javi. Oba sta procesa Java, ki obravnavata orkestracijo gradnje in upravljanje vsebine. Zlasti oba imata naloge, kot je "prenos datoteke/mape s strežnika." Kot primer: izdajanje artefaktov, prenos izvorne kode (ko agent ne prenese kode direktno iz repozitorija, ampak to namesto njega naredi strežnik), dostop do dnevnikov. Vse te naloge se razlikujejo po obremenitvi IO. To pomeni, da mora biti strežnik, odgovoren za kompleksno poslovno logiko, hkrati sposoben učinkovito potiskati velike tokove podatkov skozi sebe. In kar je najbolj zanimivo, je takšno operacijo mogoče delegirati na isti nginx po popolnoma enaki shemi (le da je treba k zahtevi dodati podatkovni ključ).

Vendar, če se vrnemo v naš sistem, dobimo podoben diagram:

Priročni arhitekturni vzorci

Kot lahko vidite, je sistem postal radikalno bolj zapleten. Zdaj to ni le mini proces, ki lokalno shranjuje datoteke. Zdaj ni potrebna najpreprostejša podpora, nadzor različice API-ja itd. Zato je po izrisu vseh diagramov najbolje podrobno oceniti, ali je razširljivost vredna stroškov. Če pa želite imeti možnost razširitve sistema (tudi za delo s še večjim številom uporabnikov), potem boste morali poseči po takšnih rešitvah. Toda posledično je sistem arhitekturno pripravljen na povečano obremenitev (skoraj vsako komponento je mogoče klonirati za horizontalno skaliranje). Sistem je mogoče posodobiti, ne da bi ga ustavili (samo nekatere operacije bodo nekoliko upočasnjene).

Kot sem rekel na samem začetku, so zdaj številne internetne storitve začele prejemati povečano obremenitev. In nekateri od njih so preprosto prenehali delovati pravilno. Pravzaprav so sistemi odpovedali ravno v trenutku, ko naj bi posel zaslužil. To pomeni, da je sistem namesto odložene dostave, namesto da bi strankam predlagal "načrtujte dostavo za prihodnje mesece", preprosto rekel "pojdite k svojim konkurentom". Pravzaprav je to cena nizke produktivnosti: izgube bodo nastale ravno takrat, ko bo dobiček največji.

Zaključek

Vsi ti pristopi so bili znani že prej. Isti VK že dolgo uporablja idejo o gostovanju statične vsebine za prikaz slik. Veliko spletnih iger uporablja shemo Sharding za razdelitev igralcev na regije ali za ločevanje lokacij igre (če je svet sam en). Pristop Event Sourcing se aktivno uporablja v e-pošti. Večina aplikacij za trgovanje, kjer se podatki nenehno prejemajo, je dejansko zgrajenih na pristopu CQRS, da bi lahko filtrirali prejete podatke. No, horizontalno skaliranje se že dolgo uporablja v številnih storitvah.

Najpomembneje pa je, da so vsi ti vzorci postali zelo enostavni za uporabo v sodobnih aplikacijah (če so seveda primerni). Oblaki takoj ponujajo Sharding in horizontalno skaliranje, kar je veliko lažje kot sami naročati različne namenske strežnike v različnih podatkovnih centrih. CQRS je postal veliko lažji, čeprav le zaradi razvoja knjižnic, kot je RX. Pred približno 10 leti je to lahko podpiralo redko spletno mesto. Event Sourcing je prav tako neverjetno enostavno nastaviti zahvaljujoč že pripravljenim vsebnikom z Apache Kafka. Pred 10 leti bi bila to inovacija, zdaj je nekaj običajnega. Enako je z gostovanjem statične vsebine: zaradi priročnejših tehnologij (vključno z dejstvom, da obstaja podrobna dokumentacija in velika baza odgovorov), je ta pristop postal še enostavnejši.

Posledično je izvedba številnih precej zapletenih arhitekturnih vzorcev postala veliko enostavnejša, kar pomeni, da je bolje, da si jo podrobneje ogledamo vnaprej. Če je bila v deset let stari aplikaciji katera od zgornjih rešitev opuščena zaradi visokih stroškov implementacije in delovanja, lahko zdaj, v novi aplikaciji ali po refaktoriranju, ustvarite storitev, ki bo že arhitekturno tako razširljiva ( v smislu zmogljivosti) in pripravljeni na nove zahteve strank (na primer za lokalizacijo osebnih podatkov).

In kar je najpomembneje: ne uporabljajte teh pristopov, če imate preprosto aplikacijo. Ja, so lepe in zanimive, ampak za spletno stran z najvišjim obiskom 100 ljudi se velikokrat znajdeš s klasičnim monolitom (vsaj na zunaj se da vse notri razdeliti na module itd.).

Vir: www.habr.com

Dodaj komentar