
V prejšnjem razpravljali smo o teoretičnih temeljih reaktivne arhitekture. Čas je, da govorimo o tokovih podatkov, načinih izvajanja reaktivnih sistemov Erlang/Elixir in vzorcih izmenjave sporočil v njih:
- Zahteva-odziv
- Zahteva-razdeljen odziv
- Odziv z zahtevo
- Objavi-naroči
- Inverzno objavi-naroči
- Porazdelitev nalog
SOA, MSA in izmenjava sporočil
SOA, MSA – sistemske arhitekture, ki določajo pravila za zgradnjo sistemov, medtem ko izmenjava sporočil zagotavlja primitivne funkcije za njihovo izvajanje.
Ne želim propagirati nobene posebej arhitekture za gradnjo sistemov. Pristojno za uporabo kar najbolj učinkovitih in koristnih praks za določen projekt in podjetje. Katero koli paradigmo izberemo, je boljše ustvariti sistemske bloke ob upoštevanju Unix-way: komponente z minimalno povezljivostjo, ki odgovarjajo za posamezne entitete. Metode API izvajajo čim bolj preproste akcije z entitetami.
Izmenjava sporočil – kot je razvidno iz imena – je posrednik sporočil. Njegov glavni cilj je sprejemati in pošiljati sporočila. Odgovoren je za vmesnike za pošiljanje informacij, oblikovanje logičnih kanalov prenosa informacij znotraj sistema, usmerjanje in uravnoteženje, ter obvladovanje odpovedi na sistemski ravni.
Razvijajoča izmenjava sporočil ne poskuša konkurirati rabbitmq ali ga zamenjati. Njene glavne značilnosti so:
- Porazdeljenost.
Točke izmenjave lahko ustvarimo na vseh vozliščih v gruči, čim bližje kodi, ki jih uporablja. - Enostavnost.
Usmerjenost na minimizacijo predpisanega kode in uporabniško prijaznost. - Najboljša zmogljivost.
Ne poskušamo ponoviti funkcionalnosti rabbitmq, temveč izpostavljamo le arhitekturno in transportno plast, ki jo čim lažje vključimo v OTP, pri čemer minimiziramo stroške. - Prilagodljivost.
Vsaka storitev lahko združuje številne vzorce izmenjave. - Odpornost na napake, vgrajena v dizajn.
- Širljivost.
Izmenjava sporočil raste skupaj z aplikacijo. Ko se obremenitev povečuje, lahko izmenjančke prenesemo na posamezne naprave.
Opomba. Z vidika organizacije koda, za kompleksne sisteme na Erlangu/Elixiru dobro odgovaraju meta-projekti. Sav kod projekta se nalazi u jednom repozitoriju – krovnom projektu. Pritom su mikroservisi maksimalno izolovani i obavljaju jednostavne operacije, odgovarajući za posebnu entitet. Ovim pristupom je lako održavati API celog sistema, jednostavno praviti izmene, a praktično je pisati jedinične i integracione testove.
Komponente sistema međusobno komuniciraju direktno ili putem brokera. Sa aspekta messaging, svaka usluga ima nekoliko faza života:
- Inicijalizacija usluge.
U ovoj fazi se vrši konfiguracija i pokretanje izvršne usluge i njenih zavisnosti. - Kreiranje tačke razmene.
Usluga može koristiti statičnu tačku razmene, zadatu u konfiguraciji čvora, ili stvoriti tačke razmene dinamički. - Registracija usluge.
Da bi usluga mogla da obrađuje zahteve, mora biti registrovana na tački razmene. - Normalno funkcionisanje.
Usluga obavlja korisni rad. - Završetak rada.
Postoje 2 vrste završetka rada: redovan i vanredan. U redovnom slučaju, usluga se isključuje iz tačke razmene i zaustavlja. U slučaju vanrednih događaja, messaging sprovodi jedan od scenarija obrade grešaka.
Izgleda prilično složeno, ali u kodu nije sve tako strašno. Primeri koda sa komentarima biće prikazani u analizi šablona malo kasnije.
Exchanges
Tačka razmene – proces messaging, koji realizuje logiku interakcije sa komponentama u okviru šablona razmene poruka. U svim primerima predstavljenim u nastavku, komponente komuniciraju putem tačaka razmene, čija kombinacija formira messaging.
Message exchange patterns (MEPs)
Globalno, šablone razmene možemo podeliti na dvosmerne i jednosmerne. Prvi podrazumevaju odgovor na primljenu poruku, dok drugi to ne rade. Klasičan primer dvosmernog šablona u klijent-server arhitekturi je Request-response šablon. Razmotrićemo šablon i njegove modifikacije.
Request–response ili RPC
RPC se koristi kada nam je potreban odgovor od druge usluge. Ova usluga može biti pokrenuta na istom čvoru ili se nalaziti na drugom kontinentu. U nastavku je prikazana shema interakcije klijenta i strežnika putem messaging.

Pošto je messaging potpuno asinkron, za klijenta se razmena deli na 2 faze:
Slanje zahteva
messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess).Exchange ‒ unikatno ime točke razmjene
ResponseMatchingTag ‒ lokalna oznaka za obradu odgovora. Na primjer, u slučaju slanja više istih zahtjeva različitim korisnicima.
RequestDefinition ‒ tijelo zahtjeva
HandlerProcess ‒ PID obraditelja. Ovaj proces će primiti odgovor od poslužitelja.Obrada odgovora
handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag, message = ResponsePayload}, State)ResponsePayload ‒ odgovor poslužitelja.
Za poslužitelj, proces se također sastoji od 2 faze:
- Inicijalizacija točke razmjene
- Obrada primljenih zahtjeva
Ilistrirajmo kodom ovu shemu. Pretpostavimo da trebamo implementirati jednostavnu uslugu koja pruža jedinu metodu tačnog vremena.
Kod poslužitelja
Izdvojit ćemo definiciju API usluge u api.hrl:
%% =====================================================
%% entiteti
%% =====================================================
-record(time, {
unixtime :: non_neg_integer(),
datetime :: binary()
}).
-record(time_error, {
code :: non_neg_integer(),
error :: term()
}).
%% =====================================================
%% metode
%% =====================================================
-record(time_req, {
opts :: term()
}).
-record(time_resp, {
result :: #time{} | #time_error{}
}).Definirajmo kontroler usluge u time_controller.erl
%% U primjeru je prikazan samo značajan kod. Umetanjem u šablon gen_server možete dobiti radnu uslugu.
%% inicijalizacija gen_server
init(Args) ->
%% povezivanje s točkom razmjene
messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self())
{ok, #{}}.
%% obrada događaja gubitka veze s točkom razmjene. Ovaj događaj dolazi i ako točka razmjene još nije pokrenuta.
handle_info(#exchange_die{exchange = ?EXCHANGE}, State) ->
erlang:send(self(), monitor_exchange),
{noreply, State};
%% obrada 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};
%% završetak rada gen_server
terminate(_Reason, _State) ->
messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()),
ok.Kod klijenta
Kako bismo poslali zahtjev usluzi, bilo gdje u klijentu možemo pozvati messaging request API:
case messaging:request(?EXCHANGE, tag, #time_req{opts = #{}}, self()) of
ok -> ok;
_ -> %% logika ponavljanja ili neuspjeha
endU distribuiranom sustavu konfiguracija komponenti može biti vrlo različita, te se u trenutku zahtjeva messaging možda još nije pokrenuo, ili kontroler usluge neće biti spreman obraditi zahtjev. Stoga moramo provjeriti odgovor messaging-a i obraditi slučaj odbijanja.
Nakon uspješnog slanja, klijent će primiti odgovor ili grešku od usluge.
Obradimo oba slučaja u 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};Zahteva-razdeljen odziv
Bolje je ne dopustiti prijenos velikih poruka. To utječe na odgovornost i stabilnost cijelog sustava. Ako odgovor na zahtjev zauzima previše memorije, tada je podjela u dijelove obavezna.

Navedite nekoliko primjera takvih slučajeva:
- Komponente razmjenjuju binarne podatke, na primjer, datoteke. Podjela odgovora u male dijelove pomaže učinkovito raditi s datotekama bilo koje veličine i izbjegavati prepunjenje memorije.
- Popisi. Na primjer, trebamo odabrati sve zapise iz ogromne tablice u bazi podataka i prenijeti ih drugoj komponenti.
Nazivam takve odgovore parnom lokomotivom. U svakom slučaju, 1024 poruka od 1 MB je bolje od jedne poruke veličine 1 GB.
U Erlang klasteru dobivamo dodatnu prednost - smanjenje opterećenja na točki razmjene i mreži, jer se odgovori odmah upućuju primatelju, zaobilazeći točku razmjene.
Odziv z zahtevo
To je prilično rijetka modifikacija RPC uzorka za izgradnju dijaloga sustava.

Objavi-pretplati (stabla distribucije podataka)
Sustavi temeljen na događajima isporučuju podatke potrošačima čim su dostupni. Na taj način, sustavi su skloniji push modelu nego pull ili poll. Ova karakteristika omogućava da se resursi ne troše uzalud stalnim zahtjevima i čekanjem podataka.
Na slici je prikazan proces distribucije poruka potrošačima koji su se prijavili na određenu temu.

Klasični primjeri korištenja ovog uzorka su distribucija stanja: svijeta igara u računalnim igrama, tržišnih podataka na burzama, korisnih informacija u podacima.
Pogledajmo kod pretplatnika:
init(_Args) ->
%% pretplaćujemo se na razmjenu, ključ = key
messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
{ok, #{}}.
handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) ->
%% ako točka razmjene nije dostupna, pokušavamo ponovo povezati
messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
{noreply, State};
%% obrađujemo dolazne poruke
handle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) ->
?debugVal(Msg),
{noreply, State};
%% prilikom zaustavljanja potrošača isključujemo se iz razmjene
terminate(_Reason, _State) ->
messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()),
ok.Vir se lahko sproži funkcijo objave sporočila kjerkoli je to primerno:
messaging:publish_message(Exchange, Key, Message).Exchange ‒ ime izmenjevalne točke,
Ključ ‒ usmerjevalni ključ
Sporočilo ‒ obremenitev
Inverzno objavi-naroči

Z uvedbo pub-sub dobimo vzorec, ki je priročen za beleženje. Skupina virov in potrošnikov je lahko popolnoma različna. Na sliki je prikazan primer s enim potrošnikom in več viri.
Vzorec porazdelitve nalog
V skoraj vsakem projektu se pojavijo naloge odložene obdelave, kot so sestavljanje poročil, dostava obvestil, pridobivanje podatkov iz zunanjih sistemov. Preko dodajanja obdelovalcev je enostavno skalirati zmogljivost sistema, ki te naloge izvaja. Edino, kar nam preostane, je, da oblikujemo grozd obdelovalcev in naloge enakomerno porazdelimo med njimi.
Poglejmo nastale situacije na primeru 3 obdelovalcev. Že v fazi porazdelitve nalog se pojavi vprašanje pravične razporeditve in preobremenitve obdelovalcev. Za pravičnost bo skrbelo porazdelitev po metodi round-robin, da se prepreči preobremenitev obdelovalcev, pa uvedimo omejitev prefetch_limit. V prehodnih režimih prefetch_limit ne bo dovolila enemu obdelovalcu, da dobi vse naloge.
Messaging upravlja z vrsticami in prioritetami obdelave. Obdelovalci prejmejo naloge, ko te pridejo. Izvedba naloge se lahko zaključi uspešno ali pa z neuspehom:
messaging:ack(Tack)‒ se kliče v primeru uspešne obdelave sporočilamessaging:nack(Tack)‒ se kliče v vseh nenadnih situacijah. Po vrnitvi naloge messaging jo bo posredoval drugemu obdelovalcu.

Domnevajmo, da se je pri obdelavi treh nalog zgodila zapletena napaka: obdelovalec 1 se je po prejemu naloge zrušil, ne da bi obveščal izmenjevalno točko. V tem primeru bo izmenjevalna točka po preteku ack timeout prenesla nalogo drugemu obdelovalcu. Obdelovalec 3 se je iz nekega razloga odpovedal nalogi in poslal nack, zato je naloga prav tako prešla drugemu obdelovalcu, ki jo je uspešno izvedel.
Predhodni povzetek
Obravnavali smo osnovne gradnike razdistributed sistemov in pridobili osnovno razumevanje njihove uporabe v Erlang/Elixir.
S kombiniranjem osnovnih vzorcev je mogoče oblikovati kompleksne paradigme za reševanje nastalih nalog.
V zaključnem delu cikla bomo obravnavali splošna vprašanja organizacije storitev, usmerjanja in uravnoteženja, ter spregovorili o praktični strani razširljivosti in odpornosti sistemov.
Konec drugega dela.
Fotografija
Ilustracije so pripravljene s pomočjo websequencediagrams.com
Izvor: habr.com
