Byggeklodser af distribuerede applikationer. Første tilgang

Byggeklodser af distribuerede applikationer. Første tilgang

I fortiden artiklen vi har analyseret det teoretiske grundlag for reaktiv arkitektur. Det er tid til at tale om datastrømme, måder at implementere reaktive Erlang/Elixir-systemer på og meddelelsesmønstre i dem:

  • Anmodning-svar
  • Request-Chunked Response
  • Svar med anmodning
  • Udgiv-abonner
  • Omvendt udgiv Abonner
  • Opgavefordeling

SOA, MSA og beskeder

SOA, MSA er systemarkitekturer, der definerer reglerne for byggesystemer, mens messaging giver primitiver for deres implementering.

Jeg ønsker ikke at udbrede denne eller hin systemarkitektur. Jeg går ind for anvendelsen af ​​den mest effektive og brugbare praksis for et bestemt projekt og en bestemt virksomhed. Uanset hvilket paradigme vi vælger, er det bedre at skabe systemblokke med øje på Unix-måden: komponenter med minimal tilslutning, ansvarlige for individuelle entiteter. API-metoder udfører de mest enkle handlinger med entiteter.

Messaging - som navnet antyder - en meddelelsesmægler. Dens hovedformål er at modtage og sende beskeder. Den er ansvarlig for grænseflader til afsendelse af information, dannelse af logiske kanaler til transmission af information i systemet, routing og balancering samt fejlhåndtering på systemniveau.
Den udviklede meddelelse forsøger ikke at konkurrere med eller erstatte rabbitmq. Dens hovedtræk:

  • Fordeling.
    Udvekslingspunkter kan oprettes på alle noder i klyngen, så tæt som muligt på den kode, der bruger dem.
  • Enkelhed.
    Fokuser på at minimere boilerplate-kode og brugervenlighed.
  • Bedre ydeevne.
    Vi forsøger ikke at gentage funktionaliteten af ​​rabbitmq, men vi vælger kun det arkitektoniske og transportmæssige lag, som vi passer ind i OTP så enkelt som muligt, hvilket minimerer omkostningerne.
  • Fleksibilitet.
    Hver tjeneste kan kombinere mange udvekslingsskabeloner.
  • Elastisk design.
  • Skalerbarhed.
    Beskeder vokser med appen. Efterhånden som belastningen øges, kan du flytte udvekslingspunkterne til separate maskiner.

Kommentar. Med hensyn til kodeorganisering er metaprojekter velegnede til komplekse Erlang/Elixir-systemer. Al projektkode er i ét depot - et paraplyprojekt. Samtidig er mikrotjenester isoleret så meget som muligt og udfører simple operationer, der er ansvarlige for en separat enhed. Med denne tilgang er det nemt at vedligeholde hele systemets API, det er nemt at foretage ændringer, det er praktisk at skrive enheds- og integrationstest.

Systemkomponenter interagerer direkte eller gennem en mægler. Fra meddelelsespositionen har hver tjeneste flere livsfaser:

  • Serviceinitialisering.
    På dette trin finder konfigurationen og lanceringen af ​​processen, der udfører tjenesten og afhængigheder sted.
  • Opret et udvekslingspunkt.
    Tjenesten kan bruge et statisk udvekslingspunkt, der er angivet i værtskonfigurationen, eller oprette udvekslingspunkter dynamisk.
  • Tjenesteregistrering.
    For at tjenesten kan betjene forespørgsler, skal den være registreret på byttestedet.
  • Normal drift.
    Tjenesten udfører nyttigt arbejde.
  • Lukke ned.
    Der er 2 typer af nedlukning: regelmæssig og nødsituation. Med en almindelig service afbryder den forbindelsen fra udvekslingspunktet og stopper. I nødstilfælde udfører meddelelser et af failover-scenarierne.

Det ser ret kompliceret ud, men koden er ikke så skræmmende. Kodeeksempler med kommentarer vil blive givet i analysen af ​​skabeloner lidt senere.

Udvekslinger

Et udvekslingspunkt er en meddelelsesproces, der implementerer logikken i interaktion med komponenter i meddelelsesskabelonen. I alle eksemplerne nedenfor interagerer komponenterne gennem udvekslingspunkter, hvis kombination danner meddelelser.

Beskedudvekslingsmønstre (MEP'er)

Globalt kan udvekslingsmønstre opdeles i tosidede og ensidige. Førstnævnte indebærer et svar på den indkommende besked, sidstnævnte gør ikke. Et klassisk eksempel på et tovejsmønster i en klient-server-arkitektur er Request-response-mønsteret. Overvej skabelonen og dens ændringer.

Request-response eller RPC

RPC bruges, når vi skal have et svar fra en anden proces. Denne proces kan køre på den samme vært eller på et andet kontinent. Nedenfor er et diagram over interaktionen mellem klienten og serveren gennem meddelelser.

Byggeklodser af distribuerede applikationer. Første tilgang

Da meddelelser er fuldstændig asynkrone, er udvekslingen for klienten opdelt i 2 faser:

  1. Sender en anmodning

    messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess).

    Exchange (Udveksling) ‒ unikt byttepunktsnavn
    ResponseMatchingTag ‒ lokal etiket til behandling af svaret. For eksempel i tilfælde af at sende flere identiske anmodninger, der tilhører forskellige brugere.
    Anmodningsdefinition ‒ anmodningsorgan
    HandlerProcess ‒ PID for føreren. Denne proces vil modtage et svar fra serveren.

  2. Svarbehandling

    handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag,message = ResponsePayload}, State)

    ResponsePayload - serversvar.

For serveren består processen også af 2 faser:

  1. Initialisering af udskiftningspunkt
  2. Behandling af indkommende anmodninger

Lad os illustrere denne skabelon med kode. Lad os sige, at vi skal implementere en simpel tjeneste, der giver en enkelt nøjagtig tidsmetode.

Serverkode

Lad os flytte service-API-definitionen til api.hrl:

%% =====================================================
%%  entities
%% =====================================================
-record(time, {
  unixtime :: non_neg_integer(),
  datetime :: binary()
}).

-record(time_error, {
  code :: non_neg_integer(),
  error :: term()
}).

%% =====================================================
%%  methods
%% =====================================================
-record(time_req, {
  opts :: term()
}).
-record(time_resp, {
  result :: #time{} | #time_error{}
}).

Definer servicecontrolleren i time_controller.erl

%% В примере показан только значимый код. Вставив его в шаблон gen_server можно получить рабочий сервис.

%% инициализация gen_server
init(Args) ->
  %% подключение к точке обмена
  messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self())
  {ok, #{}}.

%% обработка события потери связи с точкой обмена. Это же событие приходит, если точка обмена еще не запустилась.
handle_info(#exchange_die{exchange = ?EXCHANGE}, State) ->
  erlang:send(self(), monitor_exchange),
  {noreply, State};

%% обработка API
handle_info(#time_req{opts = _Opts}, State) ->
  messaging:response_once(Client, #time_resp{
result = #time{ unixtime = time_utils:unixtime(now()), datetime = time_utils:iso8601_fmt(now())}
  });
  {noreply, State};

%% завершение работы gen_server
terminate(_Reason, _State) ->
  messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()),
  ok.

Kundekode

For at sende en anmodning til en tjeneste kan du kalde beskedanmodnings-API'en hvor som helst på klienten:

case messaging:request(?EXCHANGE, tag, #time_req{opts = #{}}, self()) of
    ok -> ok;
    _ -> %% repeat or fail logic
end

I et distribueret system kan konfigurationen af ​​komponenter være meget forskellig, og på tidspunktet for anmodningen starter meddelelser muligvis ikke endnu, eller servicecontrolleren vil ikke være klar til at betjene anmodningen. Derfor skal vi tjekke beskedsvaret og håndtere fejlsagen.
Efter vellykket afsendelse til klienten vil tjenesten modtage et svar eller en fejl.
Lad os håndtere begge sager i handle_info:

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time{unixtime = Utime}}}, State) ->
  ?debugVal(Utime),
  {noreply, State};

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time_error{code = ErrorCode}}}, State) ->
  ?debugVal({error, ErrorCode}),
  {noreply, State};

Request-Chunked Response

Det er bedst at undgå at sende store beskeder. Reaktionsevnen og stabil drift af hele systemet afhænger af dette. Hvis svaret på en forespørgsel fylder meget, er opdeling obligatorisk.

Byggeklodser af distribuerede applikationer. Første tilgang

Her er et par eksempler på sådanne tilfælde:

  • Komponenter udveksler binære data, såsom filer. At dele svaret op i små dele hjælper med at arbejde effektivt med filer af enhver størrelse og ikke fange hukommelsesoverløb.
  • Fortegnelser. For eksempel skal vi vælge alle poster fra en enorm tabel i databasen og sende den til en anden komponent.

Jeg kalder sådanne svar et lokomotiv. Under alle omstændigheder er 1024 1MB beskeder bedre end en enkelt 1GB besked.

I Erlang-klyngen får vi en ekstra fordel - at reducere belastningen på udvekslingspunktet og netværket, da svarene straks sendes til modtageren uden om udvekslingspunktet.

Svar med anmodning

Dette er en ret sjælden modifikation af RPC-mønsteret til opbygning af samtalesystemer.

Byggeklodser af distribuerede applikationer. Første tilgang

Udgiv-abonner (datadistributionstræ)

Hændelsesdrevne systemer leverer data til forbrugerne, så snart de er klar. Systemer er således mere tilbøjelige til push-modellen end for pull- eller poll-modellen. Denne funktion giver dig mulighed for ikke at spilde ressourcer ved konstant at anmode om og vente på data.
Figuren viser processen med at distribuere en besked til forbrugere, der abonnerer på et bestemt emne.

Byggeklodser af distribuerede applikationer. Første tilgang

Klassiske eksempler på brug af dette mønster er fordelingen af ​​staten: spilverdenen i computerspil, markedsdata om børser, nyttig information i datafeeds.

Overvej abonnentkoden:

init(_Args) ->
  %% подписываемся на обменник, ключ = key
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {ok, #{}}.

handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) ->
  %% если точка обмена недоступна, то пытаемся переподключиться
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {noreply, State};

%% обрабатываем пришедшие сообщения
handle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) ->
  ?debugVal(Msg),
  {noreply, State};

%% при остановке потребителя - отключаемся от точки обмена
terminate(_Reason, _State) ->
  messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()),
  ok.

Kilden kan kalde meddelelsens publiceringsfunktion ethvert passende sted:

messaging:publish_message(Exchange, Key, Message).

Exchange (Udveksling) - navn på byttestedet,
Nøgle ‒ rutenøgle
Besked - nyttelast

Omvendt udgiv Abonner

Byggeklodser af distribuerede applikationer. Første tilgang

Ved at implementere pub-sub kan du få et mønster, der er praktisk til logning. Sættet af kilder og forbrugere kan være helt anderledes. Figuren viser sagen med én forbruger og mange kilder.

Opgavefordelingsmønster

I næsten alle projekter er der opgaver med udskudt behandling, såsom generering af rapporter, levering af notifikationer og modtagelse af data fra tredjepartssystemer. Gennemløbet af et system, der udfører disse opgaver, skaleres nemt ved at tilføje processorer. Det eneste, der er tilbage for os, er at danne en klynge af processorer og fordele opgaverne jævnt mellem dem.

Overvej de situationer, der opstår ved at bruge eksemplet med 3 handlere. Selv på tidspunktet for opgavefordelingen opstår spørgsmålet om retfærdigheden af ​​fordelingen og overløbet af handlere. Round-robin distributionen vil være ansvarlig for retfærdighed, og for at undgå en situation med overløb af handlere vil vi indføre en begrænsning prefetch_limit. I overgangstilstande prefetch_limit vil ikke tillade én behandler at modtage alle opgaver.

Beskeder administrerer køer og behandlingsprioritet. Processorer modtager opgaver, efterhånden som de ankommer. Opgaven kan fuldføres med succes eller mislykkes:

  • messaging:ack(Tack) ‒ kaldet i tilfælde af vellykket behandling af meddelelsen
  • messaging:nack(Tack) ‒ tilkaldt i alle nødsituationer. Når opgaven vender tilbage, sender beskeder den videre til en anden behandler.

Byggeklodser af distribuerede applikationer. Første tilgang

Lad os antage, at der under behandlingen af ​​tre opgaver opstod en kompleks fejl: handler 1, efter at have modtaget opgaven, styrtede ned uden at have tid til at rapportere noget til udvekslingspunktet. I dette tilfælde vil udvekslingspunktet overføre jobbet til en anden handler, efter at ack timeout er udløbet. Handler 3 forlod af en eller anden grund opgaven og sendte et nack, som et resultat, opgaven gik også videre til en anden behandler, der fuldførte den.

Indledende resumé

Vi har nedbrudt de grundlæggende byggeklodser i distribuerede systemer og fået en grundlæggende forståelse for deres brug i Erlang/Elixir.

Ved at kombinere grundlæggende skabeloner kan komplekse paradigmer bygges til at løse nye problemer.

I den sidste del af cyklussen vil vi overveje generelle spørgsmål om organisering af tjenester, routing og balancering, og vi vil også tale om den praktiske side af skalerbarhed og fejltolerance af systemer.

Slut på anden del.

Foto Marius Christensen
Illustrationer udlånt af websequencediagrams.com

Kilde: www.habr.com

Tilføj en kommentar