Pogodni arhitektonski uzorci

Hej Habr!

U svjetlu trenutnih dešavanja usljed koronavirusa, veliki broj internetskih servisa počeo je da dobija povećano opterećenje. Na primjer, Jedan od maloprodajnih lanaca u Velikoj Britaniji jednostavno je zaustavio svoju stranicu za online naručivanje., jer nije bilo dovoljno kapaciteta. I nije uvijek moguće ubrzati server jednostavnim dodavanjem moćnije opreme, ali zahtjevi klijenata moraju biti obrađeni (ili će ići konkurentima).

U ovom članku ću ukratko govoriti o popularnim praksama koje će vam omogućiti da napravite brzu uslugu otpornu na greške. Međutim, od mogućih razvojnih šema odabrao sam samo one koje su trenutno jednostavan za korištenje. Za svaku stavku imate ili gotove biblioteke, ili imate priliku da riješite problem koristeći cloud platformu.

Horizontalno skaliranje

Najjednostavnija i najpoznatija tačka. Konvencionalno, najčešće dvije sheme raspodjele opterećenja su horizontalno i vertikalno skaliranje. U prvom slučaju dozvoljavate servisima da rade paralelno, čime se raspoređuje opterećenje između njih. U drugom naručite moćnije servere ili optimizirate kod.

Na primjer, uzeću apstraktnu pohranu datoteka u oblaku, odnosno neki analog OwnCloud-a, OneDrive-a i tako dalje.

Standardna slika takvog kola je ispod, ali ona samo pokazuje složenost sistema. Na kraju krajeva, moramo nekako uskladiti usluge. Šta se dešava ako korisnik sačuva datoteku sa tableta, a zatim želi da je pogleda sa telefona?

Pogodni arhitektonski uzorci
Razlika između pristupa: u vertikalnom skaliranju spremni smo povećati snagu čvorova, au horizontalnom skaliranju spremni smo dodati nove čvorove za raspodjelu opterećenja.

CQRS

Odvajanje odgovornosti za upit naredbe Prilično važan obrazac, jer omogućava različitim klijentima ne samo da se povežu na različite usluge, već i da primaju iste tokove događaja. Njegove prednosti nisu toliko očigledne za jednostavnu aplikaciju, ali je izuzetno važno (i jednostavno) za užurbanu uslugu. Njegova suština: dolazni i odlazni tokovi podataka ne bi se trebali ukrštati. Odnosno, ne možete poslati zahtjev i očekivati ​​odgovor; umjesto toga, šaljete zahtjev servisu A, ali dobijate odgovor od servisa B.

Prvi bonus ovog pristupa je mogućnost prekida veze (u širem smislu te riječi) dok se izvršava dug zahtjev. Na primjer, uzmimo manje-više standardni niz:

  1. Klijent je poslao zahtjev serveru.
  2. Server je započeo dugo vrijeme obrade.
  3. Server je odgovorio klijentu sa rezultatom.

Zamislimo da je u tački 2 veza prekinuta (ili se mreža ponovo povezala, ili je korisnik otišao na drugu stranicu, prekinuvši vezu). U ovom slučaju, serveru će biti teško da pošalje odgovor korisniku sa informacijama o tome šta je tačno obrađeno. Koristeći CQRS, redoslijed će biti malo drugačiji:

  1. Klijent se pretplatio na ažuriranja.
  2. Klijent je poslao zahtjev serveru.
  3. Server je odgovorio "zahtjev prihvaćen."
  4. Server je odgovorio rezultatom putem kanala iz tačke “1”.

Pogodni arhitektonski uzorci

Kao što vidite, shema je malo složenija. Štaviše, ovdje nedostaje intuitivni pristup zahtjev-odgovor. Međutim, kao što vidite, prekid veze tokom obrade zahtjeva neće dovesti do greške. Štoviše, ako je korisnik zapravo povezan s uslugom s nekoliko uređaja (na primjer, s mobilnog telefona i s tableta), možete se pobrinuti da odgovor dođe 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 i one od drugih klijenata.

Međutim, u stvarnosti dobijamo dodatni bonus zbog činjenice da se jednosmjernim protokom može upravljati u funkcionalnom stilu (koristeći RX i slično). I to je već ozbiljan plus, jer se u suštini 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 kombiniramo ovaj pristup s horizontalnim skaliranjem, tada kao bonus dobivamo mogućnost slanja zahtjeva na jedan server i primanja odgovora od drugog. Tako klijent može izabrati uslugu koja mu odgovara, a sistem će i dalje moći ispravno da obrađuje događaje.

Event Sourcing

Kao što znate, jedna od glavnih karakteristika distribuiranog sistema je odsustvo zajedničkog vremena, zajedničkog kritičnog dijela. Za jedan proces možete izvršiti sinhronizaciju (na istim muteksima), u okviru koje ste sigurni da niko drugi ne izvršava ovaj kod. Međutim, ovo je opasno za distribuirani sistem, jer će zahtijevati dodatne troškove, a također će ubiti svu ljepotu skaliranja - sve komponente će i dalje čekati na jednu.

Odavde dobijamo važnu činjenicu - brzi distribuirani sistem se ne može sinhronizovati, jer ćemo tada smanjiti performanse. S druge strane, često nam je potrebna određena konzistentnost između komponenti. A za ovo možete koristiti pristup sa konačnu konzistentnost, pri čemu je zagarantovano da ako nema promjena podataka u određenom vremenskom periodu nakon posljednjeg ažuriranja (“eventualno”), svi upiti će vratiti posljednju ažuriranu vrijednost.

Važno je shvatiti da se za klasične baze podataka prilično često koristi jaka konzistencija, pri čemu svaki čvor ima iste informacije (ovo se često postiže u slučaju kada se transakcija smatra uspostavljenom tek nakon što drugi server odgovori). Ovdje postoje neke relaksacije zbog nivoa izolacije, ali generalna ideja ostaje ista - možete živjeti u potpuno harmoniziranom svijetu.

Međutim, vratimo se prvobitnom zadatku. Ako se dio sistema može izgraditi sa konačnu konzistentnost, tada možemo konstruirati sljedeći dijagram.

Pogodni arhitektonski uzorci

Važne karakteristike ovog pristupa:

  • Svaki dolazni zahtjev stavlja se u jedan red čekanja.
  • Dok obrađuje zahtjev, usluga također može postaviti zadatke u druge redove čekanja.
  • Svaki dolazni događaj ima identifikator (koji je neophodan za deduplikaciju).
  • Red ideološki radi prema shemi „samo dodaj“. Ne možete ukloniti elemente iz njega ili ih preurediti.
  • Red radi prema FIFO šemi (izvinite na tautologiji). Ako trebate raditi paralelno izvršavanje, tada bi u jednoj fazi trebali premjestiti objekte u različite redove.

Da vas podsjetim da razmatramo slučaj skladištenja datoteka na mreži. U ovom slučaju, sistem će izgledati otprilike ovako:

Pogodni arhitektonski uzorci

Važno je da usluge na dijagramu ne znače nužno poseban server. Čak i proces može biti isti. Još jedna stvar je važna: ideološki, te stvari su razdvojene na način da se horizontalno skaliranje može lako primijeniti.

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

Pogodni arhitektonski uzorci

Bonusi od takve kombinacije:

  • Usluge obrade informacija su odvojene. Redovi su takođe odvojeni. Ako trebamo povećati propusnost sistema, onda samo trebamo pokrenuti više usluga na više servera.
  • Kada dobijemo informaciju od korisnika, ne moramo čekati da se podaci u potpunosti sačuvaju. Naprotiv, samo treba da odgovorimo „ok” i onda postepeno da počnemo da radimo. U isto vrijeme, red izglađuje vrhove, jer se dodavanje novog objekta događa brzo, a korisnik ne mora čekati potpuni prolazak kroz cijeli ciklus.
  • Kao primjer, dodao sam uslugu deduplikacije koja pokušava spojiti identične datoteke. Ako dugo radi u 1% slučajeva, klijent će to jedva primijetiti (vidi gore), što je veliki plus, jer se više ne traži da budemo XNUMX% brzi i pouzdani.

Međutim, nedostaci su odmah vidljivi:

  • Naš sistem je izgubio svoju strogu konzistentnost. To znači da ako se, na primjer, pretplatite na različite usluge, teoretski možete dobiti drugačije stanje (pošto jedna od usluga možda neće imati vremena da primi obavijest iz internog reda čekanja). Kao druga posljedica, sistem sada nema zajedničko vrijeme. Odnosno, nemoguće je, na primjer, sve događaje sortirati jednostavno po vremenu dolaska, jer satovi između servera možda nisu sinhroni (štaviše, isto vrijeme na dva servera je utopija).
  • Nijedan događaj se sada ne može jednostavno vratiti (kao što se može učiniti s bazom podataka). Umjesto toga, trebate dodati novi događaj − kompenzacijski događaj, što ć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 urezivanje u git-u, ali možete napraviti poseban rollback urezivanje, što u suštini samo vraća staro stanje. Međutim, i pogrešno urezivanje i vraćanje ostat će u istoriji.
  • Šema podataka može se mijenjati od izdanja do izdanja, ali stari događaji više neće moći biti ažurirani na novi standard (pošto se događaji u principu ne mogu mijenjati).

Kao što vidite, Event Sourcing dobro funkcioniše sa CQRS. Štaviše, implementacija sistema sa efikasnim i praktičnim redovima, ali bez razdvajanja tokova podataka, već je sama po sebi teška, jer ćete morati da dodate tačke sinhronizacije koje će neutralisati ceo pozitivan efekat redova. Primjenjujući oba pristupa odjednom, potrebno je malo prilagoditi programski kod. U našem slučaju, prilikom slanja fajla na server, odgovor dolazi samo „ok“, što samo znači da je „operacija dodavanja fajla sačuvana“. Formalno, to ne znači da su podaci već dostupni na drugim uređajima (na primjer, usluga deduplikacije može ponovo izgraditi indeks). Međutim, nakon nekog vremena, klijent će dobiti obavijest u stilu „datoteka X je sačuvana“.

Kao rezultat:

  • Broj statusa slanja datoteka se povećava: umjesto klasičnog „datoteka poslana“, dobijamo dva: „fajl je dodat u red na serveru“ i „datoteka je sačuvana u memoriji“. Ovo posljednje znači da drugi uređaji već mogu početi primati fajl (prilagođeno činjenici da redovi rade različitim brzinama).
  • Zbog činjenice da informacije o podnošenju sada dolaze kroz različite kanale, moramo smisliti rješenja kako bismo dobili status obrade datoteke. Kao posljedica ovoga: za razliku od klasičnog zahtjeva-odgovora, klijent se može ponovo pokrenuti dok obrađuje datoteku, ali će status same obrade biti ispravan. Štaviše, ova stavka funkcionira, u suštini, izvan kutije. Kao posljedica toga: sada smo tolerantniji na neuspjehe.

sharding

Kao što je gore opisano, sistemima izvora događaja nedostaje stroga konzistentnost. To znači da možemo koristiti nekoliko skladišta bez ikakve sinhronizacije između njih. Približavajući se našem problemu, možemo:

  • Odvojite fajlove po vrsti. Na primjer, slike/video zapisi se mogu dekodirati i odabrati efikasniji format.
  • Odvojite račune po zemljama. Zbog mnogih zakona, ovo može biti potrebno, ali ova šema arhitekture pruža takvu mogućnost automatski

Pogodni arhitektonski uzorci

Ako želite prenijeti podatke iz jednog skladišta u drugi, standardna sredstva više nisu dovoljna. Nažalost, u ovom slučaju, morate zaustaviti red čekanja, izvršiti migraciju, a zatim ga pokrenuti. U općenitom slučaju, podaci se ne mogu prenijeti „u hodu“, međutim, ako je red događaja pohranjen u potpunosti, a imate snimke prethodnih stanja pohrane, tada možemo ponoviti 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 skladište - id posljednjeg obrađenog elementa.
  • Dupliciramo red tako da se svi događaji mogu obraditi za nekoliko nezavisnih skladišta (prvo je ono u kojem su podaci već pohranjeni, a drugo je novo, ali još uvijek prazno). Drugi red se, naravno, još ne obrađuje.
  • Pokrećemo drugi red čekanja (tj. počinjemo reproducirati događaje).
  • Kada je novi red relativno prazan (to jest, prosječna vremenska razlika između dodavanja elementa i njegovog preuzimanja je prihvatljiva), možete početi prebacivati ​​čitače na novu memoriju.

Kao što vidite, mi nismo imali, niti imamo, strogu konzistentnost u našem sistemu. Postoji samo eventualna dosljednost, odnosno garancija da se događaji obrađuju istim redoslijedom (ali moguće s različitim kašnjenjima). I, koristeći ovo, možemo relativno lako prenijeti podatke bez zaustavljanja sistema na drugu stranu svijeta.

Dakle, nastavljajući naš primjer o online pohrani za datoteke, takva arhitektura nam već daje brojne bonuse:

  • Objekte možemo pomicati bliže korisnicima na dinamičan način. Na ovaj način možete poboljšati kvalitet usluge.
  • Neke podatke možemo pohraniti unutar kompanija. Na primjer, Enterprise korisnici često zahtijevaju da se njihovi podaci pohranjuju u kontroliranim podatkovnim centrima (kako bi izbjegli curenje podataka). Shardingom to možemo lako podržati. A zadatak je još lakši ako korisnik ima kompatibilan oblak (npr. Azure self hosted).
  • I najvažnije je da ovo ne moramo da radimo. Na kraju krajeva, za početak, bili bismo sasvim zadovoljni sa jednom pohranom za sve račune (da bismo brzo počeli s radom). A ključna karakteristika ovog sistema je da je, iako je proširiv, u početnoj fazi prilično jednostavan. Jednostavno ne morate odmah pisati kod koji radi sa milion zasebnih nezavisnih redova itd. Ako je potrebno, to se može učiniti u budućnosti.

Hosting statičkog sadržaja

Ova točka može izgledati prilično očigledna, ali je i dalje neophodna za više ili manje standardno učitanu aplikaciju. Njegova suština je jednostavna: sav statički sadržaj se distribuira ne sa istog servera na kojem se nalazi aplikacija, već sa posebnih posvećenih ovom zadatku. Kao rezultat toga, ove operacije se izvode brže (uslovni nginx opslužuje datoteke brže i jeftinije od Java servera). Plus CDN arhitektura (Network Delivery Network) omogućava nam da svoje datoteke lociramo bliže krajnjim korisnicima, što pozitivno utiče na praktičnost rada sa servisom.

Najjednostavniji i najstandardniji primjer statičkog sadržaja je skup skripti i slika za web stranicu. Kod njih je sve jednostavno - unaprijed su poznati, zatim se arhiva učitava na CDN servere, odakle se distribuira 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 (online skladištenje datoteka), u kojem trebamo distribuirati datoteke korisnicima. Najjednostavnije rješenje je kreiranje servisa koji za svaki zahtjev korisnika radi sve potrebne provjere (autorizacija i sl.), a zatim preuzima fajl direktno iz našeg skladišta. Glavni nedostatak ovog pristupa je u tome što se statički sadržaj (a fajl sa određenom revizijom je, u stvari, statički sadržaj) distribuira od strane istog servera koji sadrži poslovnu logiku. Umjesto toga, možete napraviti sljedeći dijagram:

  • Server daje URL za preuzimanje. Može biti u obliku file_id + ključ, gdje je ključ mini-digitalni potpis koji daje pravo pristupa resursu u naredna 24 sata.
  • Datoteka se distribuira jednostavnim nginxom sa sljedećim opcijama:
    • Keširanje sadržaja. Budući da ovaj servis može biti smješten na zasebnom serveru, ostavili smo sebi rezervu za budućnost sa mogućnošću pohranjivanja svih najnovijih preuzetih datoteka na disk.
    • Provjera ključa u trenutku kreiranja veze
  • Opciono: obrada streaming sadržaja. Na primjer, ako komprimujemo sve datoteke u servisu, onda možemo izvršiti raspakivanje direktno u ovom modulu. Kao posljedica toga: IO operacije se rade tamo gdje im je mjesto. Arhiver u Javi će lako dodijeliti mnogo dodatne memorije, ali prepisivanje usluge s poslovnom logikom u Rust/C++ uslovne može također biti neefikasno. U našem slučaju se koriste različiti procesi (pa čak i usluge) i stoga možemo prilično efikasno odvojiti poslovnu logiku i IO operacije.

Pogodni arhitektonski uzorci

Ova šema nije mnogo slična distribuciji statičkog sadržaja (pošto ne učitavamo negdje cijeli statički paket), ali se u stvarnosti ovaj pristup bavi upravo distribucijom nepromjenjivih podataka. Štaviše, ova šema se može generalizirati na druge slučajeve u kojima sadržaj nije jednostavno statičan, već se može predstaviti kao skup nepromjenjivih i neizbrisivih blokova (iako se mogu dodati).

Kao drugi primjer (za pojačanje): ako ste radili sa Jenkins/TeamCity, onda znate da su oba rješenja napisana u Javi. Oba su Java proces koji se bavi i orkestracijom izgradnje i upravljanjem sadržajem. Konkretno, obojica imaju zadatke kao što je "prenos datoteke/fascikle sa servera." Kao primjer: izdavanje artefakata, prijenos izvornog koda (kada agent ne preuzima kod direktno iz spremišta, već server to radi umjesto njega), pristup logovima. Svi ovi zadaci se razlikuju po svom IO opterećenju. Odnosno, ispada da server odgovoran za složenu poslovnu logiku mora istovremeno biti u stanju da efikasno progura velike tokove podataka kroz sebe. I ono što je najzanimljivije je da se takva operacija može delegirati istom nginx-u po potpuno istoj šemi (osim što u zahtjev treba dodati ključ podataka).

Međutim, ako se vratimo na naš sistem, dobićemo sličan dijagram:

Pogodni arhitektonski uzorci

Kao što vidite, sistem je postao radikalno složeniji. Sada to nije samo mini-proces koji pohranjuje datoteke lokalno. Ono što je sada potrebno nije najjednostavnija podrška, kontrola verzije API-ja itd. Stoga, nakon što su svi dijagrami nacrtani, najbolje je detaljno procijeniti da li je proširivost vrijedna troškova. Međutim, ako želite da budete u mogućnosti da proširite sistem (uključujući rad sa još većim brojem korisnika), onda ćete morati da se odlučite za slična rešenja. Ali, kao rezultat, sistem je arhitektonski spreman za povećano opterećenje (skoro svaka komponenta se može klonirati za horizontalno skaliranje). Sistem se može ažurirati bez zaustavljanja (jednostavno će neke operacije biti malo usporene).

Kao što sam rekao na samom početku, sada su brojni internet servisi počeli da dobijaju povećano opterećenje. A neki od njih su jednostavno počeli da prestaju da rade ispravno. Zapravo, sistemi su otkazali upravo u trenutku kada je posao trebalo da zaradi. Odnosno, umjesto odgođene isporuke, umjesto da sugeriše kupcima „isplanirajte isporuku za naredne mjesece“, sistem je jednostavno rekao „idite svojim konkurentima“. Zapravo, ovo je cijena niske produktivnosti: gubici će nastati upravo kada bi profit bio najveći.

zaključak

Svi ovi pristupi bili su poznati i ranije. Isti VK već dugo koristi ideju hostinga statičkog sadržaja za prikaz slika. Mnoge online igre koriste Sharding shemu za podjelu igrača na regije ili za razdvajanje lokacija za igru ​​(ako je svijet jedan). Pristup izvorima događaja se aktivno koristi u e-pošti. Većina aplikacija za trgovanje u kojima se podaci stalno primaju zapravo su izgrađene na CQRS pristupu kako bi se mogli filtrirati primljeni podaci. Pa, horizontalno skaliranje se koristi u mnogim servisima već duže vrijeme.

Međutim, što je najvažnije, svi su ovi obrasci postali vrlo laki za primjenu u modernim aplikacijama (ako su primjereni, naravno). Clouds odmah nudi Sharding i horizontalno skaliranje, što je mnogo lakše nego da sami naručite različite namjenske servere 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 mogla je to podržati. Event Sourcing je takođe neverovatno jednostavan za postavljanje zahvaljujući gotovim kontejnerima sa Apache Kafka. Prije 10 godina ovo bi bila inovacija, sada je uobičajeno. Isto je i sa statičnim hostingom sadržaja: zbog praktičnijih tehnologija (uključujući i činjenicu da postoji detaljna dokumentacija i velika baza podataka), ovaj pristup je 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 da je bolje pogledate unaprijed. Ako je u aplikaciji staroj deset godina jedno od gore navedenih rješenja napušteno zbog visoke cijene implementacije i rada, sada, u novoj aplikaciji, ili nakon refaktoriranja, možete kreirati servis koji će već arhitektonski biti i proširiv ( u smislu performansi) i spremni za nove zahtjeve klijenata (na primjer, za lokalizaciju ličnih podataka).

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

izvor: www.habr.com

Dodajte komentar