Mugavad arhitektuurimustrid

Tere Habr!

Koroonaviirusest tingitud praeguste sündmuste valguses on mitmed Interneti-teenused hakanud üha suuremat koormust saama. Näiteks, Üks Ühendkuningriigi jaemüügikettidest peatas lihtsalt oma veebipõhise tellimislehe., sest maht ei olnud piisavalt. Ja alati pole võimalik serverit kiirendada lihtsalt võimsama varustuse lisamisega, vaid klientide taotlusi tuleb töödelda (muidu lähevad need konkurentidele).

Selles artiklis räägin lühidalt populaarsetest tavadest, mis võimaldavad teil luua kiire ja tõrketaluva teenuse. Võimalikest arendusskeemidest valisin aga välja vaid need, mis hetkel on lihtne kasutada. Iga üksuse jaoks on teil kas valmis teegid või on teil võimalus probleem pilveplatvormi kasutades lahendada.

Horisontaalne skaleerimine

Lihtsaim ja tuntuim punkt. Tavaliselt on kaks levinumat koormuse jaotusskeemi horisontaalne ja vertikaalne skaleerimine. Esimesel juhul lubate teenustel paralleelselt töötada, jaotades sellega koormuse nende vahel. Teises tellite võimsamad serverid või optimeerite koodi.

Näiteks võtan abstraktse pilvefailide salvestusruumi, st mõne OwnCloudi, OneDrive'i ja nii edasi analoogi.

Sellise vooluringi standardpilt on allpool, kuid see näitab ainult süsteemi keerukust. Lõppude lõpuks peame teenused kuidagi sünkroonima. Mis juhtub, kui kasutaja salvestab faili tahvelarvutist ja soovib seda seejärel telefonist vaadata?

Mugavad arhitektuurimustrid
Lähenemisviiside erinevus: vertikaalsel skaleerimisel oleme valmis suurendama sõlmede võimsust ja horisontaalsel skaleerimisel oleme valmis lisama koormuse jaotamiseks uusi sõlmi.

CQRS

Käskude päringu vastutuse eraldamine Üsna oluline muster, kuna see võimaldab erinevatel klientidel mitte ainult erinevate teenustega ühendust luua, vaid ka samu sündmuste vooge vastu võtta. Selle eelised pole lihtsa rakenduse jaoks nii ilmsed, kuid see on äärmiselt oluline (ja lihtne) hõivatud teenuse jaoks. Selle olemus: sissetulevad ja väljaminevad andmevood ei tohiks ristuda. See tähendab, et te ei saa saata päringut ja oodata vastust; selle asemel saadate päringu teenusele A, kuid saate teenuselt B vastuse.

Selle lähenemisviisi esimene boonus on võime katkestada ühendus (selle sõna laiemas tähenduses) pika päringu täitmise ajal. Näiteks võtame enam-vähem standardse järjestuse:

  1. Klient saatis serverile päringu.
  2. Server alustas pikka töötlemisaega.
  3. Server vastas kliendile tulemusega.

Kujutagem ette, et punktis 2 katkes ühendus (või ühendati võrk uuesti või läks kasutaja teisele lehele, katkestades ühenduse). Sel juhul on serveril keeruline saata kasutajale vastust teabega selle kohta, mida täpselt töödeldakse. CQRS-i kasutamisel on järjestus veidi erinev:

  1. Klient on värskendused tellinud.
  2. Klient saatis serverile päringu.
  3. Server vastas "taotlus vastu võetud".
  4. Server vastas tulemusega kanali kaudu punktist “1”.

Mugavad arhitektuurimustrid

Nagu näete, on skeem veidi keerulisem. Lisaks puudub siin intuitiivne päringu-vastuse lähenemine. Kuid nagu näete, ei põhjusta ühenduse katkemine päringu töötlemisel viga. Veelgi enam, kui kasutaja on teenusega ühenduses mitmest seadmest (näiteks mobiiltelefonist ja tahvelarvutist), saate veenduda, et vastus tuleb mõlemasse seadmesse.

Huvitaval kombel muutub sissetulevate sõnumite töötlemise kood samaks (mitte 100%) nii sündmuste puhul, mida mõjutas klient ise, kui ka muude sündmuste puhul, sealhulgas teistelt klientidelt.

Tegelikkuses saame aga lisaboonuse tänu sellele, et ühesuunalist voogu saab käsitleda funktsionaalses stiilis (kasutades RX-i jms). Ja see on juba tõsine pluss, kuna sisuliselt saab rakenduse muuta täiesti reaktiivseks ja kasutades ka funktsionaalset lähenemist. Rasvaprogrammide puhul võib see oluliselt säästa arendus- ja tugiressursse.

Kui kombineerida see lähenemine horisontaalse skaleerimisega, siis boonusena saame võimaluse saata päringuid ühte serverisse ja saada vastuseid teiselt. Seega saab klient valida endale sobiva teenuse ning sees olev süsteem suudab sündmusi siiski korrektselt töödelda.

Sündmuste hankimine

Nagu teate, on hajutatud süsteemi üks peamisi omadusi ühise aja, ühise kriitilise lõigu puudumine. Ühe protsessi jaoks saate teha sünkroonimise (samadel mutexidel), mille käigus olete kindel, et keegi teine ​​seda koodi ei käivita. Kuid see on hajutatud süsteemi jaoks ohtlik, kuna see nõuab üldkulusid ja hävitab ka skaleerimise ilu - kõik komponendid ootavad endiselt üht.

Siit saame olulise fakti – kiirelt hajutatud süsteemi ei saa sünkroniseerida, sest siis vähendame jõudlust. Teisest küljest vajame sageli komponentide vahel teatud järjepidevust. Ja selleks saate kasutada lähenemist lõplik järjepidevus, kus on garanteeritud, et kui pärast viimast värskendust teatud aja jooksul (“lõpuks”) andmetes muudatusi ei toimu, tagastavad kõik päringud viimati värskendatud väärtuse.

Oluline on mõista, et klassikaliste andmebaaside puhul kasutatakse seda üsna sageli tugev konsistents, kus igal sõlmel on sama teave (sageli saavutatakse see juhul, kui tehing loetakse sooritatuks alles pärast teise serveri reageerimist). Isolatsioonitasemete tõttu on siin mõningaid lõõgastusi, kuid üldine idee jääb samaks – elada saab täiesti harmoneeritud maailmas.

Tuleme siiski tagasi algse ülesande juurde. Kui osa süsteemist saab ehitada lõplik järjepidevus, siis saame koostada järgmise diagrammi.

Mugavad arhitektuurimustrid

Selle lähenemisviisi olulised omadused:

  • Iga sissetulev päring asetatakse ühte järjekorda.
  • Päringu töötlemise ajal võib teenus paigutada ülesandeid ka teistesse järjekordadesse.
  • Igal sissetuleval sündmusel on identifikaator (mis on vajalik dubleerimiseks).
  • Järjekord töötab ideoloogiliselt skeemi "ainult lisamine" järgi. Te ei saa sealt elemente eemaldada ega ümber korraldada.
  • Järjekord töötab FIFO skeemi järgi (vabandan tautoloogia pärast). Kui on vaja teha paralleelkäivitust, siis ühes etapis tuleks objekte teisaldada erinevatesse järjekordadesse.

Lubage mul teile meelde tuletada, et kaalume failide veebisalvestuse juhtumit. Sel juhul näeb süsteem välja umbes selline:

Mugavad arhitektuurimustrid

On oluline, et diagrammil olevad teenused ei pruugi tähendada eraldi serverit. Isegi protsess võib olla sama. Teine asi on oluline: ideoloogiliselt on need asjad eraldatud nii, et horisontaalset skaleerimist saab hõlpsasti rakendada.

Ja kahe kasutaja jaoks näeb diagramm välja selline (eri kasutajatele mõeldud teenused on tähistatud erinevate värvidega):

Mugavad arhitektuurimustrid

Boonused sellisest kombinatsioonist:

  • Infotöötlusteenused on eraldatud. Järjekorrad on samuti eraldatud. Kui peame suurendama süsteemi läbilaskevõimet, peame lihtsalt käivitama rohkem teenuseid rohkemates serverites.
  • Kui saame kasutajalt teavet, ei pea me ootama, kuni andmed on täielikult salvestatud. Vastupidi, peame lihtsalt vastama "ok" ja seejärel järk-järgult tööle asuma. Samal ajal silub järjekord tippe, kuna uue objekti lisamine toimub kiiresti ja kasutaja ei pea ootama kogu tsükli täielikku läbimist.
  • Näitena lisasin deduplikatsiooniteenuse, mis proovib identseid faile liita. Kui see töötab pikka aega 1% juhtudest, ei märka klient seda peaaegu (vt ülalt), mis on suur pluss, kuna meilt ei nõuta enam XNUMX% kiirust ja usaldusväärsust.

Puudused on aga kohe nähtavad:

  • Meie süsteem on kaotanud oma range järjepidevuse. See tähendab, et kui tellite näiteks erinevaid teenuseid, siis teoreetiliselt võite saada teistsuguse oleku (kuna ühel teenusel ei pruugi olla aega sisejärjekorrast teatise saamiseks). Teise tagajärjena pole süsteemil nüüd ühist aega. See tähendab, et kõiki sündmusi pole võimalik näiteks lihtsalt saabumisaja järgi sortida, kuna serveritevahelised kellad ei pruugi olla sünkroonsed (pealegi on sama kellaaeg kahes serveris utoopia).
  • Ühtegi sündmust ei saa nüüd lihtsalt tagasi kerida (nagu saab teha andmebaasiga). Selle asemel peate lisama uue sündmuse − hüvitamise sündmus, mis muudab viimase oleku vajalikuks. Näitena sarnasest piirkonnast: ilma ajalugu ümber kirjutamata (mis on mõnel juhul halb), ei saa te git-is tehtud kohustust tagasi pöörata, kuid saate teha spetsiaalse tagasipööramine, mis sisuliselt lihtsalt tagastab vana oleku. Ajalukku jäävad aga nii ekslik kohustus kui ka tagasipööramine.
  • Andmeskeem võib versiooniti muutuda, kuid vanu sündmusi ei saa enam uuele standardile värskendada (kuna sündmusi ei saa põhimõtteliselt muuta).

Nagu näete, töötab sündmuste hankimine CQRS-iga hästi. Pealegi on tõhusate ja mugavate järjekordadega, kuid andmevoogusid eraldamata süsteemi juurutamine juba iseenesest keeruline, kuna peate lisama sünkroonimispunkte, mis neutraliseerivad kogu järjekordade positiivse mõju. Mõlemat lähenemist korraga rakendades on vaja programmi koodi veidi kohandada. Meie puhul tuleb faili serverisse saatmisel vastus ainult "ok", mis tähendab ainult, et "faili lisamise toiming salvestati". Formaalselt ei tähenda see, et andmed on juba teistes seadmetes saadaval (näiteks saab deduplikatsiooniteenus indeksi uuesti üles ehitada). Kuid mõne aja pärast saab klient teate stiilis "Fail X on salvestatud".

Tulemusena:

  • Failide saatmise olekute arv kasvab: klassikalise "fail saadetud" asemel saame kaks: "fail on lisatud serveri järjekorda" ja "fail on salvestatud salvestusruumi". Viimane tähendab, et teised seadmed võivad juba hakata faili vastu võtma (kohandatud sellega, et järjekorrad töötavad erineva kiirusega).
  • Kuna esitamise teave tuleb nüüd läbi erinevate kanalite, peame leidma lahendusi faili töötlemise oleku saamiseks. Selle tulemusena: erinevalt klassikalisest päring-vastusest saab klienti faili töötlemise ajal taaskäivitada, kuid selle töötlemise enda olek on õige. Pealegi töötab see üksus sisuliselt karbist välja võttes. Selle tulemusena oleme nüüd ebaõnnestumiste suhtes sallivamad.

Varjutamine

Nagu ülalpool kirjeldatud, puudub sündmuste hankimise süsteemidel range järjepidevus. See tähendab, et saame kasutada mitut salvestusruumi ilma nendevahelise sünkroonimiseta. Oma probleemile lähenedes saame:

  • Eraldage failid tüübi järgi. Näiteks saab pilte/videoid dekodeerida ja valida tõhusama vormingu.
  • Eraldage kontod riigiti. Paljude seaduste tõttu võib see olla vajalik, kuid antud arhitektuuriskeem annab sellise võimaluse automaatselt

Mugavad arhitektuurimustrid

Kui soovid andmeid ühest salvestusruumist teise üle kanda, siis tavalistest vahenditest enam ei piisa. Kahjuks peate sel juhul järjekorra peatama, tegema migratsiooni ja seejärel alustama. Üldjuhul ei saa andmeid "lennult" edastada, kuid kui sündmuste järjekord on täielikult salvestatud ja teil on eelmiste salvestusolekute hetktõmmised, saame sündmused uuesti esitada järgmiselt:

  • Sündmuse allikas on igal sündmusel oma identifikaator (ideaaljuhul mitte vähenev). See tähendab, et saame salvestusruumi lisada välja – viimati töödeldud elemendi ID.
  • Dubleerime järjekorra, et kõiki sündmusi saaks töödelda mitme sõltumatu salvestusruumi jaoks (esimene on see, kuhu andmed on juba salvestatud, ja teine ​​on uus, kuid veel tühi). Teist järjekorda muidugi veel ei töödelda.
  • Käivitame teise järjekorra (see tähendab, et hakkame sündmusi taasesitama).
  • Kui uus järjekord on suhteliselt tühi (st keskmine ajavahe elemendi lisamise ja allalaadimise vahel on vastuvõetav), võite hakata lugejaid uude salvestusruumi vahetama.

Nagu näete, ei olnud ega ole ka praegu meie süsteemis ranget järjepidevust. On ainult lõplik järjepidevus, st garantii, et sündmusi töödeldakse samas järjekorras (kuid võib-olla erinevate viivitustega). Ja seda kasutades saame suhteliselt lihtsalt andmeid edastada ilma süsteemi peatamata teisele poole maakera.

Seega, jätkates meie näidet failide võrgus salvestamise kohta, annab selline arhitektuur meile juba mitmeid boonuseid:

  • Saame objekte dünaamiliselt kasutajatele lähemale teisaldada. Nii saate parandada teenuse kvaliteeti.
  • Võime säilitada teatud andmeid ettevõtete sees. Näiteks nõuavad ettevõtte kasutajad sageli oma andmete salvestamist kontrollitud andmekeskustesse (andmelekete vältimiseks). Jagamise kaudu saame seda hõlpsasti toetada. Ja ülesanne on veelgi lihtsam, kui kliendil on ühilduv pilv (näiteks Azure ise hostitud).
  • Ja kõige tähtsam on see, et me ei pea seda tegema. Alustuseks oleksime ju üsna rahul, kui kõigi kontode jaoks oleks üks salvestusruum (töö kiireks alustamiseks). Ja selle süsteemi põhiomadus on see, et kuigi see on laiendatav, on see algstaadiumis üsna lihtne. Sa lihtsalt ei pea kohe kirjutama koodi, mis töötab miljoni eraldiseisva järjekorraga jne. Vajadusel saab seda ka edaspidi teha.

Staatiline sisu hostimine

See punkt võib tunduda üsna ilmne, kuid see on siiski vajalik enam-vähem standardse laaditud rakenduse jaoks. Selle olemus on lihtne: kogu staatiline sisu levitatakse mitte samast serverist, kus rakendus asub, vaid spetsiaalsetest, spetsiaalselt sellele ülesandele pühendatud serveritest. Selle tulemusena tehakse neid toiminguid kiiremini (tingimuslik nginx teenindab faile kiiremini ja odavamalt kui Java server). Lisaks CDN-i arhitektuur (Content Delivery Network) võimaldab meil leida oma failid lõppkasutajatele lähemale, mis avaldab positiivset mõju teenusega töötamise mugavusele.

Staatilise sisu lihtsaim ja standardseim näide on veebisaidi skriptide ja piltide komplekt. Nendega on kõik lihtne – need on ette teada, seejärel laaditakse arhiiv üles CDN-i serveritesse, kust need lõppkasutajatele laiali jaotatakse.

Kuid tegelikkuses saate staatilise sisu jaoks kasutada lähenemist, mis sarnaneb lambda arhitektuuriga. Pöördume tagasi oma ülesande juurde (onlain-failisalvestus), mille puhul peame faile kasutajatele laiali jagama. Lihtsaim lahendus on luua teenus, mis iga kasutaja soovi korral teeb kõik vajalikud kontrollid (volitused jne) ning laadib seejärel faili otse meie salvestusruumist alla. Selle lähenemisviisi peamine puudus on see, et staatilist sisu (ja teatud versiooniga fail on tegelikult staatiline sisu) levitab sama server, mis sisaldab äriloogikat. Selle asemel saate teha järgmise diagrammi:

  • Server pakub allalaadimise URL-i. See võib olla kujul file_id + võti, kus võti on mini-digitaalallkiri, mis annab õiguse ressursile ligi pääseda järgmise XNUMX tunni jooksul.
  • Faili levitab lihtne nginx järgmiste valikutega:
    • Sisu vahemällu salvestamine. Kuna see teenus võib asuda eraldi serveris, oleme jätnud endale tulevikuks reservi võimaluse salvestada kõik viimased allalaaditud failid kettale.
    • Võtme kontrollimine ühenduse loomise ajal
  • Valikuline: voogesituse sisu töötlemine. Näiteks kui tihendame kõik teenuses olevad failid, saame lahti pakkida otse selles moodulis. Selle tulemusena: IO toimingud tehakse seal, kus need kuuluvad. Java arhiveerija eraldab hõlpsalt palju lisamälu, kuid äriloogikaga teenuse ümberkirjutamine Rust/C++ tingimustingimustesse võib samuti osutuda ebaefektiivseks. Meie puhul kasutatakse erinevaid protsesse (või isegi teenuseid) ja seetõttu saame äriloogika ja IO toimingud üsna tõhusalt eraldada.

Mugavad arhitektuurimustrid

See skeem ei ole väga sarnane staatilise sisu levitamisega (kuna me ei laadi kogu staatilist paketti kuskile üles), kuid tegelikult on see lähenemine seotud just muutumatute andmete levitamisega. Veelgi enam, seda skeemi saab üldistada muudeks juhtudeks, kus sisu ei ole lihtsalt staatiline, vaid seda saab esitada muutumatute ja mittekustutavate plokkide komplektina (kuigi neid saab lisada).

Teise näitena (tugevdamiseks): kui olete töötanud Jenkinsiga/TeamCityga, siis teate, et mõlemad lahendused on Java keeles kirjutatud. Mõlemad on Java protsessid, mis tegelevad nii ehituse orkestreerimise kui ka sisuhaldusega. Eelkõige on neil mõlemal sellised ülesanded nagu "faili/kausta ülekandmine serverist". Näitena: artefaktide väljastamine, lähtekoodi ülekandmine (kui agent ei laadi koodi otse hoidlast alla, vaid server teeb seda tema eest), ligipääs logidele. Kõik need ülesanded erinevad IO koormuse poolest. See tähendab, et selgub, et keerulise äriloogika eest vastutav server peab samal ajal suutma suuri andmevoogusid tõhusalt läbi suruda. Ja mis kõige huvitavam on see, et sellise toimingu saab delegeerida samale nginxile täpselt sama skeemi järgi (välja arvatud see, et päringule tuleks lisada andmevõti).

Kui aga naaseme oma süsteemi juurde, saame sarnase diagrammi:

Mugavad arhitektuurimustrid

Nagu näete, on süsteem muutunud radikaalselt keerukamaks. Nüüd pole see lihtsalt miniprotsess, mis salvestab faile kohapeal. Nüüd pole vaja kõige lihtsamat tuge, API versioonikontrolli jne. Seetõttu on kõige parem pärast kõigi diagrammide koostamist üksikasjalikult hinnata, kas laiendatavus on oma kulusid väärt. Kui aga soovite süsteemi laiendada (sh töötada veelgi suurema hulga kasutajatega), peate otsima sarnaseid lahendusi. Kuid selle tulemusel on süsteem arhitektuuriliselt valmis suurenenud koormuse jaoks (peaaegu iga komponenti saab horisontaalseks skaleerimiseks kloonida). Süsteemi saab värskendada ilma seda peatamata (lihtsalt mõned toimingud aeglustuvad veidi).

Nagu ma alguses ütlesin, on nüüdseks mitmed Interneti-teenused hakanud suuremat koormust saama. Ja mõned neist hakkasid lihtsalt lakkama õigesti töötamast. Tegelikult läksid süsteemid üles just sel hetkel, kui äri pidi raha teenima. See tähendab, et edasilükatud kohaletoimetamise asemel, selle asemel, et soovitada klientidele "planeerida tarne lähikuudeks", ütles süsteem lihtsalt "mine konkurentide juurde". Tegelikult on see madala tootlikkuse hind: kahjud tekivad just siis, kui kasum on suurim.

Järeldus

Kõik need lähenemisviisid olid varem tuntud. Sama VK on pikka aega kasutanud piltide kuvamiseks staatilise sisu hostimise ideed. Paljud võrgumängud kasutavad mängijate piirkondadeks jagamiseks või mängukohtade eraldamiseks (kui maailm ise seda on) skeemi Sharding. Sündmuste hankimise lähenemisviisi kasutatakse meilis aktiivselt. Enamik kauplemisrakendusi, kus andmeid pidevalt vastu võetakse, on tegelikult ehitatud CQRS-i lähenemisviisile, et saaks saadud andmeid filtreerida. Noh, horisontaalset skaleerimist on paljudes teenustes kasutatud üsna pikka aega.

Kuid mis kõige tähtsam, kõiki neid mustreid on tänapäevastes rakendustes väga lihtne rakendada (muidugi juhul, kui need sobivad). Pilved pakuvad kohe jagamist ja horisontaalset skaleerimist, mis on palju lihtsam kui tellida ise erinevatesse andmekeskustesse erinevaid pühendatud servereid. CQRS on muutunud palju lihtsamaks, kasvõi ainult selliste teekide nagu RX arendamise tõttu. Umbes 10 aastat tagasi võis haruldane veebisait seda toetada. Sündmuste hankimist on tänu Apache Kafkaga valmis konteineritele ka uskumatult lihtne seadistada. 10 aastat tagasi oleks see olnud uuendus, nüüd on see tavaline. Sama lugu on staatilise sisu hostimisega: tänu mugavamatele tehnoloogiatele (sh asjaolu, et on olemas üksikasjalik dokumentatsioon ja suur vastuste andmebaas) on see lähenemine muutunud veelgi lihtsamaks.

Tänu sellele on mitmete üsna keerukate arhitektuurimustrite rakendamine muutunud palju lihtsamaks, mis tähendab, et parem on seda eelnevalt lähemalt uurida. Kui kümne aasta vanuses rakenduses loobuti ühest ülaltoodud lahendustest juurutamise ja käitamise kalliduse tõttu, siis nüüd saate uues rakenduses või pärast ümbertöötamist luua teenuse, mis on juba arhitektuuriliselt laiendatav ( jõudluse osas) ja klientide uutele taotlustele valmis (näiteks isikuandmete lokaliseerimiseks).

Ja mis kõige tähtsam: ärge kasutage neid lähenemisviise, kui teil on lihtne rakendus. Jah, need on ilusad ja huvitavad, aga 100 inimese tippkülastusega saidi puhul saab sageli hakkama ka klassikalise monoliidiga (vähemalt väliselt saab kõik seespoolt mooduliteks jaotada jne).

Allikas: www.habr.com

Lisa kommentaar