Gradniki porazdeljenih aplikacij. Prvi pristop

Gradniki porazdeljenih aplikacij. Prvi pristop

V preteklosti članek Preučili smo teoretične osnove reaktivne arhitekture. Čas je za pogovor o podatkovnih tokovih, načinih implementacije reaktivnih sistemov Erlang/Elixir in vzorcih sporočanja v njih:

  • Zahteva-odgovor
  • Odziv, razdeljen na zahtevo
  • Odgovor z zahtevo
  • Objavi-naroči se
  • Obrnjeno Objavi-naroči se
  • Porazdelitev nalog

SOA, MSA in sporočanje

SOA, MSA sta sistemski arhitekturi, ki določata pravila za gradnjo sistemov, medtem ko sporočanje zagotavlja primitive za njihovo implementacijo.

Ne želim promovirati te ali one sistemske arhitekture. Sem za uporabo najbolj učinkovitih in uporabnih praks za določen projekt in posel. Ne glede na to, katero paradigmo izberemo, je bolje ustvariti sistemske bloke s pogledom na način Unixa: komponente z minimalno povezljivostjo, odgovorne za posamezne entitete. Metode API izvajajo najenostavnejša možna dejanja z entitetami.

Messaging je, kot že ime pove, posrednik sporočil. Njegov glavni namen je prejemanje in pošiljanje sporočil. Odgovoren je za vmesnike za pošiljanje informacij, oblikovanje logičnih kanalov za prenos informacij znotraj sistema, usmerjanje in uravnoteženje ter obravnavo napak na sistemski ravni.
Sporočila, ki jih razvijamo, ne poskušajo konkurirati ali nadomestiti rabbitmq. Njegove glavne značilnosti:

  • Distribucija.
    Točke izmenjave je mogoče ustvariti na vseh vozliščih gruče, čim bližje kodi, ki jih uporablja.
  • Preprostost.
    Osredotočite se na zmanjšanje standardne kode in enostavnost uporabe.
  • Boljše delovanje.
    Ne poskušamo ponoviti funkcionalnosti rabbitmq, ampak izpostavljamo le arhitekturno in transportno plast, ki ju čim bolj preprosto umestimo v OTP in minimiziramo stroške.
  • Prilagodljivost.
    Vsaka storitev lahko združuje številne predloge za izmenjavo.
  • Odpornost po zasnovi.
  • Razširljivost.
    Sporočila rastejo z aplikacijo. Ko se obremenitev poveča, lahko menjalne točke premaknete na posamezne stroje.

Opomba Z vidika organizacije kode so metaprojekti zelo primerni za kompleksne sisteme Erlang/Elixir. Vsa projektna koda se nahaja v enem repozitoriju – krovnem projektu. Hkrati so mikrostoritve maksimalno izolirane in izvajajo preproste operacije, ki so odgovorne za ločeno entiteto. S tem pristopom je enostavno vzdrževati API celotnega sistema, enostavno je spreminjati, priročno je pisati enotne in integracijske teste.

Komponente sistema medsebojno delujejo neposredno ali prek posrednika. Z vidika sporočanja ima vsaka storitev več življenjskih faz:

  • Inicializacija storitve.
    Na tej stopnji so proces in odvisnosti, ki izvajajo storitev, konfigurirani in zagnani.
  • Ustvarjanje menjalnega mesta.
    Storitev lahko uporablja statično točko izmenjave, določeno v konfiguraciji vozlišča, ali dinamično ustvari točke izmenjave.
  • Registracija storitve.
    Da storitev lahko služi zahtevam, mora biti registrirana na menjalnem mestu.
  • Normalno delovanje.
    Storitev proizvaja koristno delo.
  • Ugasniti.
    Možni sta 2 vrsti izklopa: običajni in zasilni. Med normalnim delovanjem je storitev prekinjena z menjalnega mesta in se ustavi. V nujnih primerih sporočanje izvede enega od samodejnih skriptov.

Videti je precej zapleteno, vendar koda ni tako strašljiva. Primeri kode s komentarji bodo podani v analizi predlog nekoliko kasneje.

Izmenjave

Točka izmenjave je proces sporočanja, ki izvaja logiko interakcije s komponentami znotraj predloge za sporočanje. V vseh spodaj predstavljenih primerih komponente medsebojno delujejo prek točk izmenjave, katerih kombinacija tvori sporočanje.

Vzorci izmenjave sporočil (EP)

Globalno lahko menjalne vzorce razdelimo na dvosmerne in enosmerne. Prvi pomenijo odgovor na dohodno sporočilo, drugi pa ne. Klasičen primer dvosmernega vzorca v arhitekturi odjemalec-strežnik je vzorec zahteva-odgovor. Oglejmo si predlogo in njene spremembe.

Zahteva-odgovor ali RPC

RPC se uporablja, ko moramo prejeti odgovor drugega procesa. Ta proces se lahko izvaja na istem vozlišču ali se nahaja na drugi celini. Spodaj je diagram interakcije med odjemalcem in strežnikom prek sporočil.

Gradniki porazdeljenih aplikacij. Prvi pristop

Ker je sporočanje popolnoma asinhrono, je za odjemalca izmenjava razdeljena na 2 fazi:

  1. Pošiljanje povpraševanja

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

    <span style="color: #f7f7f7;">Izmenjava</span> ‒ edinstveno ime menjalnega mesta
    ResponseMatchingTag ‒ lokalna oznaka za obdelavo odgovora. Na primer v primeru pošiljanja več enakih zahtevkov, ki pripadajo različnim uporabnikom.
    Definicija zahteve - telo zahteve
    HandlerProcess ‒ PID upravljalnika. Ta postopek bo prejel odgovor od strežnika.

  2. Obdelava odgovora

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

    ResponsePayload - odziv strežnika.

Tudi za strežnik je postopek sestavljen iz dveh faz:

  1. Inicializacija menjalne točke
  2. Obdelava prejetih zahtevkov

Ponazorimo to predlogo s kodo. Recimo, da moramo implementirati preprosto storitev, ki zagotavlja metodo ene same točne ure.

Koda strežnika

Definirajmo storitveni API v 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{}
}).

Definirajmo krmilnik storitve v 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.

Koda odjemalca

Če želite poslati zahtevo storitvi, lahko pokličete API za zahteve za sporočila kjer koli v odjemalcu:

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

V porazdeljenem sistemu je lahko konfiguracija komponent zelo različna in v času zahteve se sporočanje morda še ne začne ali pa servisni krmilnik ne bo pripravljen servisirati zahteve. Zato moramo preveriti odziv na sporočila in obravnavati primer napake.
Po uspešnem pošiljanju bo stranka prejela odgovor ali napako storitve.
Obravnavajmo oba primera v 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};

Odziv, razdeljen na zahtevo

Najbolje je, da se izogibate pošiljanju velikih sporočil. Od tega je odvisna odzivnost in stabilno delovanje celotnega sistema. Če odgovor na poizvedbo zavzame veliko pomnilnika, je razdelitev na dele obvezna.

Gradniki porazdeljenih aplikacij. Prvi pristop

Naj vam navedem nekaj primerov takih primerov:

  • Komponente izmenjujejo binarne podatke, kot so datoteke. Če odgovor razdelite na majhne dele, vam pomaga učinkovito delati z datotekami poljubne velikosti in se izogniti prelivom pomnilnika.
  • Oglasi. Na primer, izbrati moramo vse zapise iz ogromne tabele v bazi podatkov in jih prenesti v drugo komponento.

Tem odzivom pravim lokomotiva. V vsakem primeru je 1024 sporočil po 1 MB boljše od enega samega sporočila po 1 GB.

V gruči Erlang pridobimo še dodatno ugodnost - zmanjšamo obremenitev menjalne točke in omrežja, saj se odgovori takoj pošljejo prejemniku mimo menjalne točke.

Odgovor z zahtevo

To je precej redka sprememba vzorca RPC za gradnjo pogovornih sistemov.

Gradniki porazdeljenih aplikacij. Prvi pristop

Objavi-naroči (drevo distribucije podatkov)

Sistemi, ki jih vodijo dogodki, jih dostavijo potrošnikom takoj, ko so podatki pripravljeni. Tako so sistemi bolj nagnjeni k potisnemu modelu kot k vlečnemu ali anketnemu modelu. Ta funkcija vam omogoča, da se izognete zapravljanju virov z nenehnim zahtevanjem in čakanjem na podatke.
Slika prikazuje postopek distribucije sporočila potrošnikom, naročenim na določeno temo.

Gradniki porazdeljenih aplikacij. Prvi pristop

Klasični primeri uporabe tega vzorca so porazdelitev stanja: svet igre v računalniških igrah, tržni podatki na borzah, koristne informacije v virih podatkov.

Poglejmo naročniško kodo:

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.

Vir lahko pokliče funkcijo za objavo sporočila na katerem koli priročnem mestu:

messaging:publish_message(Exchange, Key, Message).

<span style="color: #f7f7f7;">Izmenjava</span> - naziv menjalnega mesta,
Ključne - usmerjevalni ključ
Sporočilo - tovor

Obrnjeno Objavi-naroči se

Gradniki porazdeljenih aplikacij. Prvi pristop

Z razširitvijo pub-sub lahko dobite vzorec, primeren za beleženje. Nabor virov in potrošnikov je lahko popolnoma drugačen. Slika prikazuje primer z enim porabnikom in več viri.

Vzorec porazdelitve nalog

Skoraj vsak projekt vključuje naloge odložene obdelave, kot so ustvarjanje poročil, dostava obvestil in pridobivanje podatkov iz sistemov tretjih oseb. Prepustnost sistema, ki izvaja te naloge, je mogoče preprosto povečati z dodajanjem upravljavcev. Preostane nam le, da oblikujemo gručo procesorjev in mednje enakomerno porazdelimo naloge.

Oglejmo si situacije, ki se pojavijo na primeru 3 vodnikov. Že v fazi razdelitve nalog se pojavi vprašanje pravičnosti razdelitve in prelivanja obdelovalcev. Za pravičnost bo skrbela krožna distribucija, v izogib situaciji prenapolnjenosti obdelovalcev pa bomo uvedli omejitev prefetch_limit. V prehodnih razmerah prefetch_limit prepreči, da bi en vodja prejel vse naloge.

Messaging upravlja čakalne vrste in prioriteto obdelave. Procesorji prejmejo naloge, ko prispejo. Naloga se lahko zaključi uspešno ali neuspešno:

  • messaging:ack(Tack) - kliče, če je sporočilo uspešno obdelano
  • messaging:nack(Tack) - klic v vseh nujnih primerih. Ko je opravilo vrnjeno, ga bo sporočilo posredovalo drugemu upravljavcu.

Gradniki porazdeljenih aplikacij. Prvi pristop

Recimo, da je med obdelavo treh nalog prišlo do zapletene napake: procesor 1 se je po prejemu naloge zrušil, ne da bi imel čas, da karkoli sporoči točki izmenjave. V tem primeru bo točka izmenjave opravilo prenesla na drugega upravljavca po preteku časovne omejitve potrditve. Iz nekega razloga je upravljavec 3 opustil nalogo in poslal nack; posledično je bila naloga tudi prenesena na drugega upravljavca, ki jo je uspešno opravil.

Predhodni povzetek

Pokrili smo osnovne gradnike porazdeljenih sistemov in pridobili osnovno razumevanje njihove uporabe v Erlang/Elixir.

S kombiniranjem osnovnih vzorcev lahko zgradite kompleksne paradigme za reševanje nastajajočih problemov.

V zadnjem delu serije si bomo ogledali splošna vprašanja organiziranja storitev, usmerjanja in uravnoteženja, govorili pa bomo tudi o praktični strani razširljivosti in odpornosti sistemov na napake.

Konec drugega dela.

Photo Shoot Marius Christensen
Ilustracije, pripravljene z uporabo websequencediagrams.com

Vir: www.habr.com

Dodaj komentar