Prikladni arhitektonski obrasci

Hej Habr!

U svjetlu aktualnih događanja zbog koronavirusa, brojne internetske usluge počele su dobivati ​​povećano opterećenje. Na primjer, Jedan od britanskih trgovačkih lanaca jednostavno je zaustavio svoju stranicu za online naručivanje., jer nije bilo dovoljno kapaciteta. I nije uvijek moguće ubrzati poslužitelj jednostavnim dodavanjem moćnije opreme, ali zahtjevi klijenata moraju se obraditi (ili će ići konkurentima).

U ovom ću članku ukratko govoriti o popularnim praksama koje će vam omogućiti stvaranje brze usluge otporne na greške. Međutim, od mogućih shema razvoja odabrao sam samo one koji su trenutno jednostavan za korištenje. Za svaku stavku ili imate gotove biblioteke ili imate priliku riješiti problem pomoću platforme u oblaku.

Horizontalno skaliranje

Najjednostavnija i najpoznatija točka. Uobičajeno, dvije najčešće dvije sheme raspodjele opterećenja su vodoravno i okomito skaliranje. U prvom slučaju omogućujete usporedni rad usluga, raspoređujući tako opterećenje između njih. U drugom naručite snažnije poslužitelje ili optimizirajte kod.

Na primjer, uzet ću apstraktno skladište datoteka u oblaku, to jest neki analog OwnClouda, OneDrivea i tako dalje.

Standardna slika takvog sklopa nalazi se ispod, ali ona samo pokazuje složenost sustava. Uostalom, trebamo nekako sinkronizirati usluge. Što se događa ako korisnik spremi datoteku s tableta, a zatim je želi pogledati s telefona?

Prikladni arhitektonski obrasci
Razlika između pristupa: kod okomitog skaliranja spremni smo povećati snagu čvorova, a kod horizontalnog skaliranja spremni smo dodati nove čvorove za raspodjelu opterećenja.

CQRS

Odvajanje odgovornosti za naredbu Prilično važan obrazac, budući da omogućuje različitim klijentima ne samo povezivanje s različitim uslugama, već i primanje istih tokova događaja. Njegove prednosti nisu toliko očite za jednostavnu aplikaciju, ali je iznimno važan (i jednostavan) za prometnu uslugu. Njegova suština: dolazni i odlazni tokovi podataka ne bi se trebali presijecati. Odnosno, ne možete poslati zahtjev i očekivati ​​odgovor; umjesto toga šaljete zahtjev servisu A, ali primate odgovor od servisa B.

Prvi bonus ovog pristupa je mogućnost prekida veze (u širem smislu riječi) tijekom izvršavanja dugog zahtjeva. Na primjer, uzmimo više-manje standardni slijed:

  1. Klijent je poslao zahtjev poslužitelju.
  2. Poslužitelj je započeo dugu obradu.
  3. Poslužitelj je klijentu odgovorio rezultatom.

Zamislimo da je u točki 2 veza prekinuta (ili se mreža ponovno spojila, ili je korisnik otišao na drugu stranicu, prekidajući vezu). U tom će slučaju poslužitelju biti teško poslati odgovor korisniku s informacijom o tome što je točno obrađeno. Koristeći CQRS, redoslijed će biti malo drugačiji:

  1. Klijent se pretplatio na ažuriranja.
  2. Klijent je poslao zahtjev poslužitelju.
  3. Poslužitelj je odgovorio "zahtjev prihvaćen".
  4. Server je odgovorio rezultatom kroz kanal iz točke “1”.

Prikladni arhitektonski obrasci

Kao što vidite, shema je malo složenija. Štoviše, ovdje nedostaje intuitivni pristup zahtjev-odgovor. Međutim, kao što vidite, prekid veze tijekom obrade zahtjeva neće dovesti do pogreške. Štoviše, ako je korisnik zapravo povezan s uslugom s nekoliko uređaja (na primjer, s mobilnog telefona i s tableta), možete osigurati da odgovor dolazi na oba uređaja.

Zanimljivo je da kod za obradu dolaznih poruka postaje isti (ne 100%) kako za događaje na koje je utjecao sam klijent, tako i za druge događaje, uključujući one od drugih klijenata.

Međutim, u stvarnosti dobivamo dodatni bonus zbog činjenice da se jednosmjernim protokom može rukovati u funkcionalnom stilu (koristeći RX i slično). I to je već ozbiljan plus, budući da se u biti aplikacija može učiniti potpuno reaktivnom, a također i korištenjem funkcionalnog pristupa. Za masne programe ovo može značajno uštedjeti resurse za razvoj i podršku.

Ako ovaj pristup kombiniramo s horizontalnim skaliranjem, tada kao bonus dobivamo mogućnost slanja zahtjeva jednom poslužitelju i primanja odgovora od drugog. Dakle, klijent može odabrati uslugu koja mu odgovara, a sustav unutar njega i dalje će moći ispravno obrađivati ​​događaje.

Izvor događaja

Kao što znate, jedna od glavnih značajki distribuiranog sustava je nepostojanje zajedničkog vremena, zajedničkog kritičnog odjeljka. Za jedan proces možete napraviti sinkronizaciju (na istim muteksima), unutar koje ste sigurni da nitko drugi ne izvršava ovaj kod. Međutim, to je opasno za distribuirani sustav, jer će zahtijevati dodatne troškove, a također će ubiti svu ljepotu skaliranja - sve komponente će i dalje čekati jednu.

Odavde dobivamo važnu činjenicu - brzo distribuirani sustav se ne može sinkronizirati, jer ćemo tada smanjiti performanse. S druge strane, često nam je potrebna određena dosljednost između komponenti. A za ovo možete koristiti pristup sa eventualna dosljednost, pri čemu je zajamčeno da će svi upiti vratiti posljednju ažuriranu vrijednost ako nema promjena podataka određeno vrijeme nakon zadnjeg ažuriranja („eventualno”).

Važno je razumjeti da se za klasične baze podataka dosta često koristi jaka konzistencija, gdje svaki čvor ima iste informacije (ovo se često postiže u slučaju kada se transakcija smatra uspostavljenom tek nakon što drugi poslužitelj odgovori). Ovdje ima nekih opuštanja zbog razine izolacije, ali opća ideja ostaje ista - možete živjeti u potpuno harmoniziranom svijetu.

Ipak, vratimo se izvornom zadatku. Ako se dio sustava može izgraditi s eventualna dosljednost, tada možemo konstruirati sljedeći dijagram.

Prikladni arhitektonski obrasci

Važne značajke ovog pristupa:

  • Svaki dolazni zahtjev stavlja se u jedan red čekanja.
  • Tijekom obrade zahtjeva, usluga također može staviti zadatke u druge redove.
  • Svaki dolazni događaj ima identifikator (koji je neophodan za deduplikaciju).
  • Red ideološki radi prema shemi "samo dodavanje". Ne možete uklanjati elemente iz njega niti ih mijenjati.
  • Red čekanja radi prema FIFO shemi (oprostite na tautologiji). Ako trebate raditi paralelno izvođenje, tada biste u jednoj fazi trebali premjestiti objekte u različite redove čekanja.

Dopustite mi da vas podsjetim da razmatramo slučaj mrežne pohrane datoteka. U ovom slučaju sustav će izgledati otprilike ovako:

Prikladni arhitektonski obrasci

Važno je da usluge u dijagramu ne znače nužno poseban poslužitelj. Čak i postupak može biti isti. Bitno je još nešto: ideološki su te stvari razdvojene na način da se lako može primijeniti horizontalno skaliranje.

A za dva korisnika dijagram će izgledati ovako (usluge namijenjene različitim korisnicima označene su različitim bojama):

Prikladni arhitektonski obrasci

Bonusi od takve kombinacije:

  • Usluge obrade informacija su odvojene. Redovi su također odvojeni. Ako trebamo povećati propusnost sustava, onda samo trebamo pokrenuti više usluga na više poslužitelja.
  • Kada primimo informaciju od korisnika, ne moramo čekati da se podaci u potpunosti pohrane. Naprotiv, samo trebamo odgovoriti "u redu" i onda postupno početi raditi. U isto vrijeme, red čekanja izglađuje vrhove, jer se dodavanje novog objekta događa brzo, a korisnik ne mora čekati potpuni prolaz kroz cijeli ciklus.
  • Kao primjer, dodao sam uslugu deduplikacije koja pokušava spojiti identične datoteke. Ako radi dugo u 1% slučajeva, klijent to jedva da će primijetiti (vidi gore), što je veliki plus jer se od nas više ne traži stopostotna brzina i pouzdanost.

Međutim, nedostaci su odmah vidljivi:

  • Naš je sustav izgubio svoju strogu dosljednost. To znači da ako se, na primjer, pretplatite na različite usluge, tada teoretski možete dobiti drugačije stanje (budući da jedna od usluga možda neće imati vremena primiti obavijest iz internog reda čekanja). Kao još jedna posljedica, sustav sada nema zajedničko vrijeme. To jest, nemoguće je, na primjer, sortirati sve događaje jednostavno prema vremenu dolaska, budući da satovi između poslužitelja možda nisu sinkronizirani (štoviše, isto vrijeme na dva poslužitelja je utopija).
  • Nijedan događaj sada se ne može jednostavno vratiti (kao što bi se moglo učiniti s bazom podataka). Umjesto toga, trebate dodati novi događaj − kompenzacijski događaj, koji će promijeniti posljednje stanje u traženo. Kao primjer iz sličnog područja: bez ponovnog pisanja povijesti (što je loše u nekim slučajevima), ne možete vratiti commit u git-u, ali možete napraviti poseban povrat počiniti, koji u biti samo vraća staro stanje. Međutim, i pogrešna predaja i vraćanje ostat će u povijesti.
  • Shema podataka može se mijenjati od izdanja do izdanja, ali stari događaji se više neće moći ažurirati na novi standard (budući da se događaji u načelu ne mogu mijenjati).

Kao što vidite, Event Sourcing dobro funkcionira s CQRS-om. Štoviše, implementacija sustava s učinkovitim i praktičnim redovima čekanja, ali bez odvajanja protoka podataka, već je sama po sebi teška, jer ćete morati dodati sinkronizacijske točke koje će neutralizirati cjelokupni pozitivni učinak redova čekanja. Primjenom oba pristupa odjednom, potrebno je malo prilagoditi programski kod. U našem slučaju, prilikom slanja datoteke na poslužitelj, odgovor dolazi samo "ok", što samo znači da je "operacija dodavanja datoteke spremljena." Formalno, to ne znači da su podaci već dostupni na drugim uređajima (na primjer, usluga deduplikacije može ponovno izgraditi indeks). Međutim, nakon nekog vremena klijent će dobiti obavijest u stilu "datoteka X je spremljena".

Kao rezultat:

  • Povećava se broj statusa slanja datoteke: umjesto klasičnog “datoteka poslana” dobivamo dva: “datoteka je dodana u red čekanja na poslužitelju” i “datoteka je spremljena u pohranu”. Potonje znači da drugi uređaji već mogu početi primati datoteku (prilagođeno činjenici da redovi čekanja rade različitim brzinama).
  • Zbog činjenice da informacije o podnošenju sada dolaze različitim kanalima, moramo smisliti rješenja za primanje statusa obrade datoteke. Kao posljedica toga: za razliku od klasičnog zahtjeva-odgovora, klijent se može ponovno pokrenuti tijekom obrade datoteke, ali će status same obrade biti ispravan. Štoviše, ova stavka radi, u biti, izvan kutije. Kao posljedica toga: sada smo tolerantniji na neuspjehe.

Oštrenje

Kao što je gore opisano, sustavima izvora događaja nedostaje stroga dosljednost. To znači da možemo koristiti nekoliko skladišta bez ikakve sinkronizacije među njima. Pristupajući našem problemu, možemo:

  • Odvojite datoteke po vrsti. Na primjer, slike/video zapisi se mogu dekodirati i može se odabrati učinkovitiji format.
  • Odvojeni računi po zemlji. Zbog mnogih zakona to može biti potrebno, ali ova arhitektonska shema automatski pruža takvu mogućnost

Prikladni arhitektonski obrasci

Ako želite prenijeti podatke iz jednog skladišta u drugo, standardna sredstva više nisu dovoljna. Nažalost, u ovom slučaju morate zaustaviti red čekanja, obaviti migraciju i zatim je pokrenuti. U općem slučaju, podaci se ne mogu prenositi "u hodu", međutim, ako je red događaja u potpunosti pohranjen i imate snimke prethodnih stanja pohrane, tada možemo ponovno reproducirati događaje na sljedeći način:

  • U izvoru događaja svaki događaj ima svoj identifikator (idealno, neopadajući). To znači da možemo dodati polje u pohranu - ID zadnjeg obrađenog elementa.
  • Dupliciramo red čekanja tako da se svi događaji mogu obraditi za nekoliko neovisnih pohrana (prva je ona u kojoj su podaci već pohranjeni, a druga je nova, ali još uvijek prazna). Drugi red čekanja, naravno, još nije u obradi.
  • Pokrećemo drugi red čekanja (tj. počinjemo ponavljati događaje).
  • Kada je novi red čekanja relativno prazan (to jest, prosječna vremenska razlika između dodavanja elementa i njegovog dohvaćanja je prihvatljiva), možete početi prebacivati ​​čitače na novu pohranu.

Kao što vidite, nismo imali, a i danas nemamo, strogu dosljednost u našem sustavu. Postoji samo eventualna dosljednost, odnosno jamstvo da se događaji obrađuju istim redoslijedom (ali po mogućnosti s različitim kašnjenjima). I, koristeći ovo, možemo relativno lako prenijeti podatke bez zaustavljanja sustava na drugu stranu zemaljske kugle.

Stoga, nastavljajući naš primjer o mrežnoj pohrani datoteka, takva nam arhitektura već daje niz bonusa:

  • Objekte možemo na dinamičan način približiti korisnicima. Na taj način možete poboljšati kvalitetu usluge.
  • Neke podatke možemo pohraniti unutar tvrtki. Na primjer, Enterprise korisnici često zahtijevaju da se njihovi podaci pohranjuju u kontroliranim podatkovnim centrima (kako bi se izbjeglo curenje podataka). Kroz dijeljenje to možemo lako podržati. A zadatak je još lakši ako korisnik ima kompatibilan oblak (npr. Azure se samostalno hostira).
  • I najvažnije je da to ne moramo raditi. Uostalom, za početak, bili bismo sasvim zadovoljni jednom pohranom za sve račune (da brzo počnemo raditi). A ključna značajka ovog sustava je da je, iako je proširiv, u početnoj fazi vrlo jednostavan. Samo ne morate odmah napisati kod koji radi s milijunima zasebnih neovisnih redova itd. Ako je potrebno, to se može učiniti u budućnosti.

Hosting statičnog sadržaja

Ova se točka može činiti prilično očitom, ali je još uvijek neophodna za više-manje standardno učitanu aplikaciju. Njegova je bit jednostavna: sav statički sadržaj distribuira se ne s istog poslužitelja na kojem se nalazi aplikacija, već s posebnih posvećenih ovom zadatku. Kao rezultat toga, te se operacije izvode brže (uvjetni nginx poslužuje datoteke brže i jeftinije od Java poslužitelja). Plus CDN arhitektura (Sadržaj isporuke mreže) omogućuje nam da svoje datoteke lociramo bliže krajnjim korisnicima, što pozitivno utječe na udobnost rada s uslugom.

Najjednostavniji i najstandardniji primjer statičkog sadržaja je skup skripti i slika za web mjesto. S njima je sve jednostavno - unaprijed se znaju, zatim se arhiva postavlja na CDN poslužitelje, odakle se distribuiraju krajnjim korisnicima.

Međutim, u stvarnosti, za statički sadržaj možete koristiti pristup donekle sličan lambda arhitekturi. Vratimo se našem zadatku (mrežna pohrana datoteka), u kojem trebamo distribuirati datoteke korisnicima. Najjednostavnije rješenje je napraviti servis koji za svaki zahtjev korisnika radi sve potrebne provjere (autorizacija i sl.), a zatim preuzima datoteku direktno iz naše pohrane. Glavni nedostatak ovakvog pristupa je što statički sadržaj (a datoteka s određenom revizijom je zapravo statički sadržaj) distribuira isti poslužitelj koji sadrži poslovnu logiku. Umjesto toga, možete napraviti sljedeći dijagram:

  • Poslužitelj pruža URL za preuzimanje. Može biti u obliku file_id + ključ, gdje je ključ mini-digitalni potpis koji daje pravo pristupa resursu sljedeća 24 sata.
  • Datoteku distribuira jednostavan nginx sa sljedećim opcijama:
    • Predmemoriranje sadržaja. Budući da se ovaj servis može nalaziti na zasebnom poslužitelju, ostavili smo si rezervu za budućnost uz mogućnost pohranjivanja svih najnovijih preuzetih datoteka na disk.
    • Provjera ključa u trenutku stvaranja veze
  • Dodatno: obrada strujanja sadržaja. Na primjer, ako komprimiramo sve datoteke u servisu, tada možemo raspakirati izravno u ovom modulu. Kao posljedica toga: IO operacije se rade tamo gdje pripadaju. Program za arhiviranje u Javi lako će dodijeliti puno dodatne memorije, ali prepisivanje usluge s poslovnom logikom u uvjete Rust/C++ također može biti neučinkovito. U našem slučaju koriste se različiti procesi (ili čak usluge), pa stoga možemo prilično učinkovito razdvojiti poslovnu logiku i IO operacije.

Prikladni arhitektonski obrasci

Ova shema nije jako slična distribuciji statičkog sadržaja (budući da ne učitavamo cijeli statički paket negdje), ali u stvarnosti se ovaj pristup bavi upravo distribucijom nepromjenjivih podataka. Štoviše, ova se shema može generalizirati na druge slučajeve gdje sadržaj nije jednostavno statičan, već se može predstaviti kao skup nepromjenjivih blokova koji se ne mogu izbrisati (iako se mogu dodati).

Kao još jedan primjer (za pojačanje): ako ste radili s Jenkins/TeamCity, onda znate da su oba rješenja napisana u Javi. Oba su Java proces koji upravlja i orkestracijom izgradnje i upravljanjem sadržajem. Konkretno, obojica imaju zadatke poput "prijenos datoteke/mape s poslužitelja." Kao primjer: izdavanje artefakata, prijenos izvornog koda (kada agent ne preuzima kod izravno iz repozitorija, već to umjesto njega radi poslužitelj), pristup zapisima. Svi ovi zadaci razlikuju se po svom IO opterećenju. Odnosno, ispada da poslužitelj koji je odgovoran za složenu poslovnu logiku mora u isto vrijeme moći učinkovito progurati velike tokove podataka kroz sebe. I ono što je najzanimljivije je da se takva operacija može delegirati istom nginxu prema potpuno istoj shemi (osim što se podatkovni ključ treba dodati zahtjevu).

Međutim, vratimo li se našem sustavu, dobit ćemo sličan dijagram:

Prikladni arhitektonski obrasci

Kao što vidite, sustav je postao radikalno složeniji. Sada to nije samo mini-proces koji lokalno pohranjuje datoteke. Ono što je sada potrebno nije najjednostavnija podrška, kontrola verzije API-ja itd. Stoga, nakon što su nacrtani svi dijagrami, najbolje je detaljno procijeniti je li proširenje vrijedno tog troška. Međutim, ako želite moći proširiti sustav (uključujući rad s još većim brojem korisnika), tada ćete morati ići na slična rješenja. Ali, kao rezultat toga, sustav je arhitektonski spreman za povećano opterećenje (gotovo svaka komponenta može se klonirati za horizontalno skaliranje). Sustav se može ažurirati bez zaustavljanja (jednostavno će se neke operacije malo usporiti).

Kao što sam rekao na samom početku, sada su brojne internetske usluge počele dobivati ​​povećano opterećenje. A neki od njih su jednostavno počeli prestati raditi ispravno. Zapravo, sustavi su zakazali upravo u trenutku kada je posao trebao zarađivati. To jest, umjesto odgođene isporuke, umjesto sugeriranja kupcima "planirajte svoju isporuku za naredne mjesece", sustav je jednostavno rekao "idite svojim konkurentima". Zapravo, to je cijena niske produktivnosti: gubici će se dogoditi upravo onda kada će profiti biti najveći.

Zaključak

Svi ovi pristupi bili su poznati i prije. Isti VK već dugo koristi ideju hostinga statičnog sadržaja za prikaz slika. Mnoge online igre koriste shemu Sharding za podjelu igrača u regije ili za odvajanje lokacija u igri (ako je sam svijet jedan). Event Sourcing pristup se aktivno koristi u e-pošti. Većina aplikacija za trgovanje gdje se podaci stalno primaju zapravo je izgrađena na CQRS pristupu kako bi se mogli filtrirati primljeni podaci. Pa, horizontalno skaliranje koristi se u mnogim uslugama već dosta dugo.

Međutim, što je najvažnije, sve te uzorke postalo je vrlo lako primijeniti u modernim aplikacijama (ako su prikladni, naravno). Oblaci odmah nude Sharding i horizontalno skaliranje, što je mnogo lakše nego da sami naručite različite namjenske poslužitelje u različitim podatkovnim centrima. CQRS je postao mnogo lakši, makar samo zbog razvoja biblioteka kao što je RX. Prije otprilike 10 godina, rijetka web stranica je to mogla podržati. Event Sourcing je također nevjerojatno jednostavan za postavljanje zahvaljujući gotovim spremnicima s Apache Kafka. Prije 10 godina ovo bi bila inovacija, sada je uobičajeno. Isto je i s hostingom statičnog sadržaja: zbog praktičnijih tehnologija (uključujući činjenicu da postoji detaljna dokumentacija i velika baza odgovora), ovaj je pristup postao još jednostavniji.

Kao rezultat toga, implementacija niza prilično složenih arhitektonskih obrazaca sada je postala mnogo jednostavnija, što znači da je bolje unaprijed pogledati pobliže. Ako je u deset godina staroj aplikaciji jedno od gore navedenih rješenja napušteno zbog visokih troškova implementacije i rada, sada, u novoj aplikaciji ili nakon refaktoriranja, možete kreirati uslugu koja će već biti arhitektonski i proširiva ( u smislu izvedbe) i spremni za nove zahtjeve klijenata (na primjer, za lokalizaciju osobnih podataka).

I što je najvažnije: nemojte koristiti ove pristupe ako imate jednostavnu aplikaciju. Da, lijepi su i zanimljivi, ali za stranicu s vršnom posjetom od 100 ljudi često se može proći s klasičnim monolitom (barem izvana, unutra se sve može podijeliti na module itd.).

Izvor: www.habr.com

Dodajte komentar