
Në të kaluarën Ne shqyrtuam bazat teorike të arkitekturës reaktive. Është koha të flasim për fluxet e të dhënave, mënyrat e realizimit të sistemeve reaktive Erlang/Elixir dhe modelet e shkëmbimit të mesazheve në to:
- Request-response
- Request-Chunked Response
- Response with Request
- Publish-subscribe
- Inverted Publish-subscribe
- Shpërndarja e detyrave
SOA, MSA dhe shkëmbimi i mesazheve
SOA, MSA – arkitektura sistemore që përcaktojnë rregullat e ndërtimit të sistemeve, ndërsa messaging ofron primitive për realizimin e tyre.
Nuk dëshiroj të propagandoj një arkitekturë të caktuar të ndërtimit të sistemeve. Unë jam për aplikimin e praktikave që janë më efikase dhe të dobishme për projektin dhe biznesin specifik. Çfarëdo paradigme të zgjedhim, është më mirë të krijojmë blloqe sistemore me orientim në metodën Unix: komponente me lidhje minimale, përgjegjëse për entitete të veçanta. Metodat API kryejnë veprime sa më të thjeshta me entitetet.
Mesazhi ‒ siç e tregon emri ‒ është një broker mesazhesh. Qëllimi i tij kryesor është të pranojë dhe dërgojë mesazhe. Ai është përgjegjës për ndërfaqet e dërgimit të informacionit, formimin e kanaleve logjikë të transmetimit të informacionit brenda sistemit, ruterimin dhe balancimin, si dhe trajtimin e dështimeve në nivel sistemik.
Messaging që po zhvillohet nuk përpiqet të konkurrojë me rabbitmq ose ta zëvendësojë atë. Karakteristikat e tij kryesore janë:
- Shpërndarja.
Pikat e shkëmbimit mund të krijohen në të gjitha nyjat e klasterit, sa më afër kodit që i përdor ato. - Thjeshtësia.
Orientimi drejt minimalizimit të kodit të shablloneve dhe lehtësisë së përdorimit. - Performanca më e mirë.
Ne nuk përpiqemi të përsërisim funksionalitetin e rabbitmq, por theksojmë vetëm shtresën arkitekturore dhe transportuese, të cilën e inkorporojmë sa më thjeshtë në OTP, duke minimizuar kostot. - Fleksibiliteti.
Çdo shërbim mund të kombinonte shumë shabllone shkëmbimi. - Qëndrueshmëria, e vendosur në dizajn.
- Shkallëzueshmëria.
Messaging rritet së bashku me aplikacionin. Kur rritet ngarkesa, mund të nxjerrim pikat e shkëmbimit në makina të veçanta.
Vërejtje. Nga pikëpamja e organizimit të kodit, për sistemet e komplikuara në Erlang/Elixir, projektet meta janë mjaft të përshtatshme. E gjithë kodi i projektit ndodhet në një deposh - një projekt mbulues. Në të njëjtën kohë, mikroserviset janë maksimalisht të izoluar dhe kryejnë operacione të thjeshta, duke u përgjigjur për një entitet të veçantë. Me këtë qasje, është e lehtë të mbash API-në e sistemit të tërë, të bësh ndryshime lehtësisht dhe të shkruash teste njësie dhe integrimi me lehtësi.
KompONENTET e sistemit ndërveprojnë drejtpërdrejt ose përmes një brokeri. Nga pozita e mesazheve, çdo shërbim ka disa faza jetësore:
- Inicimi i shërbimit.
Në këtë fazë, konfigurohet dhe niset procesi ekzekutiv të shërbimit dhe varësive. - Krijimi i një pike shkëmbimi.
Shërbimi mund të përdorë një pikë shkëmbimi statike, të caktuar në konfigurimin e nodit, ose të krijojë pika shkëmbimi në mënyrë dynamike. - Regjistrimi i shërbimit.
Për t'i shërbyer kërkesave, shërbimi duhet të regjistrohet në pikën e shkëmbimit. - Funksionimi normal.
Shërbimi kryen punë të dobishme. - Mbyllja e punës.
Janë të mundshme 2 lloje të përfundimeve: normale dhe emergjente. Në rastin normal, shërbimi ndërpritet nga pika e shkëmbimit dhe ndalet. Në raste emergjente, messaging realizon një nga skenarët e trajtimit të dështimeve.
Duket mjaft e komplikuar, por në kod nuk është aq frikshëm. Shembujt e kodit me komente do të paraqiten në shqyrtimin e shablloneve pak më vonë.
Shkëmbimet
Pika e shkëmbimit është procesi messaging që implementon logjikën e bashkëpunimit me komponentët brenda shabllonit të shkëmbimit të mesazheve. Në të gjitha shembujt e dhënë më poshtë, komponentët bashkëpunojnë përmes pikave të shkëmbimit, kombinimi i të cilave formon messaging.
Modelet e shkëmbimit të mesazheve (MEPs)
Në mënyrë globale, modelet e shkëmbimit mund të ndahen në dy-kahëshe dhe një-kahëshe. Të parat nënkuptojnë një përgjigje ndaj mesazhit të marrë, përdorimi i të dytëve nuk e kërkon këtë. Një shembull klasik i modelit dy-kahësh në arkitekturën klient-server është modeli Request-response. Le të shqyrtojmë modelin dhe modifikimet e tij.
Request–response ose RPC
RPC përdoret kur na nevojitet një përgjigje nga një proces tjetër. Ky proces mund të jetë i aktivizuar në të njëjtin nyje ose ndodhet në një kontinent tjetër. Më poshtë paraqitet skema e bashkëpunimit të klientit dhe server përmes messaging.

Duke qenë që messaging është plotësisht asinkron, komunikimi për klientin ndahet në 2 faza:
Dërgimi i kërkesës
messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess).Exchange ‒ emri unik i pikës së shkëmbimit
ResponseMatchingTag ‒ etiketë lokale për trajtimin e përgjigjes. P.sh në rastin e dërgimit të disa kërkesave të njëjta, që i përkasin përdoruesve të ndryshëm.
RequestDefinition ‒ trupi i kërkesës
HandlerProcess ‒ PID i trajtuesit. Ky proces do të marrë përgjigjen nga serveri.Trajtimi i përgjigjes
handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag,message = ResponsePayload}, State)ResponsePayload ‒ përgjigja e serverit.
Për serverin, procesi gjithashtu përbëhet nga 2 faza:
- Inicializimi i pikës së shkëmbimit
- Trajtimi i kërkesave të pranuara
Le të ilustrojmë këtë model me kod. Supozoni se na nevojitet të implementojmë një shërbim të thjeshtë që ofron metodën e vetme të kohës së saktë.
Kodi i serverit
Le të nxjerrim përkufizimin e API-së së shërbimit në api.hrl:
%% =====================================================
%% entitete
%% =====================================================
-record(time, {
unixtime :: non_neg_integer(),
datetime :: binary()
}).
-record(time_error, {
code :: non_neg_integer(),
error :: term()
}).
%% =====================================================
%% metoda
%% =====================================================
-record(time_req, {
opts :: term()
}).
-record(time_resp, {
result :: #time{} | #time_error{}
}).Definojmë kontrolerin e shërbimit në time_controller.erl
%% Në shembull tregohet vetëm kodi i rëndësishëm. Duke e futur në modelin gen_server mund të marrësh një shërbim në punë.
%% inicializimi i gen_server
init(Args) ->
%% lidhja me pikën e shkëmbimit
messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self())
{ok, #{}}.
%% trajtimi i ngjarjes së humbjes së lidhjes me pikën e shkëmbimit. Kjo ngjarje vjen gjithashtu nëse pika e shkëmbimit nuk ka filluar ende.
handle_info(#exchange_die{exchange = ?EXCHANGE}, State) ->
erlang:send(self(), monitor_exchange),
{noreply, State};
%% trajtimi i 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};
%% përfundimi i punës së gen_server
terminate(_Reason, _State) ->
messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()),
ok.Kodi i klientit
Për të dërguar një kërkesë në shërbim, në çdo vend të klientit mund të thërrasësh API-n e kërkesave të mesazheve:
mesazhi i rastit: kërkesë(?EXCHANGE, tag, #time_req{opts = #{}}, vetë()) e
ok -> ok;
_ -> %% logjika e përsëritjes apo dështimit
fundNë një sistem të shpërndarë, konfigurimi i komponentëve mund të jetë shumë i ndryshëm dhe në momentin e kërkesës për mesazhimin, ai mund të mos ketë filluar akoma, ose kontrolluesi i shërbimit nuk mund të jetë i gatshëm për të shërbyer kërkesën. Prandaj, është e nevojshme të kontrollojmë përgjigjen e mesazhimit dhe të përpunojmë rastin e dështimit.
Pas dërgimit të suksesshëm, klienti do të marrë një përgjigje ose një gabim nga shërbimi.
Do të përpunojmë të dy rastet në handle_info:
handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time{unixtime = Utime}}}, Shteti) ->
?debugVal(Utime),
{noreply, Shteti};
handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time_error{code = ErrorCode}}}, Shteti) ->
?debugVal({error, ErrorCode}),
{noreply, Shteti};Request-Chunked Response
Është më mirë të mos lejoni transferimin e mesazheve të mëdha. Kjo ndikon në reagueshmërinë dhe stabilitetin e gjithë sistemit. Nëse përgjigjja në kërkesë merr shumë memorie, ndarja në pjesë është e detyrueshme.

Do të jap disa shembuj të tillë:
- Komponentët shkëmbejnë të dhëna binarë, për shembull skedarë. Ndarja e përgjigjes në pjesë të vogla ndihmon në punën efektive me skedarë të çdo madhësie dhe parandalon mbingarkesën e memories.
- Listing. Për shembull, na nevojitet të zgjedhim të gjitha rekordet nga një tabelë të madhe në bazën e të dhënave dhe t'i kalojmë një komponenti tjetër.
Unë i quaj këto përgjigje locomotiva. Sidoqoftë, 1024 mesazhe me nga 1 MB janë më mirë se një mesazh i vetëm me madhësi 1 GB.
Në klasterin Erlang, ne fitojmë një përfitim shtesë ‒ ulje të ngarkesës në pikën e exchange dhe rrjet, pasi përgjigjet dërgohen direkt te marrësi, duke anashkaluar pikën e exchange.
Response with Request
Ky është një modifikim mjaft i rrallë i modelit RPC për ndërtimin e sistemeve dialoguese.

Publish-subscribe (pemë shpërndarjeje të të dhënave)
Sistemet e orientuara nga ngjarjet i dërgojnë të dhënat konsumatorëve sipas gatishmërisë. Kështu, sistemet janë më prone ndaj modelit push sesa ndaj pull ose poll. Kjo veçori parandalon harxhuar burimet duke kërkuar vazhdimisht dhe duke pritur të dhëna.
Në figurë paraqitet procesi i shpërndarjes së një mesazhi për konsumatorët e regjistruar në një temë të caktuar.

Shembujt klasikë të përdorimit të këtij modeli janë shpërndarja e gjendjes: botës lojëra në lojërat kompjuterike, të dhënave të tregut në bursë, informacioneve të dobishme në data feeds.
Le të shqyrtojmë kodin e regjistruesit:
init(_Args) ->
%% abonehemi në treguesin, çelësi = key
messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
{ok, #{}}.
handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) ->
%% nëse pika e tregut nuk është e qasshme, provojmë të rishkëllehemi
messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
{noreply, State};
%% përpunojmë mesazhet e ardhura
handle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) ->
?debugVal(Msg),
{noreply, State};
%% kur ndalon konsumatori - shkëputemi nga pika e tregut
terminate(_Reason, _State) ->
messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()),
ok.Burimi mund të thërrasë funksionin e publikimit të mesazheve në çdo vend të përshtatshëm:
messaging:publish_message(Exchange, Key, Message).Exchange ‒ emri i treguesit,
Key ‒ çelësi i ruterit
Mesazh ‒ ngarkesa e dobishme
Inverted Publish-subscribe

Duke e rregulluar pub-sub, mund të marrim një model të përshtatshëm për regjistrim. Grupi i burimeve dhe konsumatorëve mund të jetë krejtësisht i ndryshëm. Në ilustrim paraqitet rasti me një konsumator dhe shumë burime.
Modeli i shpërndarjes së detyrave
Në pothuajse çdo projekt ndodhin detyra të përpunimit të vonuar, të tilla si formimi i raportëve, dërgimi i njoftimeve, marrja e të dhënave nga sisteme të jashtme. Kapaciteti i sistemit që ekzekuton këto detyra është lehtësisht i shkallëzueshëm duke shtuar përpunues. E gjithë çfarë na mbetet është të formojmë një grup prej përpunuesish dhe të shpërndajmë detyrat midis tyre në mënyrë të barabartë.
Le të shqyrtojmë situatat që lindin, duke përdorur shembullin e 3 përpunuesve. Që në fazën e shpërndarjes së detyrave lind pyetja e drejtësisë së shpërndarjes dhe mbingarkesës së përpunuesve. Drejtësia do të përballohet nga shpërndarja round-robin, dhe për të parandaluar situatat e mbingarkesës së përpunuesve, do të vendosim një kufizim prefetch_limit. Në modet kalimtare prefetch_limit nuk do t'i lejojë një përpunuesi të marrë të gjitha detyrat.
Messaging menaxhon radhët dhe prioritetin e përpunimit. Përpunuesit marrin detyrat sipas renditjes së tyre. Ekzekutimi i një detyre mund të përfundojë me sukses ose me dështim:
messaging:ack(Tack)‒ thirret në rastin e përpunimit të suksesshëm të mesazhitmessaging:nack(Tack)‒ thirret në të gjitha situatat e jashtzakonshme. Pas kthimit të detyrës, messaging do ta transferojë atë te një trajtues tjetër.

Supozoni se gjatë përpunimit të tre detyrave ndodhi një dështim kompleks: trajtuesi 1 ra pas marrjes së detyrës, pa arritur të njoftojë ndonjë informacion te pika e shkëmbimit. Në këtë rast, pika e shkëmbimit, pas skadimit të kohës së ack, do të kalojë detyrën te një trajtues tjetër. Trajtuesi 3, për një arsye, refuzoi detyrën dhe dërgoi nack, përfundimisht detyra kaloi te një trajtues tjetër që e realizoi me sukses.
Rezultati paraprak
Ne shqyrtuam komponentët bazë të sistemeve të shpërndara dhe fituam një kuptim të bazës së përdorimit të tyre në Erlang/Elixir.
Duke kombinuar modelet bazë, mund të ndërtosh paradigmat komplekse për të zgjidhur detyrat që lindin.
Në pjesën përfundimtare të ciklit, do të diskutojmë pyetje të zakonshme mbi organizimin e shërbimeve, routing-un dhe balancimin, si dhe do të flasim për aspektin praktik të shkallëzueshmërisë dhe qëndrueshmërisë së sistemeve.
Fundi i pjesës së dytë.
Foto
Ilustrimet janë përgatitur me ndihmën e websequencediagrams.com
Burimi: habr.com
