Byggesteiner av distribuerte applikasjoner. Andre tilnærming

Kunngjøring

Kolleger, midt på sommeren planlegger jeg å gi ut en annen serie artikler om design av køsystemer: "VTrade Experiment" - et forsøk på å skrive et rammeverk for handelssystemer. Serien vil undersøke teori og praksis for å bygge en børs, auksjon og butikk. På slutten av artikkelen inviterer jeg deg til å stemme på emnene som interesserer deg mest.

Byggesteiner av distribuerte applikasjoner. Andre tilnærming

Dette er den siste artikkelen i serien om distribuerte reaktive applikasjoner i Erlang/Elixir. I første artikkel du kan finne det teoretiske grunnlaget for reaktiv arkitektur. Andre artikkel illustrerer de grunnleggende mønstrene og mekanismene for å konstruere slike systemer.

I dag vil vi ta opp spørsmål om utvikling av kodebasen og prosjekter generelt.

Organisering av tjenester

I det virkelige liv, når du utvikler en tjeneste, må du ofte kombinere flere interaksjonsmønstre i en kontroller. For eksempel må brukertjenesten, som løser problemet med å administrere prosjektbrukerprofiler, svare på req-resp forespørsler og rapportere profiloppdateringer via pub-sub. Denne saken er ganske enkel: bak meldingstjenester er det én kontroller som implementerer tjenestelogikken og publiserer oppdateringer.

Situasjonen blir mer komplisert når vi skal implementere en feiltolerant distribuert tjeneste. La oss forestille oss at kravene til brukere har endret seg:

  1. nå skal tjenesten behandle forespørsler på 5 klyngenoder,
  2. kunne utføre bakgrunnsbehandlingsoppgaver,
  3. og også kunne administrere abonnementslister for profiloppdateringer dynamisk.

Kommentar: Vi vurderer ikke spørsmålet om konsistent lagring og datareplikering. La oss anta at disse problemene har blitt løst tidligere og at systemet allerede har et pålitelig og skalerbart lagringslag, og behandlere har mekanismer for å samhandle med det.

Den formelle beskrivelsen av brukertjenesten har blitt mer komplisert. Fra en programmerers synspunkt er endringer minimale på grunn av bruk av meldinger. For å tilfredsstille det første kravet, må vi konfigurere balansering ved req-resp utvekslingspunktet.

Kravet om å behandle bakgrunnsoppgaver forekommer ofte. Hos brukere kan dette være å sjekke brukerdokumenter, behandle nedlastede multimedia eller synkronisere data med sosiale medier. nettverk. Disse oppgavene må på en eller annen måte fordeles innenfor klyngen og fremdriften i utførelse overvåkes. Derfor har vi to løsningsalternativer: enten bruk oppgavedistribusjonsmalen fra forrige artikkel, eller, hvis den ikke passer, skriv en tilpasset oppgaveplanlegger som vil administrere utvalget av prosessorer på den måten vi trenger.

Punkt 3 krever pub-sub-malutvidelsen. Og for implementering, etter å ha opprettet et pub-sub-utvekslingspunkt, må vi i tillegg starte kontrolleren for dette punktet i tjenesten vår. Dermed er det som om vi flytter logikken for behandling av abonnementer og avmeldinger fra meldingslaget til implementering av brukere.

Som et resultat viste dekomponeringen av problemet at for å oppfylle kravene, må vi lansere 5 forekomster av tjenesten på forskjellige noder og opprette en ekstra enhet - en pub-underkontroller, ansvarlig for abonnementet.
For å kjøre 5 behandlere trenger du ikke endre servicekoden. Den eneste ekstra handlingen er å sette opp balanseringsregler ved byttepunktet, som vi skal snakke om litt senere.
Det er også en ekstra kompleksitet: pub-underkontrolleren og den tilpassede oppgaveplanleggeren må fungere i en enkelt kopi. Igjen, meldingstjenesten, som en grunnleggende tjeneste, må gi en mekanisme for å velge en leder.

Ledervalg

I distribuerte systemer er ledervalg prosedyren for å utnevne en enkelt prosess som er ansvarlig for å planlegge distribuert behandling av en viss belastning.

I systemer som ikke er utsatt for sentralisering, brukes universelle og konsensusbaserte algoritmer, som paxos eller raft.
Siden meldingstjenester er en megler og et sentralt element, kjenner den til alle tjenestekontrollører - kandidatledere. Meldinger kan utnevne en leder uten å stemme.

Etter start og tilkobling til utvekslingspunktet mottar alle tjenester en systemmelding #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Hvis LeaderPid sammenfaller med pid gjeldende prosess, er det oppnevnt som leder, og listen Servers inkluderer alle noder og deres parametere.
I det øyeblikket en ny dukker opp og en fungerende klyngennode er frakoblet, mottar alle tjenestekontrollere #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} henholdsvis.

På denne måten er alle komponenter oppmerksomme på alle endringer, og klyngen har garantert én leder til enhver tid.

Mellommenn

For å implementere komplekse distribuerte prosesseringsprosesser, så vel som i problemer med å optimalisere en eksisterende arkitektur, er det praktisk å bruke mellomledd.
For ikke å endre tjenestekoden og løse for eksempel problemer med tilleggsbehandling, ruting eller logging av meldinger, kan du aktivere en proxy-behandler før tjenesten, som vil utføre alt tilleggsarbeidet.

Et klassisk eksempel på pub-sub-optimalisering er en distribuert applikasjon med en forretningskjerne som genererer oppdateringshendelser, for eksempel prisendringer i markedet, og et tilgangslag - N-servere som gir en websocket API for nettklienter.
Hvis du bestemmer deg direkte, ser kundeservice slik ut:

  • klienten etablerer forbindelser med plattformen. På siden av serveren som avslutter trafikken, startes en prosess for å betjene denne forbindelsen.
  • I forbindelse med tjenesteprosessen skjer autorisasjon og abonnement på oppdateringer. Prosessen kaller abonnementsmetoden for emner.
  • Når en hendelse er generert i kjernen, leveres den til prosessene som betjener forbindelsene.

La oss forestille oss at vi har 50000 5 abonnenter på emnet "nyheter". Abonnenter er jevnt fordelt på 50000 servere. Som et resultat vil hver oppdatering, som ankommer utvekslingspunktet, bli replikert 10000 XNUMX ganger: XNUMX XNUMX ganger på hver server, i henhold til antall abonnenter på den. Ikke en veldig effektiv ordning, ikke sant?
For å forbedre situasjonen, la oss introdusere en proxy som har samme navn som byttepunktet. Den globale navneregistratoren må kunne returnere nærmeste prosess ved navn, dette er viktig.

La oss starte denne proxyen på tilgangslagets servere, og alle våre prosesser som betjener websocket-api vil abonnere på den, og ikke til det opprinnelige pub-sub-utvekslingspunktet i kjernen. Proxy abonnerer kun på kjernen i tilfelle av et unikt abonnement og replikerer den innkommende meldingen til alle abonnentene.
Som et resultat vil 5 meldinger sendes mellom kjernen og tilgangsservere, i stedet for 50000 XNUMX.

Ruting og balansering

Req-Resp

I den nåværende meldingsimplementeringen er det 7 strategier for distribusjon av forespørsel:

  • default. Forespørselen sendes til alle kontrollører.
  • round-robin. Forespørsler er oppregnet og syklisk fordelt mellom kontrollerene.
  • consensus. Kontrollørene som betjener tjenesten er delt inn i ledere og slaver. Forespørsler sendes kun til leder.
  • consensus & round-robin. Gruppen har en leder, men ønsker fordeles på alle medlemmer.
  • sticky. Hash-funksjonen beregnes og tilordnes en spesifikk behandler. Påfølgende forespørsler med denne signaturen går til samme behandler.
  • sticky-fun. Ved initialisering av byttepunktet vil hashberegningsfunksjonen for sticky balansering.
  • fun. I likhet med sticky-fun, er det bare du som i tillegg kan omdirigere, avvise eller forhåndsbehandle den.

Distribusjonsstrategien settes når byttepunktet initialiseres.

I tillegg til balansering lar meldingstjenester deg merke enheter. La oss se på typene tagger i systemet:

  • Tilkoblingsmerke. Lar deg forstå hvilken sammenheng hendelsene kom. Brukes når en kontrollerprosess kobles til samme utvekslingspunkt, men med forskjellige rutenøkler.
  • Servicebrikke. Lar deg kombinere behandlere i grupper for én tjeneste og utvide ruting- og balanseringsmuligheter. For req-resp-mønsteret er ruting lineær. Vi sender en forespørsel til byttepunktet, så sender det den videre til tjenesten. Men hvis vi trenger å dele opp behandlerne i logiske grupper, så gjøres delingen ved hjelp av tagger. Når du spesifiserer en tag, vil forespørselen bli sendt til en bestemt gruppe kontroller.
  • Be om tag. Lar deg skille mellom svar. Siden systemet vårt er asynkront, må vi for å behandle tjenestesvar kunne spesifisere en RequestTag når vi sender en forespørsel. Fra det vil vi kunne forstå svaret på hvilken forespørsel som kom til oss.

Pub-sub

For pub-sub er alt litt enklere. Vi har et utvekslingspunkt som meldinger publiseres til. Utvekslingspunktet distribuerer meldinger blant abonnenter som har abonnert på rutenøklene de trenger (vi kan si at dette er analogt med emner).

Skalerbarhet og feiltoleranse

Skalerbarheten til systemet som helhet avhenger av graden av skalerbarhet av lagene og komponentene i systemet:

  • Tjenester skaleres ved å legge til flere noder til klyngen med behandlere for denne tjenesten. Under prøvedrift kan du velge den optimale balansepolitikken.
  • Selve meldingstjenesten innenfor en separat klynge skaleres vanligvis enten ved å flytte spesielt belastede utvekslingspunkter til separate klyngenoder, eller ved å legge til proxy-prosesser til spesielt belastede områder av klyngen.
  • Skalerbarheten til hele systemet som karakteristikk avhenger av fleksibiliteten til arkitekturen og evnen til å kombinere individuelle klynger til en felles logisk enhet.

Suksessen til et prosjekt avhenger ofte av enkelheten og hastigheten på skaleringen. Meldingstjenester i sin nåværende versjon vokser sammen med applikasjonen. Selv om vi mangler en klynge på 50-60 maskiner, kan vi ty til føderasjon. Dessverre er temaet forbund utenfor rammen av denne artikkelen.

Reservasjon

Når vi analyserte lastbalansering, diskuterte vi allerede redundans for tjenestekontrollere. Men meldinger må også reserveres. I tilfelle en node eller maskinkrasj, skal meldinger automatisk gjenopprettes, og på kortest mulig tid.

I mine prosjekter bruker jeg ekstra noder som tar opp lasten ved fall. Erlang har en standard distribuert modusimplementering for OTP-applikasjoner. Distribuert modus utfører gjenoppretting i tilfelle feil ved å starte det mislykkede programmet på en annen tidligere lansert node. Prosessen er gjennomsiktig; etter en feil flyttes applikasjonen automatisk til failover-noden. Du kan lese mer om denne funksjonaliteten her.

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

La oss prøve å i det minste grovt sammenligne ytelsen til rabbitmq og våre tilpassede meldinger.
jeg fant offisielle resultater rabbitmq-testing fra openstack-teamet.

I avsnitt 6.14.1.2.1.2.2. Det originale dokumentet viser resultatet av RPC CAST:
Byggesteiner av distribuerte applikasjoner. Andre tilnærming

Vi vil ikke gjøre noen ekstra innstillinger til OS-kjernen eller erlang VM på forhånd. Vilkår for testing:

  • erl velger: +A1 +sbtu.
  • Testen innenfor en enkelt erlang-node kjøres på en bærbar PC med en gammel i7 i mobilversjon.
  • Klyngetester utføres på servere med 10G-nettverk.
  • Koden kjører i docker-containere. Nettverk i NAT-modus.

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.

Scenario 1: Testen kjøres på en bærbar PC med en gammel i7 mobilversjon. Testen, meldingene og tjenesten utføres på én node i én Docker-beholder:

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 kjører på forskjellige 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 tilfeller oversteg ikke CPU-bruken 250 %

Resultater av

Jeg håper denne syklusen ikke ser ut som en tankedump og min erfaring vil være til stor nytte for både forskere av distribuerte systemer og praktikere som er helt i begynnelsen av å bygge distribuerte arkitekturer for sine forretningssystemer og ser på Erlang/Elixir med interesse , men tviler på om det er verdt...

Bilde @chuttersnap

Kun registrerte brukere kan delta i undersøkelsen. Logg inn, vær så snill.

Hvilke emner bør jeg dekke mer detaljert som en del av VTrade Experiment-serien?

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

  • Ordrebok. Teori og praksis for å implementere en bok med grupperinger

  • Visualisering av handel: Haker, søyler, oppløsninger. Hvordan lagre og hvordan lim

  • Backoffice. Planlegging og utvikling. Ansattovervåking og hendelsesundersøkelse

  • API. La oss finne ut hvilke grensesnitt som trengs og hvordan de implementeres

  • Informasjonslagring: PostgreSQL, Timescale, Tarantool i handelssystemer

  • Reaktivitet i handelssystemer

  • Annen. Jeg skal skrive i kommentarfeltet

6 brukere stemte. 4 brukere avsto.

Kilde: www.habr.com

Legg til en kommentar