Byggeklodser af distribuerede applikationer. Anden tilnærmelse

annoncering

Kolleger, midt på sommeren planlægger jeg at udgive en anden serie af artikler om design af køsystemer: "VTrade Experiment" - et forsøg på at skrive en ramme for handelssystemer. Serien vil undersøge teori og praksis om at bygge en børs, auktion og butik. I slutningen af ​​artiklen inviterer jeg dig til at stemme på de emner, der interesserer dig mest.

Byggeklodser af distribuerede applikationer. Anden tilnærmelse

Dette er den sidste artikel i serien om distribuerede reaktive applikationer i Erlang/Elixir. I første artikel du kan finde det teoretiske grundlag for reaktiv arkitektur. Anden artikel illustrerer de grundlæggende mønstre og mekanismer til at konstruere sådanne systemer.

I dag vil vi rejse spørgsmål om udvikling af kodebasen og projekter generelt.

Tilrettelæggelse af tjenester

I det virkelige liv, når man udvikler en tjeneste, skal man ofte kombinere flere interaktionsmønstre i én controller. For eksempel skal brugertjenesten, som løser problemet med at administrere projektbrugerprofiler, svare på req-resp anmodninger og rapportere profilopdateringer via pub-sub. Dette tilfælde er ret simpelt: bag meddelelser er der en controller, der implementerer servicelogikken og udgiver opdateringer.

Situationen bliver mere kompliceret, når vi skal implementere en fejltolerant distribueret service. Lad os forestille os, at kravene til brugerne har ændret sig:

  1. nu skal tjenesten behandle anmodninger på 5 klynge noder,
  2. være i stand til at udføre baggrundsbehandlingsopgaver,
  3. og også være i stand til dynamisk at administrere abonnementslister til profilopdateringer.

Kommentar: Vi overvejer ikke spørgsmålet om konsekvent lagring og datareplikering. Lad os antage, at disse problemer er blevet løst tidligere, og at systemet allerede har et pålideligt og skalerbart lagerlag, og behandlere har mekanismer til at interagere med det.

Den formelle beskrivelse af brugerservicen er blevet mere kompliceret. Fra en programmørs synspunkt er ændringer minimale på grund af brugen af ​​beskeder. For at opfylde det første krav er vi nødt til at konfigurere balancering på req-resp udvekslingspunktet.

Kravet om at behandle baggrundsopgaver forekommer ofte. Hos brugere kan dette være at tjekke brugerdokumenter, behandle downloadede multimedier eller synkronisere data med sociale medier. netværk. Disse opgaver skal på en eller anden måde fordeles inden for klyngen, og udførelsens fremskridt skal overvåges. Derfor har vi to løsningsmuligheder: enten brug opgavefordelingsskabelonen fra den forrige artikel, eller, hvis det ikke passer, skriv en brugerdefineret opgaveplanlægger, der vil administrere puljen af ​​processorer på den måde, vi har brug for.

Punkt 3 kræver pub-sub skabelonudvidelsen. Og til implementering, efter at have oprettet et pub-sub-udvekslingspunkt, skal vi desuden starte controlleren for dette punkt i vores tjeneste. Det er således, som om vi flytter logikken for behandling af abonnementer og afmeldinger fra beskedlaget over i implementeringen af ​​brugere.

Som et resultat viste dekomponeringen af ​​problemet, at for at opfylde kravene skal vi lancere 5 forekomster af tjenesten på forskellige noder og oprette en ekstra enhed - en pub-sub-controller, der er ansvarlig for abonnementet.
For at køre 5 handlere behøver du ikke at ændre servicekoden. Den eneste yderligere handling er at opsætte balanceringsregler ved byttepunktet, som vi vil tale om lidt senere.
Der er også en ekstra kompleksitet: pub-sub-controlleren og den brugerdefinerede opgaveplanlægger skal fungere i en enkelt kopi. Igen skal beskedtjenesten, som en grundlæggende tjeneste, give en mekanisme til at vælge en leder.

Ledervalg

I distribuerede systemer er ledervalg proceduren for at udpege en enkelt proces, der er ansvarlig for at planlægge distribueret behandling af en vis belastning.

I systemer, der ikke er tilbøjelige til centralisering, anvendes universelle og konsensusbaserede algoritmer, såsom paxos eller raft.
Da messaging er en mægler og et centralt element, kender den til alle servicecontrollere - kandidatledere. Messaging kan udpege en leder uden at stemme.

Efter start og forbindelse til udvekslingspunktet modtager alle tjenester en systemmeddelelse #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Hvis LeaderPid falder sammen med pid nuværende proces, det er udpeget som leder, og listen Servers omfatter alle noder og deres parametre.
I det øjeblik en ny dukker op, og en fungerende klynge node er afbrudt, modtager alle servicecontrollere #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} hhv.

På denne måde er alle komponenter opmærksomme på alle ændringer, og klyngen er garanteret at have én leder på et givet tidspunkt.

Mellemmænd

For at implementere komplekse distribuerede behandlingsprocesser, såvel som i problemer med at optimere en eksisterende arkitektur, er det praktisk at bruge mellemled.
For ikke at ændre servicekoden og løse for eksempel problemer med yderligere behandling, routing eller logning af meddelelser, kan du aktivere en proxy-handler før tjenesten, som udfører alt det ekstra arbejde.

Et klassisk eksempel på pub-sub-optimering er en distribueret applikation med en forretningskerne, der genererer opdateringsbegivenheder, såsom prisændringer i markedet, og et adgangslag - N-servere, der leverer en websocket API til webklienter.
Hvis du beslutter dig direkte, så ser kundeservice sådan ud:

  • klienten etablerer forbindelser til platformen. På den side af serveren, der afslutter trafikken, startes en proces for at betjene denne forbindelse.
  • I forbindelse med serviceprocessen forekommer autorisation og abonnement på opdateringer. Processen kalder abonnementsmetoden for emner.
  • Når en hændelse er genereret i kernen, leveres den til de processer, der betjener forbindelserne.

Lad os forestille os, at vi har 50000 abonnenter på emnet "nyheder". Abonnenter er fordelt jævnt på 5 servere. Som et resultat heraf vil hver opdatering, der ankommer til udvekslingspunktet, blive replikeret 50000 gange: 10000 gange på hver server, alt efter antallet af abonnenter på den. Ikke en særlig effektiv ordning, vel?
For at forbedre situationen, lad os introducere en proxy, der har samme navn som byttepunktet. Den globale navneregistrator skal være i stand til at returnere den nærmeste proces ved navn, dette er vigtigt.

Lad os starte denne proxy på adgangslagets servere, og alle vores processer, der betjener websocket-api'et, vil abonnere på den og ikke på det originale pub-sub-udvekslingspunkt i kernen. Proxy abonnerer kun på kernen i tilfælde af et unikt abonnement og replikerer den indgående besked til alle dens abonnenter.
Som et resultat vil der blive sendt 5 beskeder mellem kernen og adgangsservere i stedet for 50000.

Routing og balancering

Req-Hhv

I den nuværende messaging-implementering er der 7 anmodningsdistributionsstrategier:

  • default. Anmodningen sendes til alle controllere.
  • round-robin. Anmodninger er opregnet og cyklisk fordelt mellem controllere.
  • consensus. De controllere, der betjener tjenesten, er opdelt i ledere og slaver. Anmodninger sendes kun til lederen.
  • consensus & round-robin. Gruppen har en leder, men ønsker fordeles blandt alle medlemmer.
  • sticky. Hash-funktionen beregnes og tildeles en specifik handler. Efterfølgende anmodninger med denne signatur går til den samme behandler.
  • sticky-fun. Ved initialisering af udvekslingspunktet vil hash-beregningsfunktionen for sticky balancering.
  • fun. I lighed med sticky-sjov er det kun du, der yderligere kan omdirigere, afvise eller forbehandle det.

Fordelingsstrategien indstilles, når byttepunktet initialiseres.

Ud over balancering giver beskeder dig mulighed for at tagge enheder. Lad os se på typerne af tags i systemet:

  • Forbindelsesmærke. Giver dig mulighed for at forstå, gennem hvilken forbindelse begivenhederne kom. Bruges, når en controller-proces forbindes til det samme udvekslingspunkt, men med forskellige routing-nøgler.
  • Servicemærke. Giver dig mulighed for at kombinere handlere i grupper for én tjeneste og udvide routing- og balanceringsmuligheder. For req-resp-mønsteret er routing lineær. Vi sender en forespørgsel til byttestedet, så sender det den videre til tjenesten. Men hvis vi skal opdele behandlerne i logiske grupper, så sker opdelingen ved hjælp af tags. Når du angiver et tag, vil anmodningen blive sendt til en bestemt gruppe af controllere.
  • Anmod om tag. Giver dig mulighed for at skelne mellem svar. Da vores system er asynkront, skal vi for at behandle servicesvar være i stand til at angive et RequestTag, når vi sender en anmodning. Ud fra det vil vi være i stand til at forstå svaret på hvilken anmodning, der kom til os.

Pub-sub

For pub-sub er alt lidt enklere. Vi har et udvekslingspunkt, hvortil beskeder offentliggøres. Udvekslingspunktet distribuerer beskeder blandt abonnenter, der har abonneret på de rutenøgler, de har brug for (vi kan sige, at dette er analogt med emner).

Skalerbarhed og fejltolerance

Skalerbarheden af ​​systemet som helhed afhænger af graden af ​​skalerbarhed af lagene og komponenterne i systemet:

  • Tjenester skaleres ved at tilføje yderligere noder til klyngen med handlere for denne tjeneste. Under prøvedrift kan du vælge den optimale balanceringspolitik.
  • Selve beskedtjenesten inden for en separat klynge skaleres generelt enten ved at flytte særligt belastede udvekslingspunkter til separate klyngeknuder eller ved at tilføje proxy-processer til særligt belastede områder af klyngen.
  • Skalerbarheden af ​​hele systemet som karakteristika afhænger af arkitekturens fleksibilitet og evnen til at kombinere individuelle klynger til en fælles logisk enhed.

Et projekts succes afhænger ofte af enkelheden og hastigheden af ​​skalering. Beskeder i sin nuværende version vokser sammen med applikationen. Selvom vi mangler en klynge på 50-60 maskiner, kan vi ty til føderation. Desværre er emnet forbund uden for rammerne af denne artikel.

Reservation

Ved analyse af belastningsbalancering diskuterede vi allerede redundans af servicecontrollere. Beskeder skal dog også reserveres. I tilfælde af en node eller en maskinnedbrud, bør beskeder automatisk genoprettes og på kortest mulig tid.

I mine projekter bruger jeg yderligere knudepunkter, der optager belastningen i tilfælde af et fald. Erlang har en standard implementeret distribueret tilstand til OTP-applikationer. Distribueret tilstand udfører gendannelse i tilfælde af fejl ved at starte den fejlbehæftede applikation på en anden tidligere lanceret node. Processen er gennemsigtig; efter en fejl flytter applikationen automatisk til failover-noden. Du kan læse mere om denne funktionalitet her.

Ydelse

Lad os prøve at i det mindste nogenlunde sammenligne ydeevnen af ​​rabbitmq og vores tilpassede meddelelser.
jeg fandt officielle resultater rabbitmq-test fra openstack-teamet.

I afsnit 6.14.1.2.1.2.2. Det originale dokument viser resultatet af RPC CAST:
Byggeklodser af distribuerede applikationer. Anden tilnærmelse

Vi vil ikke foretage yderligere indstillinger til OS-kernen eller erlang VM på forhånd. Betingelser for test:

  • erl vælger: +A1 +sbtu.
  • Testen indenfor en enkelt erlang node køres på en bærbar computer med en gammel i7 i mobilversion.
  • Klyngetest udføres på servere med et 10G-netværk.
  • Koden kører i docker-containere. Netværk i NAT-tilstand.

Testkode:

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.

Scenarie 1: Testen køres på en bærbar computer med en gammel i7 mobilversion. Testen, meddelelserne og tjenesten udføres på én node i én Docker-container:

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)

Scenarie 2: 3 noder kører på forskellige 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 alle tilfælde oversteg CPU-udnyttelsen ikke 250 %

Resultaterne af

Jeg håber, at denne cyklus ikke ligner en mind dump, og min erfaring vil være til virkelig gavn for både forskere af distribuerede systemer og praktikere, der er helt i begyndelsen af ​​at bygge distribuerede arkitekturer til deres forretningssystemer og ser på Erlang/Elixir med interesse , men er i tvivl om det er det værd...

Foto @chuttersnap

Kun registrerede brugere kan deltage i undersøgelsen. Log ind, Vær venlig.

Hvilke emner skal jeg dække mere detaljeret som en del af VTrade Experiment-serien?

  • Teori: Markeder, ordrer og deres timing: DAG, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Ordrebog. Teori og praksis for implementering af en bog med grupperinger

  • Visualisering af handel: Flåter, søjler, opløsninger. Sådan opbevares og limes

  • Kontor. Planlægning og udvikling. Medarbejderovervågning og hændelsesundersøgelse

  • API. Lad os finde ud af, hvilke grænseflader der er nødvendige, og hvordan de implementeres

  • Informationslagring: PostgreSQL, Timescale, Tarantool i handelssystemer

  • Reaktivitet i handelssystemer

  • Andet. Jeg skriver i kommentarerne

6 brugere stemte. 4 brugere undlod at stemme.

Kilde: www.habr.com

Tilføj en kommentar