Boustene van verspreide toepassings. Eerste benadering

Boustene van verspreide toepassings. Eerste benadering

In die laaste Artikel ons het die teoretiese grondslae van reaktiewe argitektuur ontleed. Dit is tyd om te praat oor datavloei, maniere om reaktiewe Erlang/Elixir-stelsels te implementeer, en boodskappatrone daarin:

  • Versoek-reaksie
  • Request-Chunked Response
  • Reageer met versoek
  • Publiseer-teken in
  • Omgekeerde Publiseer Teken in
  • Taakverdeling

SOA, MSA en boodskappe

SOA, MSA is stelselargitekture wat die reΓ«ls vir die bou van stelsels definieer, terwyl boodskappe primitiewe verskaf vir die implementering daarvan.

Ek wil nie hierdie of daardie stelselargitektuur propageer nie. Ek is vir die toepassing van die mees effektiewe en bruikbare praktyke vir 'n spesifieke projek en besigheid. Watter paradigma ons ook al kies, dit is beter om stelselblokke te skep met die oog op die Unix-manier: komponente met minimale konnektiwiteit, verantwoordelik vir individuele entiteite. API-metodes voer die eenvoudigste aksies uit met entiteite.

Boodskappe - soos die naam aandui - 'n boodskapmakelaar. Die hoofdoel daarvan is om boodskappe te ontvang en te stuur. Dit is verantwoordelik vir die koppelvlakke vir die stuur van inligting, die vorming van logiese kanale vir die oordrag van inligting binne die stelsel, roetering en balansering, sowel as fouthantering op stelselvlak.
Die ontwikkelde boodskappe probeer nie om rabbitmq mee te ding of te vervang nie. Sy hoofkenmerke:

  • Verspreiding.
    Uitruilpunte kan op alle nodusse van die groep geskep word, so na as moontlik aan die kode wat hulle gebruik.
  • Eenvoud.
    Fokus op die vermindering van boilerplate-kode en gebruiksgemak.
  • Beter prestasie.
    Ons probeer nie om die funksionaliteit van rabbitmq te herhaal nie, maar ons kies slegs die argitektoniese en vervoerlaag, wat ons so eenvoudig as moontlik in OTP inpas, wat koste tot die minimum beperk.
  • Buigsaamheid.
    Elke diens kan baie uitruilsjablone kombineer.
  • Veerkragtigheid deur ontwerp.
  • Skaalbaarheid.
    Boodskappe groei saam met die toepassing. Soos die vrag toeneem, kan jy die uitruilpunte na aparte masjiene skuif.

Let daarop. Wat kode-organisasie betref, is metaprojekte goed geskik vir komplekse Erlang/Elixir-stelsels. Alle projekkode is in een bewaarplek - 'n sambreelprojek. Terselfdertyd word mikrodienste soveel as moontlik geΓ―soleer en voer eenvoudige bewerkings uit wat verantwoordelik is vir 'n aparte entiteit. Met hierdie benadering is dit maklik om die API van die hele stelsel in stand te hou, dit is maklik om veranderinge aan te bring, dit is gerieflik om eenheid- en integrasietoetse te skryf.

Stelselkomponente interaksie direk of deur 'n makelaar. Vanuit die posisie van boodskappe het elke diens verskeie lewensfases:

  • Diensinisialisering.
    Op hierdie stadium vind die konfigurasie en bekendstelling van die proses wat die diens en afhanklikhede uitvoer plaas.
  • Skep 'n uitruilpunt.
    Die diens kan 'n statiese uitruilpunt gebruik wat in die gasheerkonfigurasie gespesifiseer is, of uitruilpunte dinamies skep.
  • Diensregistrasie.
    Ten einde die diens versoeke te bedien, moet dit op die ruilpunt geregistreer wees.
  • Normale werking.
    Die diens doen nuttige werk.
  • Voltooiing van werk.
    Daar is 2 tipes afsluiting: gereelde en noodgevalle. Met 'n gereelde diens ontkoppel dit van die ruilpunt en stop. In noodgevalle voer boodskappe een van die failover-scenario's uit.

Dit lyk nogal ingewikkeld, maar die kode is nie so skrikwekkend nie. Kodevoorbeelde met kommentaar sal 'n bietjie later in die ontleding van sjablone gegee word.

Vervang

'n Uitruilpunt is 'n boodskapproses wat die logika van interaksie met komponente binne die boodskapsjabloon implementeer. In al die voorbeelde hieronder werk die komponente in wisselwerking deur middel van uitruilpunte, waarvan die kombinasie boodskappe vorm.

Boodskapuitruilpatrone (LEP's)

WΓͺreldwyd kan uitruilpatrone in tweesydig en eensydig verdeel word. Eersgenoemde impliseer 'n reaksie op die inkomende boodskap, laasgenoemde nie. 'n Klassieke voorbeeld van 'n tweerigtingpatroon in 'n kliΓ«nt-bediener-argitektuur is die Request-response-patroon. Oorweeg die sjabloon en sy wysigings.

Versoek-reaksie of RPC

RPC word gebruik wanneer ons 'n reaksie van 'n ander proses moet kry. Hierdie proses kan op dieselfde gasheer of op 'n ander kontinent loop. Hieronder is 'n diagram van die interaksie tussen die kliΓ«nt en die bediener deur middel van boodskappe.

Boustene van verspreide toepassings. Eerste benadering

Aangesien boodskappe heeltemal asynchronies is, word die uitruil vir die kliΓ«nt in 2 fases verdeel:

  1. stuur tans versoek

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

    Ruil β€’ unieke ruilpuntnaam
    ResponseMatchingTag β€’ plaaslike etiket vir die verwerking van die antwoord. Byvoorbeeld, in die geval van die stuur van verskeie identiese versoeke wat aan verskillende gebruikers behoort.
    Versoek Definisie β€’ versoek liggaam
    Hanteerderproses β€’ PID van die hanteerder. Hierdie proses sal 'n antwoord van die bediener ontvang.

  2. Reaksieverwerking

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

    ResponsePayload - bediener reaksie.

Vir die bediener bestaan ​​die proses ook uit 2 fases:

  1. Ruilpuntinisialisering
  2. Verwerking van inkomende versoeke

Kom ons illustreer hierdie sjabloon met kode. Kom ons sΓͺ dat ons 'n eenvoudige diens moet implementeer wat 'n enkele presiese tydmetode bied.

Bediener kode

Kom ons skuif die diens-API-definisie na 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{}
}).

Definieer die diensbeheerder in 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.

KliΓ«nt kode

Om 'n versoek na 'n diens te stuur, kan jy die boodskapversoek-API enige plek op die kliΓ«nt bel:

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

In 'n verspreide stelsel kan die konfigurasie van die komponente baie verskil, en ten tyde van die versoek kan boodskappe nog nie begin nie, of die diensbeheerder sal nie gereed wees om die versoek te bedien nie. Daarom moet ons die boodskapreaksie nagaan en die mislukkingsgeval hanteer.
Na suksesvolle versending aan die kliΓ«nt, sal die diens 'n reaksie of 'n fout ontvang.
Kom ons hanteer beide gevalle in 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

Dit is die beste om nie groot boodskappe te stuur nie. Die responsiwiteit en stabiele werking van die hele stelsel hang hiervan af. As die reaksie op 'n navraag baie geheue in beslag neem, is splitsing verpligtend.

Boustene van verspreide toepassings. Eerste benadering

Hier is 'n paar voorbeelde van sulke gevalle:

  • Komponente ruil binΓͺre data uit, soos lΓͺers. Deur die antwoord in klein dele op te breek, help dit om doeltreffend met lΓͺers van enige grootte te werk en nie geheue oorvloei te vang nie.
  • Inskrywings. Byvoorbeeld, ons moet alle rekords uit 'n groot tabel in die databasis kies en dit na 'n ander komponent oordra.

Ek noem sulke reaksies 'n lokomotief. In elk geval, 1024 1MB-boodskappe is beter as 'n enkele 1GB-boodskap.

In die Erlang-kluster kry ons 'n bykomende voordeel - die vermindering van die las op die uitruilpunt en die netwerk, aangesien die antwoorde onmiddellik na die ontvanger gestuur word, wat die uitruilpunt omseil.

Reageer met versoek

Dit is 'n taamlik seldsame wysiging van die RPC-patroon vir die bou van gesprekstelsels.

Boustene van verspreide toepassings. Eerste benadering

Publiseer-teken in (dataverspreidingboom)

Gebeurtenisgedrewe stelsels lewer data aan verbruikers sodra dit gereed is. Stelsels is dus meer geneig tot die stootmodel as vir die trek- of peilingsmodel. Hierdie kenmerk laat jou toe om nie hulpbronne te mors deur voortdurend data aan te vra en daarvoor te wag nie.
Die figuur toon die proses van verspreiding van 'n boodskap aan verbruikers wat op 'n spesifieke onderwerp ingeteken is.

Boustene van verspreide toepassings. Eerste benadering

Klassieke voorbeelde van die gebruik van hierdie patroon is die verspreiding van staat: die spelwΓͺreld in rekenaarspeletjies, markdata oor uitruilings, nuttige inligting in datavoere.

Oorweeg die intekenaarkode:

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.

Die bron kan die publiseerfunksie van die boodskap op enige gerieflike plek noem:

messaging:publish_message(Exchange, Key, Message).

Ruil - naam van die uitruilpunt,
Sleutel β€’ roetesleutel
Boodskap - loonvrag

Omgekeerde Publiseer Teken in

Boustene van verspreide toepassings. Eerste benadering

Deur pub-sub te ontplooi, kan u 'n patroon kry wat gerieflik is om aan te meld. Die stel bronne en verbruikers kan heeltemal anders wees. Die figuur toon 'n geval met een verbruiker en baie bronne.

Taakverspreidingspatroon

In byna elke projek is daar take van uitgestelde verwerking, soos om verslae te genereer, kennisgewings te lewer en data van derdepartystelsels te ontvang. Die deurset van 'n stelsel wat hierdie take verrig, word maklik afgeskaal deur verwerkers by te voeg. Al wat vir ons oorbly, is om 'n groep verwerkers te vorm en take eweredig tussen hulle te verdeel.

Oorweeg die situasies wat ontstaan ​​deur die voorbeeld van 3 hanteerders te gebruik. Selfs in die stadium van verspreiding van take, ontstaan ​​die vraag na die regverdigheid van verspreiding en oorloop van hanteerders. Die rondomtalie-verspreiding sal verantwoordelik wees vir regverdigheid, en om 'n situasie van oorloop van hanteerders te vermy, sal ons 'n beperking instel voorafhaal_limiet. In oorgangsmodusse voorafhaal_limiet sal nie toelaat dat een hanteerder alle take ontvang nie.

Boodskappe bestuur rye en verwerkingsprioriteit. Verwerkers ontvang take soos hulle aankom. Die taak kan suksesvol voltooi of misluk:

  • messaging:ack(Tack) β€’ gebel in geval van suksesvolle verwerking van die boodskap
  • messaging:nack(Tack) β€’ in alle noodsituasies ingeroep. Nadat die taak teruggekeer het, sal boodskappe dit aan 'n ander hanteerder oorgee.

Boustene van verspreide toepassings. Eerste benadering

Kom ons neem aan dat tydens die verwerking van drie take, 'n komplekse mislukking plaasgevind het: hanteerder 1, nadat hy die taak ontvang het, het neergestort sonder om tyd te hΓͺ om iets by die uitruilpunt aan te meld. In hierdie geval sal die ruilpunt die taak na 'n ander hanteerder oordra nadat die ack timeout verstryk het. Hanteerder 3 het om een ​​of ander rede die taak laat vaar en 'n nak gestuur, gevolglik het die taak ook na 'n ander hanteerder oorgedra wat dit suksesvol voltooi het.

Voorlopige opsomming

Ons het die basiese boublokke van verspreide stelsels afgebreek en 'n basiese begrip gekry van die gebruik daarvan in Erlang/Elixir.

Deur basiese sjablone te kombineer, kan komplekse paradigmas gebou word om opkomende probleme op te los.

In die laaste deel van die reeks sal ons algemene kwessies oor die organisering van dienste, roetering en balansering oorweeg, en ook praat oor die praktiese sy van skaalbaarheid en fouttoleransie van stelsels.

Einde van die tweede deel.

Π€ΠΎΡ‚ΠΎ Marius Christensen
Illustrasies met vergunning van websequencediagrams.com

Bron: will.com

Voeg 'n opmerking