Kényelmes építészeti minták

Szia Habr!

A koronavírus miatt kialakult aktuális események fényében számos internetes szolgáltatás megnövekedett terhelésnek indult. Például, Az egyik brit kiskereskedelmi lánc egyszerűen leállította online rendelési oldalát., mert nem volt elég kapacitás. És nem mindig lehet felgyorsítani a szervert egyszerűen erősebb berendezések hozzáadásával, de az ügyfelek kéréseit fel kell dolgozni (különben a versenytársakhoz kerülnek).

Ebben a cikkben röviden szólok azokról a népszerű gyakorlatokról, amelyek lehetővé teszik egy gyors és hibatűrő szolgáltatás létrehozását. A lehetséges fejlesztési sémák közül azonban csak azokat választottam ki, amelyek jelenleg is könnyen kezelhető. Minden elemhez vagy kész könyvtárak vannak, vagy lehetősége van a probléma megoldására egy felhőplatform segítségével.

Vízszintes méretezés

A legegyszerűbb és legismertebb pont. Hagyományosan a két leggyakoribb terheléselosztási séma a vízszintes és a függőleges skálázás. Az első esetben lehetővé teszi a szolgáltatások párhuzamos működését, ezzel elosztva a terhelést közöttük. A másodikban erősebb szervereket rendel, vagy optimalizálja a kódot.

Például az absztrakt felhőalapú fájltárolót veszem, vagyis az OwnCloud, a OneDrive és így tovább.

Az alábbiakban egy ilyen áramkör szabványos képe látható, de ez csak a rendszer összetettségét mutatja be. Végül is valahogy szinkronizálnunk kell a szolgáltatásokat. Mi történik, ha a felhasználó elment egy fájlt a táblagépről, majd meg akarja nézni a telefonról?

Kényelmes építészeti minták
A megközelítések közötti különbség: vertikális skálázásnál a csomópontok teljesítményének növelésére, vízszintes skálázásnál pedig új csomópontok hozzáadására készen állunk a terhelés elosztására.

CQRS

Parancslekérdezési felelősség elkülönítése Meglehetősen fontos minta, mivel lehetővé teszi a különböző ügyfelek számára, hogy ne csak különböző szolgáltatásokhoz kapcsolódjanak, hanem ugyanazokat az eseményfolyamokat is megkapják. Előnyei nem annyira nyilvánvalóak egy egyszerű alkalmazásnál, de rendkívül fontos (és egyszerű) egy forgalmas szolgáltatásnál. Lényege: a bejövő és kimenő adatfolyamok nem keresztezhetik egymást. Ez azt jelenti, hogy nem küldhet kérelmet és nem várhat választ, hanem kérést küld az A szolgáltatásnak, de választ kap a B szolgáltatástól.

Ennek a megközelítésnek az első bónusza a kapcsolat megszakítása (a szó tág értelmében) egy hosszú kérés végrehajtása közben. Például vegyünk egy többé-kevésbé szabványos sorozatot:

  1. A kliens kérést küldött a szervernek.
  2. A szerver hosszú feldolgozási időt indított el.
  3. A szerver válaszolt a kliensnek az eredménnyel.

Képzeljük el, hogy a 2. pontban megszakadt a kapcsolat (vagy újracsatlakozott a hálózat, vagy a felhasználó másik oldalra ment, megszakítva a kapcsolatot). Ebben az esetben a szervernek nehéz lesz választ küldenie a felhasználónak a feldolgozásról. A CQRS használatával a sorrend kissé eltérő lesz:

  1. Az ügyfél feliratkozott a frissítésekre.
  2. A kliens kérést küldött a szervernek.
  3. A szerver azt válaszolta, hogy a kérést elfogadták.
  4. A szerver a csatornán keresztül válaszolt az eredménnyel az „1” ponttól.

Kényelmes építészeti minták

Amint látja, a séma egy kicsit bonyolultabb. Ráadásul itt hiányzik az intuitív kérés-válasz megközelítés. Azonban amint láthatja, a kapcsolat megszakadása a kérelem feldolgozása közben nem vezet hibához. Sőt, ha a felhasználó valójában több eszközről is csatlakozik a szolgáltatáshoz (például mobiltelefonról és táblagépről), akkor biztos lehet benne, hogy mindkét eszközre érkezik a válasz.

Érdekes módon a bejövő üzenetek feldolgozásának kódja ugyanaz lesz (nem 100%) mind az események esetében, amelyeket maga a kliens befolyásolt, és más események esetében, beleértve a más ügyfelektől érkező eseményeket is.

A valóságban azonban plusz bónuszt kapunk annak köszönhetően, hogy az egyirányú áramlás funkcionális stílusban kezelhető (RX és hasonlók használatával). Ez pedig már komoly plusz, hiszen lényegében teljesen reaktívvá tehető az alkalmazás, ráadásul funkcionális megközelítéssel. A zsíros programok esetében ez jelentősen megtakaríthatja a fejlesztési és támogatási forrásokat.

Ha ezt a megközelítést vízszintes skálázással kombináljuk, akkor bónuszként lehetőséget kapunk arra, hogy kéréseket küldjünk az egyik szervernek, és fogadjunk válaszokat a másiktól. Így az ügyfél kiválaszthatja a számára kényelmes szolgáltatást, és a belső rendszer továbbra is képes lesz az események helyes feldolgozására.

Rendezvények beszerzése

Mint tudják, az elosztott rendszer egyik fő jellemzője a közös idő, a közös kritikus szakasz hiánya. Egy folyamathoz elvégezhet egy szinkronizálást (ugyanazon mutexeken), amelyen belül biztos lehet benne, hogy senki más nem hajtja végre ezt a kódot. Ez azonban veszélyes egy elosztott rendszer számára, mivel többletköltséget igényel, és megöli a skálázás minden szépségét - az összes komponens továbbra is vár egyet.

Innen egy fontos tényt kapunk - egy gyorsan elosztott rendszert nem lehet szinkronizálni, mert akkor csökkentjük a teljesítményt. Másrészt gyakran szükségünk van bizonyos konzisztenciára az összetevők között. És ehhez használhatja a megközelítést végső következetesség, ahol garantált, hogy ha az utolsó frissítés után bizonyos ideig ("végül") nem történik adatváltozás, akkor minden lekérdezés az utoljára frissített értéket adja vissza.

Fontos megérteni, hogy a klasszikus adatbázisokhoz meglehetősen gyakran használják erős konzisztencia, ahol minden csomópont ugyanazokkal az információkkal rendelkezik (ez gyakran akkor érhető el, ha a tranzakció csak a második szerver válasza után tekinthető létrejöttnek). Az elszigeteltségi szintek miatt van itt némi lazítás, de az általános elképzelés változatlan - teljesen harmonizált világban lehet élni.

Térjünk azonban vissza az eredeti feladathoz. Ha a rendszer egy részével kiépíthető végső következetesség, akkor elkészíthetjük a következő diagramot.

Kényelmes építészeti minták

Ennek a megközelítésnek a legfontosabb jellemzői:

  • Minden bejövő kérés egy sorba kerül.
  • A kérés feldolgozása közben a szolgáltatás más sorokba is helyezhet feladatokat.
  • Minden bejövő eseménynek van egy azonosítója (amely a deduplikációhoz szükséges).
  • A sor ideológiailag a „csak hozzáfűzés” séma szerint működik. Nem távolíthat el belőle elemeket és nem rendezheti át őket.
  • A sor a FIFO séma szerint működik (elnézést a tautológiáért). Ha párhuzamos végrehajtást kell végeznie, akkor egy szakaszban át kell helyeznie az objektumokat különböző sorokba.

Hadd emlékeztessem önöket, hogy fontolgatjuk az online fájltárolás esetét. Ebben az esetben a rendszer valahogy így fog kinézni:

Kényelmes építészeti minták

Fontos, hogy a diagramon szereplő szolgáltatások nem feltétlenül jelentenek külön szervert. Még a folyamat is ugyanaz lehet. Egy másik fontos dolog: ideológiailag ezek a dolgok úgy vannak elválasztva, hogy a vízszintes skálázás könnyen alkalmazható.

És két felhasználó esetében a diagram így fog kinézni (a különböző felhasználóknak szánt szolgáltatások különböző színekkel vannak feltüntetve):

Kényelmes építészeti minták

Bónuszok egy ilyen kombinációból:

  • Az információfeldolgozási szolgáltatások elkülönülnek. A sorok is elkülönülnek. Ha növelnünk kell a rendszer átviteli sebességét, akkor csak több szolgáltatást kell elindítanunk több szerveren.
  • Amikor információt kapunk egy felhasználótól, nem kell megvárnunk az adatok teljes mentését. Éppen ellenkezőleg, csak azt kell válaszolnunk, hogy „ok”, majd fokozatosan elkezdjük a munkát. Ugyanakkor a sor kisimítja a csúcsokat, mivel egy új objektum hozzáadása gyorsan megtörténik, és a felhasználónak nem kell megvárnia a teljes ciklus teljes áthaladását.
  • Példaként hozzáadtam egy deduplikációs szolgáltatást, amely megpróbálja egyesíteni az azonos fájlokat. Ha az esetek 1%-ában sokáig működik, akkor a kliens szinte észre sem veszi (lásd fent), ami nagy plusz, hiszen már nem kell XNUMX%-os gyorsaságnak és megbízhatóságnak lennünk.

A hátrányok azonban azonnal láthatóak:

  • Rendszerünk elvesztette szigorú következetességét. Ez azt jelenti, hogy ha például különböző szolgáltatásokra fizet elő, akkor elméletileg eltérő állapotot kaphat (mivel előfordulhat, hogy az egyik szolgáltatásnak nincs ideje értesítést kapni a belső sorból). Egy másik következmény, hogy a rendszernek most nincs közös ideje. Vagyis például lehetetlen az összes eseményt egyszerűen érkezési idő szerint rendezni, mivel a szerverek közötti órák nem biztos, hogy szinkronban vannak (sőt, két szerveren azonos idő utópia).
  • Mostantól egyetlen eseményt sem lehet egyszerűen visszagörgetni (mint azt egy adatbázissal meg lehetne tenni). Ehelyett új eseményt kell hozzáadnia − kártérítési esemény, amely az utolsó állapotot a szükségesre módosítja. Példaként egy hasonló területről: az előzmények átírása nélkül (ami bizonyos esetekben rossz) a git-ben nem lehet visszavonni a véglegesítést, de készíthet egy speciális visszaállítási kötelezettség, ami lényegében csak a régi állapotot adja vissza. Azonban mind a hibás commit, mind a rollback a történelemben marad.
  • Az adatséma kiadásonként változhat, de a régi események már nem frissíthetők az új szabványra (mivel az eseményeket elvileg nem lehet módosítani).

Amint láthatja, az Event Sourcing jól működik a CQRS-sel. Ráadásul a hatékony és kényelmes sorokat tartalmazó, de az adatfolyamok szétválasztása nélküli rendszer megvalósítása már önmagában is nehéz, mert szinkronizálási pontokat kell hozzáadnia, amelyek semlegesítik a sorok teljes pozitív hatását. Mindkét megközelítést egyszerre alkalmazva kissé módosítani kell a programkódot. Esetünkben, amikor fájlt küldünk a szerverre, csak „ok” válasz érkezik, ami csak azt jelenti, hogy „a fájl hozzáadásának művelete el lett mentve”. Formálisan ez nem jelenti azt, hogy az adatok már elérhetőek más eszközökön (például a deduplikációs szolgáltatás újra tudja építeni az indexet). Egy idő után azonban az ügyfél „X fájl elmentve” stílusú értesítést kap.

Ennek eredményeként:

  • A fájlküldési állapotok száma növekszik: a klasszikus „fájl elküldve” helyett kettőt kapunk: „a fájl felkerült a szerveren lévő sorba” és „a fájl el lett mentve a tárhelyen”. Utóbbi azt jelenti, hogy más eszközök már elkezdhetik fogadni a fájlt (korrigáljuk, hogy a sorok eltérő sebességgel működnek).
  • Tekintettel arra, hogy a benyújtási információk ma már különböző csatornákon érkeznek, megoldásokat kell találnunk a fájl feldolgozási állapotának fogadására. Ennek következménye: a klasszikus kérés-választól eltérően a kliens újraindítható a fájl feldolgozása közben, de ennek a feldolgozásnak magának a státusza megfelelő lesz. Sőt, ez az elem lényegében a dobozból kivéve működik. Következésképpen: ma már toleránsabbak vagyunk a kudarcokkal szemben.

Szilánkos

Mint fentebb leírtuk, az eseményforrás-rendszerekből hiányzik a szigorú következetesség. Ez azt jelenti, hogy több tárolót is használhatunk anélkül, hogy szinkronizálnánk őket. A problémánkat megközelítve a következőket tehetjük:

  • Fájlok szétválasztása típus szerint. Például a képek/videók dekódolhatók, és hatékonyabb formátum választható.
  • Külön fiókok országonként. Számos jogszabály miatt erre szükség lehet, de ez az architektúra séma automatikusan biztosít ilyen lehetőséget

Kényelmes építészeti minták

Ha adatokat szeretne átvinni egyik tárolóról a másikra, akkor a szabványos eszközök már nem elegendőek. Sajnos ebben az esetben le kell állítani a sort, el kell végezni az áttelepítést, majd el kell indítani. Általános esetben az adatátvitel „menet közben” nem lehetséges, viszont ha az eseménysor teljes mértékben el van tárolva, és vannak pillanatképei a korábbi tárolási állapotokról, akkor az alábbiak szerint tudjuk visszajátszani az eseményeket:

  • Az Eseményforrásban minden eseménynek saját azonosítója van (ideális esetben nem csökkenő). Ez azt jelenti, hogy hozzáadhatunk egy mezőt a tárolóhoz - az utoljára feldolgozott elem azonosítóját.
  • Megkettőzzük a sort, hogy az összes eseményt több független tárolóhoz lehessen feldolgozni (az első az, amelyikben az adatok már tárolva vannak, a második pedig új, de még üres). A második sor feldolgozása természetesen még nem zajlik.
  • Elindítjuk a második sort (azaz elkezdjük az események újrajátszását).
  • Amikor az új várólista viszonylag üres (vagyis az elem hozzáadása és lekérése közötti átlagos időeltérés elfogadható), elkezdheti az olvasók átváltását az új tárolóra.

Amint látja, rendszerünkben nem volt és még mindig nincs szigorú következetesség. Csak esetleges konzisztencia van, azaz garancia arra, hogy az események ugyanabban a sorrendben (de esetleg eltérő késleltetéssel) történjenek. És ennek segítségével viszonylag egyszerűen, a rendszer leállítása nélkül tudunk adatokat átvinni a földkerekség másik felére.

Így folytatva a fájlok online tárolására vonatkozó példánkat, egy ilyen architektúra már számos bónuszt ad nekünk:

  • Az objektumokat dinamikusan közelebb vihetjük a felhasználókhoz. Így javíthatja a szolgáltatás minőségét.
  • Előfordulhat, hogy egyes adatokat vállalaton belül tárolunk. Például a vállalati felhasználók gyakran megkövetelik, hogy adataikat ellenőrzött adatközpontokban tárolják (az adatszivárgás elkerülése érdekében). A felosztással ezt könnyen támogathatjuk. A feladat pedig még egyszerűbb, ha az ügyfél rendelkezik kompatibilis felhővel (pl. Azure saját üzemeltetésű).
  • És ami a legfontosabb, hogy ezt nem kell megtennünk. Végtére is, először is nagyon boldogok lennénk, ha minden fiókhoz egyetlen tárhely lenne (a munka gyors megkezdéséhez). Ennek a rendszernek az a legfontosabb jellemzője, hogy bár bővíthető, kezdetben meglehetősen egyszerű. Csak nem kell azonnal kódot írni, ami millió különálló, független sorral működik stb. Ha szükséges, ez a jövőben megtehető.

Statikus tartalomtárhely

Ez a pont elég nyilvánvalónak tűnhet, de egy többé-kevésbé szabványos betöltött alkalmazáshoz még mindig szükséges. A lényege egyszerű: az összes statikus tartalmat nem ugyanarról a szerverről terjesztik, ahol az alkalmazás található, hanem speciális, kifejezetten erre a feladatra szánt szerverekről. Ennek eredményeként ezeket a műveleteket gyorsabban hajtják végre (a feltételes nginx gyorsabban és olcsóbban szolgálja ki a fájlokat, mint egy Java szerver). Plusz CDN architektúra (Content Delivery Network) lehetővé teszi számunkra, hogy a végfelhasználókhoz közelebb helyezzük el fájljainkat, ami pozitív hatással van a szolgáltatással való munkavégzés kényelmére.

A statikus tartalom legegyszerűbb és legszokványosabb példája egy webhely szkriptjei és képei. Minden egyszerű velük - előre ismertek, majd az archívumot feltöltik a CDN-kiszolgálókra, ahonnan elosztják a végfelhasználókhoz.

A valóságban azonban statikus tartalom esetén a lambda architektúrához némileg hasonló megközelítést használhat. Térjünk vissza a feladatunkhoz (online fájltárolás), amelyben fájlokat kell szétosztanunk a felhasználók között. A legegyszerűbb megoldás egy olyan szolgáltatás létrehozása, amely minden felhasználói kérésre elvégzi az összes szükséges ellenőrzést (jogosultság stb.), majd közvetlenül a tárhelyünkről tölti le a fájlt. Ennek a megközelítésnek a fő hátránya, hogy a statikus tartalmat (és egy bizonyos revízióval rendelkező fájl valójában statikus tartalom) ugyanaz a szerver terjeszti, amely az üzleti logikát tartalmazza. Ehelyett elkészítheti a következő diagramot:

  • A szerver egy letöltési URL-t biztosít. Formája lehet file_id + key, ahol a kulcs egy mini-digitális aláírás, amely a következő XNUMX órára jogosítja fel az erőforrást.
  • A fájlt az egyszerű nginx terjeszti a következő lehetőségekkel:
    • Tartalom gyorsítótárazása. Mivel ez a szolgáltatás külön szerveren is elhelyezhető, tartalékot hagytunk magunknak a jövőre nézve azzal a lehetőséggel, hogy a legfrissebb letöltött fájlokat lemezen tároljuk.
    • A kulcs ellenőrzése a kapcsolat létrehozásakor
  • Választható: streaming tartalomfeldolgozás. Például, ha a szolgáltatásban lévő összes fájlt tömörítjük, akkor közvetlenül ebben a modulban tudjuk kicsomagolni. Következésképpen: az IO-műveletek ott vannak végrehajtva, ahol a helyükön vannak. Egy Java archiváló könnyen lefoglal sok extra memóriát, de az üzleti logikával rendelkező szolgáltatás Rust/C++ feltételes feltételekbe történő átírása szintén hatástalan lehet. Esetünkben különböző folyamatok (vagy akár szolgáltatások) használatosak, így az üzleti logika és az IO műveletek elég hatékonyan elkülöníthetők egymástól.

Kényelmes építészeti minták

Ez a séma nem nagyon hasonlít a statikus tartalom terjesztéséhez (mivel nem töltjük fel valahova a teljes statikus csomagot), de a valóságban ez a megközelítés éppen a megváltoztathatatlan adatok terjesztésére vonatkozik. Sőt, ez a séma általánosítható más esetekre is, ahol a tartalom nem egyszerűen statikus, hanem megváltoztathatatlan és nem törölhető blokkok halmazaként is ábrázolható (bár ezek hozzáadhatók).

Egy másik példa (megerősítésképpen): ha Jenkins/TeamCity-vel dolgozott, akkor tudja, hogy mindkét megoldás Java nyelven íródott. Mindkettő egy Java-folyamat, amely egyaránt kezeli az összeállítások összehangolását és a tartalomkezelést. Különösen mindkettőjüknek vannak olyan feladatai, mint a „fájl/mappa átvitele a szerverről”. Példaként: műtermékek kiadása, forráskód átvitele (amikor az ügynök nem közvetlenül a repository-ból tölti le a kódot, hanem a szerver végzi el helyette), hozzáférés a naplókhoz. Mindezek a feladatok különböznek az IO-terhelésükben. Azaz kiderül, hogy a komplex üzleti logikáért felelős szervernek ugyanakkor képesnek kell lennie arra, hogy hatékonyan tudjon átnyomni önmagán nagy adatfolyamokat. És ami a legérdekesebb, hogy egy ilyen műveletet ugyanarra az nginx-re lehet delegálni pontosan ugyanazon séma szerint (kivéve, hogy az adatkulcsot hozzá kell adni a kéréshez).

Ha azonban visszatérünk a rendszerünkhöz, hasonló diagramot kapunk:

Kényelmes építészeti minták

Mint látható, a rendszer radikálisan összetettebbé vált. Most már nem csak egy mini-folyamat, amely helyben tárolja a fájlokat. Most nem a legegyszerűbb támogatásra van szükség, API verzió vezérlésre stb. Ezért az összes diagram megrajzolása után a legjobb részletesen megvizsgálni, hogy a bővíthetőség megéri-e a költségeket. Ha azonban szeretnéd bővíteni a rendszert (többek között még nagyobb számú felhasználóval dolgozni), akkor hasonló megoldásokat kell keresned. Ennek eredményeként azonban a rendszer felépítésileg készen áll a megnövekedett terhelésre (szinte minden alkatrész klónozható vízszintes méretezéshez). A rendszer leállítása nélkül frissíthető (egyes műveletek enyhén lelassulnak).

Ahogy a legelején mondtam, mostanra számos internetes szolgáltatás megnövekedett terhelést kapott. És néhányuk egyszerűen elkezdett leállni a megfelelő működésben. Valójában a rendszerek éppen abban a pillanatban hibáztak meg, amikor a vállalkozásnak pénzt kellett volna keresnie. Ez azt jelenti, hogy a halasztott szállítás helyett, ahelyett, hogy azt javasolná az ügyfeleknek, hogy „tervezze meg a szállítást a következő hónapokra”, a rendszer egyszerűen azt mondta: „menjen a versenytársakhoz”. Valójában ez az alacsony termelékenység ára: a veszteségek pontosan akkor következnek be, amikor a nyereség a legmagasabb.

Következtetés

Mindezek a megközelítések korábban ismertek voltak. Ugyanez a VK régóta használja a Static Content Hosting ötletét képek megjelenítésére. Sok online játék a Sharding sémát használja a játékosok régiókra való felosztására vagy a játékhelyek elkülönítésére (ha maga a világ is ilyen). Az Event Sourcing megközelítést aktívan használják az e-mailekben. A legtöbb kereskedési alkalmazás, ahol folyamatosan adatot fogadnak, valójában CQRS megközelítésre épül, hogy szűrni tudja a kapott adatokat. Nos, a vízszintes skálázást sok szolgáltatásban már régóta használják.

A legfontosabb azonban, hogy mindezek a minták nagyon könnyen alkalmazhatóvá váltak a modern alkalmazásokban (természetesen, ha megfelelőek). A felhők azonnali felosztást és vízszintes skálázást kínálnak, ami sokkal egyszerűbb, mintha saját maga rendelne meg különböző dedikált szervereket a különböző adatközpontokban. A CQRS sokkal könnyebbé vált, már csak az olyan könyvtárak fejlesztése miatt is, mint az RX. Körülbelül 10 évvel ezelőtt egy ritka weboldal támogatta ezt. Az Apache Kafkával készült kész konténereknek köszönhetően az Event Sourcing is hihetetlenül egyszerűen beállítható. 10 éve ez még újításnak számított, ma már közhely. Ugyanez a helyzet a statikus tartalomtárolással: a kényelmesebb technológiáknak köszönhetően (többek között a részletes dokumentáció és a válaszok nagy adatbázisa) ez a megközelítés még egyszerűbbé vált.

Ennek eredményeként számos meglehetősen összetett építészeti minta megvalósítása mára sokkal egyszerűbbé vált, ami azt jelenti, hogy jobb, ha előzetesen közelebbről megvizsgáljuk. Ha egy tíz éves alkalmazásban a fenti megoldások valamelyikét elhagyták a megvalósítás és az üzemeltetés magas költsége miatt, akkor most egy új alkalmazásban, vagy átalakítás után olyan szolgáltatást készíthet, amely már architekturálisan is bővíthető lesz ( teljesítmény szempontjából) és készen áll az ügyfelektől érkező új kérésekre (például személyes adatok lokalizálására).

És ami a legfontosabb: kérjük, ne használja ezeket a megközelítéseket, ha egyszerű alkalmazása van. Igen, szépek és érdekesek, de egy 100 fős csúcslátogatású oldalnál sokszor meg lehet boldogulni egy klasszikus monolittal (legalábbis kívülről minden belül modulokra osztható, stb.).

Forrás: will.com

Hozzászólás