Hej, Habr!
I ljuset av den aktuella situationen pÄ grund av coronaviruset har ett antal internettjÀnster fÄtt en ökad belastning. Till exempel, , eftersom kapaciteten inte rÀckte till. Och det Àr lÄngt ifrÄn alltid möjligt att snabba upp en server genom att helt enkelt lÀgga till kraftfullare utrustning, ÀndÄ mÄste vi bearbeta kundernas förfrÄgningar (eller sÄ gÄr de till konkurrenterna).
I den hÀr artikeln kommer jag kort att berÀtta om populÀra metoder som kan hjÀlpa till att skapa en snabb och motstÄndskraftig tjÀnst. Men bland de möjliga utvecklingsscheman har jag valt ut bara de som Àr lÀtta att anvÀnda.För varje punkt har du antingen redan fÀrdiga bibliotek, eller sÄ finns det möjlighet att lösa uppgiften med hjÀlp av en molnplattform.
Horisontell skalning
Det enklaste och mest kĂ€nda punkten. I allmĂ€nhet finns det tvĂ„ vanliga sĂ€tt att fördela belastningen â horisontell och vertikal skalning. lĂ„ter du tjĂ€nster arbeta parallellt, vilket fördelar belastningen mellan dem. bestĂ€ller du kraftfullare servrar eller optimerar koden.
Som exempel tar jag ett abstrakt molnlagringssystem för filer, det vill sÀga en analog till OwnCloud, OneDrive och sÄ vidare.
Den standardiserade bilden av en sÄdan struktur nedan visar bara systemets komplexitet. För vi mÄste pÄ nÄgot sÀtt synkronisera tjÀnsterna. Vad hÀnder om en anvÀndare sparar en fil frÄn sin surfplatta och sedan vill titta pÄ den frÄn sin telefon?

Skillnaden mellan metoderna: vid vertikal skalning Àr vi beredda att öka kapaciteten hos noderna, medan vi vid horisontell skalning lÀgger till nya noder för att fördela belastningen.
CQRS
Àr ett ganska viktigt mönster eftersom det gör det möjligt för olika kunder att inte bara ansluta till olika tjÀnster, utan ocksÄ att fÄ samma flöden av hÀndelser. Dess fördelar Àr inte sÄ uppenbara för en enkel applikation, men den Àr extremt viktig (och enkel) för en belastad tjÀnst. KÀrnan i detta Àr att inkommande och utgÄende datastreamar inte bör korsas. Det vill sÀga att du inte kan skicka en förfrÄgan och förvÀnta dig ett svar, istÀllet skickar du en förfrÄgan till tjÀnst A men fÄr svaret i tjÀnst B.
Den första fördelen med detta tillvÀgagÄngssÀtt Àr möjligheten att avbryta anslutningen (i bred mening av ordet) under processen av ett lÄngvarigt begÀran. Som exempel kan vi ta en mer eller mindre standardiserad sekvens:
- Klienten skickade en begÀran till servern.
- Servern pÄbörjade en lÄngvarig bearbetning.
- Servern svarade klienten med resultatet.
TÀnk dig att det i punkt 2 intrÀffade ett avbrott i kommunikationen (antingen Äteranslöt nÀtverket, eller sÄ bytte anvÀndaren till en annan sida och avbröt anslutningen). I detta fall kommer det att vara svÄrt för servern att skicka svar till anvÀndaren med information om vad som faktiskt har behandlats. Genom att anvÀnda CQRS kommer sekvensen att vara nÄgot annorlunda:
- Klienten har prenumererat pÄ uppdateringar.
- Klienten skickade en begÀran till servern.
- Servern svarade 'begÀran mottagen'.
- Servern svarade med resultatet genom kanalen frÄn punkt '1'.

Som man kan se Àr schemat nÄgot mer komplext. Dessutom saknas det intuitiva tillvÀgagÄngssÀttet request-response hÀr. Men som man kan se kommer ett avbrott i kommunikationen under behandling av begÀran inte att leda till ett fel. Dessutom, om anvÀndaren verkligen Àr ansluten till tjÀnsten med flera enheter (till exempel med en mobiltelefon och en surfplatta), kan man se till att svaret kommer till bÄda enheterna.
Det intressanta Àr att koden för behandling av inkommande meddelanden blir densamma (inte till 100%) för bÄde hÀndelser som pÄverkats av sjÀlva klienten och för andra hÀndelser, inklusive frÄn andra kunder.
Men i praktiken fÄr vi ytterligare fördelar eftersom den enkla strömmen kan behandlas pÄ funktionellt sÀtt (med RX och liknande). Och det Àr redan en stor fördel, eftersom applikationen i stort sett kan göras helt reaktiv, dessutom med hjÀlp av ett funktionellt tillvÀgagÄngssÀtt. För mer omfattande program kan detta avsevÀrt spara resurser vid utveckling och underhÄll.
Om man kombinerar detta tillvÀgagÄngssÀtt med horisontell skalning fÄr vi bonusen av att kunna skicka begÀrningar till en server och fÄ svar frÄn en annan. PÄ sÄ sÀtt kan klienten sjÀlv vÀlja en tjÀnst som passar dem, och systemet kan pÄ insidan ÀndÄ korrekt bearbeta hÀndelser.
Event Sourcing
Som ni vet Ă€r en av de frĂ€msta egenskaperna hos ett distribuerat system avsaknaden av gemensam tid och gemensam kritisk sektion. För en enskild process kan du göra en synkronisering (pĂ„ samma mutexar), dĂ€r du Ă€r sĂ€ker pĂ„ att ingen annan kör den koden. Men för ett distribuerat system Ă€r detta riskabelt eftersom det skulle innebĂ€ra overhead och döda hela skönheten i skalbarhet â alla komponenter skulle Ă€ndĂ„ vĂ€nta pĂ„ varandra.
HĂ€r fĂ„r vi en viktig insikt â en snabb distribuerad system kan inte synkroniseras, för dĂ„ minskar vi prestandan. Ă andra sidan behöver vi ofta viss koherens mellan komponenterna. För detta kan vi anvĂ€nda en metod med , dĂ€r det garanteras att, i avsaknad av dataĂ€ndringar under en viss tidsperiod efter den senaste uppdateringen, kommer alla förfrĂ„gningar att returnera det senaste uppdaterade vĂ€rdet.
Det Ă€r viktigt att förstĂ„ att strikt ofta tillĂ€mpas pĂ„ klassiska databaser, dĂ€r varje nod har identisk information (detta uppnĂ„s ofta nĂ€r en transaktion anses vara genomförd först efter ett svar frĂ„n den andra servern). HĂ€r finns vissa undantag pĂ„ grund av isoleringsnivĂ„er, men den övergripande essensen förblir densamma â du kan leva i en helt koherent vĂ€rld.
Men lÄt oss ÄtergÄ till den ursprungliga uppgiften. Om en del av systemet kan byggas med , sÄ kan vi bygga följande schema.

Viktiga egenskaper hos denna metod:
- Varje inkommande förfrÄgan placeras i en kö.
- Under behandlingen av förfrÄgan kan tjÀnsten ocksÄ placera uppgifter i andra köer.
- Varje inkommande hÀndelse har ett identifieringsnummer (som behövs för deduplication).
- Kön fungerar ideologiskt enligt "append only"-modellen. Det gÄr inte att ta bort eller omorganisera element dÀrifrÄn.
- Kön arbetar enligt FIFO (ursÀkta tautologin). Om det behövs för parallell bearbetning bör föremÄlen flyttas till olika köer i en av stegen.
Jag pÄminner om att vi betraktar fallet av en online filserver. I detta fall kommer systemet att se ungefÀr ut sÄ hÀr:

Det Ă€r viktigt att tjĂ€nsterna i diagrammet inte nödvĂ€ndigtvis betyder enskilda servrar. Ăven processen kan vara densamma. Det viktiga Ă€r nĂ„got annat: ideologiskt Ă€r dessa saker Ă„tskilda pĂ„ ett sĂ„dant sĂ€tt att det enkelt gĂ„r att tillĂ€mpa horisontell skalning.
Och för tvÄ anvÀndare kommer schemat att se ut sÄ hÀr (tjÀnster avsedda för olika anvÀndare markeras med olika fÀrger):

Fördelarna med en sÄdan kombination:
- TjÀnsterna för informationsbehandling Àr Ätskilda. Köerna Àr ocksÄ Ätskilda. Om vi behöver öka systemets genomströmning behöver vi bara starta fler tjÀnster pÄ fler servrar.
- NÀr vi fÄr information frÄn en anvÀndare behöver vi inte vÀnta pÄ att all data ska sparas helt. TvÀrtom, det rÀcker att vi svarar "ok", och sedan gradvis börja arbeta. Samtidigt jÀmnar kön ut toppar, eftersom tillÀgg av ett nytt objekt gÄr snabbt, och anvÀndaren behöver inte vÀnta pÄ en fullstÀndig genomgÄng av hela cykeln.
- Som exempel har jag lagt till en dedupliceringstjÀnst som försöker sammanfoga identiska filer. Om den jobbar lÀnge i 1 % av fallen kommer kunden praktiskt taget inte att mÀrka det (se ovan), vilket Àr en stor fördel, eftersom vi inte lÀngre krÀver hundra procent hastighet och tillförlitlighet.
Men nackdelarna blir ocksÄ tydliga:
- VÄrt system har förlorat strikt konsistens. Det betyder att om man till exempel prenumererar pÄ olika tjÀnster kan man teoretiskt fÄ olika tillstÄnd (eftersom en av tjÀnsterna kan hinna ta emot meddelandet frÄn den interna kön). Som en annan följd har systemet nu ingen gemensam tid. Det gÄr alltsÄ inte att sortera alla hÀndelser endast efter ankomsttid, eftersom klockorna mellan servrarna kan vara osynkroniserade (dessutom Àr samma tid pÄ tvÄ servrar en utopi).
- Inga hĂ€ndelser kan nu helt enkelt Ă„terskapas (som skulle kunna göras med en databas). IstĂ€llet mĂ„ste man lĂ€gga till en ny hĂ€ndelse â , som kommer att Ă€ndra det senaste tillstĂ„ndet till det som krĂ€vs. Som ett exempel frĂ„n ett liknande omrĂ„de: utan att skriva om historien (vilket Ă€r negativt i vissa fall) kan man i git inte Ă„ngra en commit, men man kan göra en speciell , som i grunden bara kommer att Ă„terstĂ€lla det gamla tillstĂ„ndet. Men i historiken kommer bĂ„de den felaktiga commit och rollback att sparas.
- Dataschemat kan förÀndras frÄn version till version, men gamla hÀndelser kan nu inte uppdateras till den nya standarden (eftersom hÀndelser i princip inte kan Àndras).
Som vi ser passar Event Sourcing utmÀrkt med CQRS. Dessutom Àr det redan svÄrt att implementera ett system med effektiva och anvÀndarvÀnliga köer, utan att separera datastreams, eftersom vi dÄ mÄste lÀgga till synkroniseringspunkter som kommer att neutralisera hela den positiva effekten av köerna. NÀr man tillÀmpar bÄda tillvÀgagÄngssÀtten samtidigt mÄste koden för programmet justeras nÄgot. I vÄrt fall, nÀr en fil skickas till servern, fÄr vi bara "okej" som svar, vilket bara betyder att "filöverföringsoperationen har sparats". Formellt sett betyder det inte att datan redan Àr tillgÀnglig pÄ andra enheter (till exempel kan dedupliceringstjÀnsten omstrukturera indexet). Men efter en tid kommer klienten att fÄ en avisering i stil med "fil X har sparats".
Som ett resultat:
- Antalet statuser för filöverföring ökar: istÀllet för den klassiska "fil skickad" fÄr vi tvÄ: "fil tillagd i kön pÄ servern" och "fil sparad i lagringen". Det senare betyder att andra enheter redan kan börja ta emot filen (med förbehÄll för att köerna arbetar med olika hastigheter).
- Eftersom informationen om överföringen nu kommer via olika kanaler, mÄste vi hitta lösningar för att fÄ status pÄ filbehandlingen. Som en följd av detta: i motsats till det klassiska request-response, kan klienten startas om under filbehandlingen, men statusen för just denna bearbetning kommer att vara korrekt. Detta punkt fungerar i praktiken "out of the box". Som en följd av detta Àr vi nu mer toleranta mot fel.
Sharding
Som redan beskrivits ovan, inom system med event sourcing finns det ingen strikt konsistens. Det betyder att vi kan anvÀnda flera lagringsutrymmen utan nÄgon synkronisering mellan dem. NÀr vi nÀrmar oss vÄr uppgift kan vi:
- Dela upp filer efter typer. Till exempel kan bilder/video avkodas och ett mer effektivt format vÀljas.
- Separera konton efter lÀnder. PÄ grund av mÄnga lagar kan detta krÀvas, men denna arkitekturschema möjliggör automatiskt detta.

Om du vill överföra data frÄn en lagring till en annan behövs redan standardmetoder. TyvÀrr Àr det nödvÀndigt att stoppa kön, göra migrationen och sedan starta den igen. Generellt sett kan data inte överföras 'on-the-fly', men om hÀndelsekön lagras helt och du har kopior av tidigare tillstÄnd av lagringen kan vi Äterspela hÀndelser pÄ följande sÀtt:
- I Event Source har varje hĂ€ndelse sitt eget identifierare (idealiskt â icke minskande). Det betyder att vi kan lĂ€gga till ett fĂ€lt i lagringen â id för den senaste behandlade posten.
- Vi duplicerar kön sÄ att alla hÀndelser kan behandlas för flera oberoende lagringar (den första Àr den dÀr data redan lagras, och den andra Àr ny, men för tillfÀllet tom). Den andra kön behandlas förstÄs inte för nÀrvarande.
- Vi startar den andra kön (det vill sÀga vi pÄbörjar Äterspelningen av hÀndelser).
- NÀr den nya kön relativt sett Àr tom (dvs. den genomsnittliga tidsdifferensen mellan tillÀgg av ett objekt och dess uttag Àr acceptabel) kan vi börja vÀxla lÀsare till den nya lagringen.
Som vi kan se har vi i vÄrt system varken haft eller har strikt överensstÀmmelse. Det finns bara eventual consistency, det vill sÀga en garanti för att hÀndelser behandlas i samma ordning (men eventuellt med olika fördröjningar). Och genom att utnyttja detta kan vi relativt enkelt flytta data utan att stoppa systemet till andra sidan av jorden.
SÄledes, fortsÀtter vÄrt exempel om en online-lagring för filer, ger denna arkitektur oss redan ett antal fördelar:
- Vi kan flytta objekt nÀrmare anvÀndarna, och detta görs dynamiskt. PÄ sÄ sÀtt kan vi förbÀttra kvaliteten pÄ tjÀnsten.
- Vi kan lagra en del av data inom företagen. Till exempel krÀver Enterprise-anvÀndare ofta att lagra sina data i kontrollerade datacenter (för att förhindra dataintrÄng). Genom sharding kan vi enkelt stödja detta. Och uppgiften förenklas ytterligare om kunden har en kompatibel molnlösning (till exempel, ).
- Och det viktigaste Àr att vi kanske inte behöver göra det. För i början skulle ett enda lagringsutrymme för alla konton vara tillrÀckligt (sÄ att vi snabbt kan börja arbeta). Och den centrala egenskapen hos detta system Àr, Àven om det Àr skalbart, att det i början Àr ganska enkelt. Man behöver helt enkelt inte skriva kod som arbetar med en miljon separata oberoende köer och sÄ vidare direkt. Om det behövs kan det göras i framtiden.
Statisk InnehÄlls-hosting
Denna punkt kan verka helt uppenbar, men den Àr ÀndÄ nödvÀndig för ett mer eller mindre standardbelastat program. Dess kÀrna Àr enkel: allt statiskt innehÄll levereras inte frÄn samma server som applikationen, utan frÄn sÀrskilda servrar som Àr avsedda för detta. Som en följd av detta utförs dessa operationer snabbare (exempelvis levererar nginx filer snabbare och mer kostnadseffektivt Àn en Java-server). Dessutom möjliggör arkitekturen för CDN () att placera vÄra filer nÀrmare slutanvÀndarna, vilket positivt pÄverkar anvÀndarvÀnligheten.
Det enklaste och mest standardiserade exemplet pĂ„ statiskt innehĂ„ll Ă€r en uppsĂ€ttning skript och bilder för en webbplats. Det Ă€r enkelt â de Ă€r kĂ€nda pĂ„ förhand, och arkivet laddas upp pĂ„ CDN-servrarna, frĂ„n vilka det distribueras till slutanvĂ€ndarna.
Men i verkligheten kan en nÄgot liknande metod som lambda-arkitekturen tillÀmpas för statiskt innehÄll. LÄt oss ÄtervÀnda till vÄrt uppdrag (ett online lagringsutrymme för filer), dÀr vi behöver distribuera filer till anvÀndarna. Den enklaste lösningen Àr att skapa en tjÀnst som för varje anvÀndarförfrÄgan gör alla nödvÀndiga kontroller (auktorisering och sÄ vidare) och sedan laddar ner filen direkt frÄn vÄrt lagringsutrymme. Huvudnackdelen med denna metod Àr att det statiska innehÄllet (och en fil med en specifik_revision fungerar i grunden som statiskt innehÄll) levereras av samma server som innehÄller affÀrslogiken. IstÀllet kan man göra följande schema:
- Servern ger en URL för nedladdning. Den kan se ut som file_id + key, dÀr key Àr en miniatyrdigital signatur som ger tillgÄng till resursen under de nÀrmaste 24 timmarna.
- Nedladdningen av filen hanteras av en enkel nginx med följande alternativ:
- Caching av innehÄll. Eftersom den hÀr tjÀnsten kan vara pÄ en separat server har vi kvar en framtidslösning för att lagra alla de senaste nedladdade filerna pÄ disken.
- Nyckelkontroll vid anslutningsskapande
- Valfritt: strömmande bearbetning av innehÄll. Till exempel, om vi komprimerar alla filer i tjÀnsten, kan vi göra dekomprimeringen direkt i denna modul. Som en följd: IO-operationer genomförs dÀr de hör hemma. En Java-arkivare kan lÀtt anvÀnda mycket extra minne, men att skriva om tjÀnsten med affÀrslogik pÄ rust/c++ kan visa sig vara ineffektivt. I vÄrt fall anvÀnds olika processer (eller till och med tjÀnster), sÄ vi kan effektivt separera affÀrslogik och IO-operationer.

Denna typ av schema liknar inte mycket pÄ distribution av statiskt innehÄll (eftersom vi inte laddar upp hela paketet med statik nÄgonstans), men i verkligheten hanterar denna metod just distribution av oförÀnderliga data. Dessutom kan detta schema generaliseras till andra fall dÀr innehÄllet inte bara Àr statiskt utan kan presenteras i form av en uppsÀttning oförÀnderliga och oelimineringbara block (Àven om de kan lÀggas till).
Som ett annat exempel (för att befÀsta): om du har arbetat med Jenkins/TeamCity vet du att bÄda lösningarna Àr skrivna i Java. BÄda Àr en Java-proces som hanterar bÄde orkestrering av byggen och innehÄllshantering. Specifikt har de bÄda uppgifter som "överföra fil/-mapp frÄn servern". Ett exempel: utfÀrdande av artefakter, överföring av kÀllkod (nÀr agenten inte laddar ner koden direkt frÄn repot utan det gör servern), Ätkomst till loggar. Alla dessa uppgifter skiljer sig i belastning pÄ IO. SÄledes blir det sÄ att servern, som ansvarar för komplex affÀrslogik, ocksÄ mÄste kunna effektivt hantera stora datamÀngder. Och det mest intressanta Àr att en sÄdan operation kan delegeras till nginx enligt exakt samma schema (med tillÀgget av nyckeldatan i begÀran).
Men om vi ÄtergÄr till vÄrt system, sÄ ser vi ett sÄdant schema:

Som det framgÄr har systemet blivit radikalt mer komplicerat. Det Àr inte lÀngre bara en mini-process som lagrar filer lokalt. Nu krÀvs inte bara enklare support, utan ocksÄ versionskontroll av API:er osv. DÀrför, efter att alla diagram har ritats, Àr det bÀst att noggrant övervÀga om kostnaderna för skalbarheten Àr vÀrda det. Men om du vill ha möjlighet att skala systemet (inklusive för att arbeta med Ànnu fler anvÀndare), mÄste du acceptera sÄdana lösningar. Som ett resultat Àr systemets arkitektur redo för ökad belastning (nÀstan varje komponent kan klonas för horisontell skalning). Systemet kan uppdateras utan att stÀngas ner (enbart vissa operationer kan bromsas nÄgot).
Som jag nÀmnde i början, har flera internet-tjÀnster nu fÄtt ökad belastning. Och vissa av dem har helt enkelt slutat fungera korrekt. I sjÀlva verket misslyckades systemen precis nÀr företagen skulle börja tjÀna pengar. Det vill sÀga, istÀllet för att skjuta fram leveranser, istÀllet för att erbjuda kunderna "planera leveranser över de kommande mÄnaderna", sa systemet helt enkelt "gÄ till konkurrenterna". Och det Àr kostnaden för lÄg prestanda: förluster kommer att intrÀffa precis nÀr vinsten skulle vara som högst.
Slutsats
Alla dessa tillvÀgagÄngssÀtt har varit kÀnda tidigare. VK har lÀnge anvÀnt idén om Static Content Hosting för att leverera bilder. MÄnga online-spel anvÀnder en Sharding-plan för att dela upp spelare efter regioner eller för att dela upp spelmiljöer (om vÀrlden Àr en enhet). Event Sourcing-ansatsen anvÀnds aktivt inom e-post. De flesta handelsapplikationer, dÀr data kontinuerligt kommer in, Àr faktiskt byggda pÄ CQRS-ansatsen för att kunna filtrera den mottagna datan. Och horisontell skalning har anvÀnts lÀnge i mÄnga tjÀnster.
Men det viktigaste Àr att alla dessa mönster har blivit mycket lÀtta att tillÀmpa i moderna applikationer (om de Àr relevanta, förstÄs). MolntjÀnster erbjuder sharding och horisontell skalning som standard, vilket Àr mycket enklare Àn att bestÀlla olika dedikerade servrar i olika datacenter sjÀlv. CQRS har blivit mycket enklare, delvis tack vare utvecklingen av bibliotek som RX. För tio Är sedan skulle en sÀllsynt webbplats kunna stödja sÄdana saker. Event sourcing stÀller in sig ocksÄ otroligt lÀtt tack vare redan fÀrdiga containrar med Apache Kafka. För tio Är sedan hade detta varit en innovation, nu Àr det en sjÀlvklarhet. PÄ samma sÀtt med statisk innehÄllshosting: pÄ grund av mer anvÀndarvÀnlig teknik (dÀribland för att det finns detaljerad dokumentation och en stor databas med svar), har denna metod blivit Ànnu enklare.
Som en följd har implementeringen av ett antal ganska komplexa arkitektoniska mönster nu blivit mycket enklare, vilket gör att man bör se nÀrmare pÄ dem i förvÀg. Om ett tioÄrigt program övergav en av de ovanstÄende lösningarna pÄ grund av höga kostnader för implementering och drift, kan man nu, i ett nytt program, eller efter refaktorisering, skapa en tjÀnst som arkitektoniskt redan kommer att vara bÄde skalbar (med avseende pÄ prestanda) och redo för nya krav frÄn kunderna (till exempel för lokalisering av personuppgifter).
Och det viktigaste: anvÀnd inte dessa metoder om ni har en enkel applikation. Ja, de Àr vackra och intressanta, men för en webbplats med en toppbesökare pÄ 100 personer kan man ofta klara sig med en klassisk monolit (Ätminstone utvÀndigt, internt kan allt brytas ner i moduler etc.).
KĂ€lla: habr.com
