Byggstenar för distribuerade applikationer. Andra approximationen

Meddelande

Kolleger, i mitten av sommaren planerar jag att släppa en annan serie artiklar om designen av kösystem: "VTrade Experiment" - ett försök att skriva ett ramverk för handelssystem. Serien kommer att undersöka teorin och praktiken för att bygga en börs, auktion och butik. I slutet av artikeln inbjuder jag dig att rösta på de ämnen som intresserar dig mest.

Byggstenar för distribuerade applikationer. Andra approximationen

Detta är den sista artikeln i serien om distribuerade reaktiva applikationer i Erlang/Elixir. I första artikeln du kan hitta de teoretiska grunderna för reaktiv arkitektur. Andra artikeln illustrerar de grundläggande mönstren och mekanismerna för att konstruera sådana system.

Idag kommer vi att ta upp frågor om utveckling av kodbasen och projekt i allmänhet.

Organisation av tjänster

I verkligheten, när man utvecklar en tjänst, måste man ofta kombinera flera interaktionsmönster i en styrenhet. Användartjänsten, som löser problemet med att hantera projektanvändarprofiler, måste till exempel svara på förfrågningar om req-resp och rapportera profiluppdateringar via pub-sub. Det här fallet är ganska enkelt: bakom meddelandehantering finns det en styrenhet som implementerar tjänstelogiken och publicerar uppdateringar.

Situationen blir mer komplicerad när vi behöver implementera en feltolerant distribuerad tjänst. Låt oss föreställa oss att kraven för användare har ändrats:

  1. nu ska tjänsten behandla förfrågningar på 5 klusternoder,
  2. kunna utföra bakgrundsbearbetningsuppgifter,
  3. och även dynamiskt kunna hantera prenumerationslistor för profiluppdateringar.

anmärkning: Vi överväger inte frågan om konsekvent lagring och datareplikering. Låt oss anta att dessa problem har lösts tidigare och att systemet redan har ett tillförlitligt och skalbart lager, och hanterare har mekanismer för att interagera med det.

Den formella beskrivningen av användartjänsten har blivit mer komplicerad. Ur en programmerares synvinkel är förändringar minimala på grund av användningen av meddelanden. För att uppfylla det första kravet måste vi konfigurera balansering vid req-resp utbytespunkten.

Kravet på att bearbeta bakgrundsuppgifter förekommer ofta. Hos användare kan detta vara att kontrollera användardokument, bearbeta nedladdade multimedia eller synkronisera data med sociala medier. nätverk. Dessa uppgifter måste på något sätt fördelas inom klustret och framstegen i exekveringen övervakas. Därför har vi två lösningsalternativ: antingen använd uppgiftsdistributionsmallen från föregående artikel, eller, om den inte passar, skriv en anpassad uppgiftsschemaläggare som kommer att hantera poolen av processorer på det sätt vi behöver.

Punkt 3 kräver pub-sub-malltillägget. Och för implementering, efter att ha skapat en pub-sub-utbytespunkt, måste vi dessutom lansera kontrollern för denna punkt inom vår tjänst. Således är det som om vi flyttar logiken för att bearbeta prenumerationer och avregistreringar från meddelandelagret till implementeringen av användare.

Som ett resultat visade nedbrytningen av problemet att för att uppfylla kraven måste vi lansera 5 instanser av tjänsten på olika noder och skapa en ytterligare enhet - en pub-sub-kontrollant, ansvarig för prenumerationen.
För att köra 5 hanterare behöver du inte ändra servicekoden. Den enda ytterligare åtgärden är att sätta upp balanseringsregler vid bytespunkten, som vi kommer att prata om lite senare.
Det finns också en extra komplexitet: pub-underkontrollern och den anpassade uppgiftsschemaläggaren måste fungera i en enda kopia. Återigen måste meddelandetjänsten, som en grundläggande tjänst, tillhandahålla en mekanism för att välja en ledare.

Ledarens val

I distribuerade system är ledareval proceduren för att utse en enda process som ansvarar för att schemalägga distribuerad bearbetning av en viss belastning.

I system som inte är benägna att centralisera, används universella och konsensusbaserade algoritmer, såsom paxos eller raft.
Eftersom meddelandehantering är en mäklare och ett centralt element känner den till alla tjänstekontrollanter - kandidatledare. Meddelanden kan utse en ledare utan att rösta.

Efter start och anslutning till utbytespunkten får alla tjänster ett systemmeddelande #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Om LeaderPid sammanfaller med pid nuvarande process, det utses till ledare, och listan Servers inkluderar alla noder och deras parametrar.
I det ögonblick som en ny klusternod dyker upp och en fungerande klusternod är frånkopplad, får alla tjänstekontroller #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} respektive.

På så sätt är alla komponenter medvetna om alla förändringar, och klustret har garanterat en ledare vid varje given tidpunkt.

Förmedlarna

För att implementera komplexa distribuerade bearbetningsprocesser, såväl som i problem med att optimera en befintlig arkitektur, är det bekvämt att använda mellanhänder.
För att inte ändra tjänstekoden och lösa till exempel problem med ytterligare bearbetning, dirigering eller loggning av meddelanden, kan du aktivera en proxyhanterare innan tjänsten, som kommer att utföra allt extra arbete.

Ett klassiskt exempel på pub-sub-optimering är en distribuerad applikation med en affärskärna som genererar uppdateringshändelser, såsom prisförändringar på marknaden, och ett accesslager - N-servrar som tillhandahåller ett websocket API för webbklienter.
Om du bestämmer dig direkt ser kundtjänsten ut så här:

  • klienten upprättar förbindelser med plattformen. På den sida av servern som avslutar trafiken startas en process för att betjäna denna anslutning.
  • I samband med serviceprocessen sker auktorisering och prenumeration på uppdateringar. Processen kallar prenumerationsmetoden för ämnen.
  • När en händelse har genererats i kärnan, levereras den till processerna som servar anslutningarna.

Låt oss föreställa oss att vi har 50000 5 prenumeranter på ämnet "nyheter". Prenumeranter är jämnt fördelade över 50000 servrar. Som ett resultat kommer varje uppdatering, som kommer till utbytespunkten, att replikeras 10000 XNUMX gånger: XNUMX XNUMX gånger på varje server, beroende på antalet prenumeranter på den. Inte ett särskilt effektivt system, eller hur?
För att förbättra situationen, låt oss introducera en proxy som har samma namn som utbytespunkten. Den globala namnregistratorn måste kunna returnera närmaste process med namn, detta är viktigt.

Låt oss starta den här proxyn på åtkomstskiktsservrarna, och alla våra processer som betjänar websocket-api kommer att prenumerera på den, och inte på den ursprungliga pub-sub-utbytespunkten i kärnan. Proxy prenumererar på kärnan endast i fallet med en unik prenumeration och replikerar det inkommande meddelandet till alla dess prenumeranter.
Som ett resultat kommer 5 meddelanden att skickas mellan kärnan och åtkomstservrarna, istället för 50000 XNUMX.

Routing och balansering

Req-Resp

I den nuvarande implementeringen av meddelanden finns det sju distributionsstrategier för begäran:

  • default. Begäran skickas till alla kontrollanter.
  • round-robin. Förfrågningar är uppräknade och cykliskt fördelade mellan styrenheter.
  • consensus. Kontrollanterna som betjänar tjänsten är indelade i ledare och slavar. Förfrågningar skickas endast till ledaren.
  • consensus & round-robin. Gruppen har en ledare, men önskemål fördelas på alla medlemmar.
  • sticky. Hashfunktionen beräknas och tilldelas en specifik hanterare. Efterföljande förfrågningar med denna signatur går till samma hanterare.
  • sticky-fun. Vid initialisering av utbytespunkten, hashberäkningsfunktionen för sticky balansering.
  • fun. I likhet med sticky-fun kan bara du dessutom omdirigera, avvisa eller förbearbeta det.

Distributionsstrategin ställs in när utbytespunkten initieras.

Förutom balansering låter meddelandehantering dig tagga enheter. Låt oss titta på typerna av taggar i systemet:

  • Anslutningstagg. Låter dig förstå genom vilket samband händelserna kom. Används när en styrprocess ansluter till samma växlingspunkt, men med olika routingnycklar.
  • Servicebricka. Låter dig kombinera hanterare i grupper för en tjänst och utöka routing- och balanseringsmöjligheter. För req-resp-mönstret är routing linjär. Vi skickar en förfrågan till bytesstället, sedan skickar den vidare till tjänsten. Men om vi behöver dela upp hanterarna i logiska grupper, görs uppdelningen med taggar. När du anger en tagg kommer begäran att skickas till en specifik grupp av kontroller.
  • Begär tagg. Gör att du kan skilja mellan svaren. Eftersom vårt system är asynkront måste vi kunna ange en RequestTag för att bearbeta servicesvar när vi skickar en förfrågan. Från det kommer vi att kunna förstå svaret på vilken begäran som kom till oss.

Pub-sub

För pub-sub är allt lite enklare. Vi har en utbytespunkt dit meddelanden publiceras. Utbytespunkten distribuerar meddelanden bland abonnenter som har prenumererat på de dirigeringsnycklar de behöver (vi kan säga att detta är analogt med ämnen).

Skalbarhet och feltolerans

Skalbarheten av systemet som helhet beror på graden av skalbarhet hos lagren och komponenterna i systemet:

  • Tjänster skalas genom att lägga till ytterligare noder i klustret med hanterare för denna tjänst. Under provdrift kan du välja den optimala balanseringspolicyn.
  • Själva meddelandetjänsten inom ett separat kluster skalas i allmänhet antingen genom att flytta särskilt laddade utbytespunkter till separata klusternoder, eller genom att lägga till proxyprocesser till särskilt laddade områden i klustret.
  • Hela systemets skalbarhet som egenskap beror på arkitekturens flexibilitet och förmågan att kombinera enskilda kluster till en gemensam logisk enhet.

Framgången för ett projekt beror ofta på enkelheten och hastigheten i skalningen. Meddelanden i sin nuvarande version växer tillsammans med applikationen. Även om vi saknar ett kluster på 50-60 maskiner kan vi ta till federation. Tyvärr ligger ämnet federation utanför ramen för denna artikel.

Bokning

När vi analyserade lastbalansering diskuterade vi redan redundans för tjänstekontroller. Men meddelanden måste också reserveras. I händelse av en nod eller maskinkrasch ska meddelanden automatiskt återställas, och på kortast möjliga tid.

I mina projekt använder jag ytterligare noder som tar upp belastningen vid ett fall. Erlang har en standardimplementering av distribuerat läge för OTP-applikationer. Distribuerat läge utför återställning i händelse av fel genom att starta den misslyckade applikationen på en annan tidigare startad nod. Processen är transparent, efter ett fel flyttas applikationen automatiskt till failovernoden. Du kan läsa mer om denna funktion här.

Производительность

Låt oss försöka åtminstone grovt jämföra prestandan för rabbitmq och våra anpassade meddelanden.
jag hittade officiella resultat rabbitmq-testning från openstack-teamet.

I punkt 6.14.1.2.1.2.2. Originaldokumentet visar resultatet av RPC CAST:
Byggstenar för distribuerade applikationer. Andra approximationen

Vi kommer inte att göra några ytterligare inställningar för OS-kärnan eller erlang VM i förväg. Villkor för testning:

  • erl väljer: +A1 +sbtu.
  • Testet inom en enda erlang-nod körs på en bärbar dator med en gammal i7 i mobilversion.
  • Klustertester utförs på servrar med ett 10G-nätverk.
  • Koden körs i hamnarcontainrar. Nätverk i NAT-läge.

Testkod:

req_resp_bench(_) ->
  W = perftest:comprehensive(10000,
    fun() ->
      messaging:request(?EXCHANGE, default, ping, self()),
      receive
        #'$msg'{message = pong} -> ok
      after 5000 ->
        throw(timeout)
      end
    end
  ),
  true = lists:any(fun(E) -> E >= 30000 end, W),
  ok.

Scenario 1: Testet körs på en bärbar dator med en gammal i7 mobilversion. Testet, meddelandena och tjänsten körs på en nod i en Docker-behållare:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

Scenario 2: 3 noder som körs på olika maskiner under docker (NAT).

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

I alla fall översteg inte CPU-användningen 250 %

Resultat av

Jag hoppas att den här cykeln inte ser ut som en mind dump och min erfarenhet kommer att vara till stor nytta för både forskare av distribuerade system och praktiker som är i början av att bygga distribuerade arkitekturer för sina affärssystem och som tittar på Erlang/Elixir med intresse , men tvivlar på om det är värt...

Photo Shoot @chuttersnap

Endast registrerade användare kan delta i undersökningen. Logga in, Snälla du.

Vilka ämnen bör jag ta upp mer i detalj som en del av VTrade Experiment-serien?

  • Teori: Marknader, order och deras timing: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Orderbok. Teori och praktik för att implementera en bok med grupperingar

  • Visualisering av handel: Ticks, staplar, resolutioner. Hur man förvarar och hur man limmar

  • Backoffice. Planering och utveckling. Medarbetarövervakning och incidentutredning

  • API. Låt oss ta reda på vilka gränssnitt som behövs och hur man implementerar dem

  • Informationslagring: PostgreSQL, Timescale, Tarantool i handelssystem

  • Reaktivitet i handelssystem

  • Övrig. Jag skriver i kommentarerna

6 användare röstade. 4 användare avstod från att rösta.

Källa: will.com

Lägg en kommentar