Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Në të fundit artikull Ne shqyrtuam bazat teorike të arkitekturës reaktive. Është koha për të folur për rrjedhat e të dhënave, mënyrat për të zbatuar sistemet reaktive Erlang/Elixir dhe modelet e mesazheve në to:

  • Kërkesë-përgjigje
  • Përgjigje e copëtuar nga kërkesa
  • Përgjigje me Kërkesë
  • Publiko-subscribe
  • I përmbysur Publikimi-pajtohu
  • Shpërndarja e detyrave

SOA, MSA dhe Mesazhimi

SOA, MSA janë arkitektura të sistemit që përcaktojnë rregullat për ndërtimin e sistemeve, ndërsa mesazhet ofrojnë primitive për zbatimin e tyre.

Unë nuk dua të promovoj këtë apo atë arkitekturë të sistemit. Unë jam për përdorimin e praktikave më efektive dhe më të dobishme për një projekt dhe biznes specifik. Cilado qoftë paradigma që zgjedhim, është më mirë të krijojmë blloqe të sistemit me një sy në rrugën Unix: komponentë me lidhje minimale, përgjegjës për entitete individuale. Metodat API kryejnë veprimet më të thjeshta të mundshme me entitetet.

Mesazhimi është, siç sugjeron emri, një ndërmjetës mesazhesh. Qëllimi i tij kryesor është të marrë dhe të dërgojë mesazhe. Ai është përgjegjës për ndërfaqet për dërgimin e informacionit, formimin e kanaleve logjike për transmetimin e informacionit brenda sistemit, drejtimin dhe balancimin, si dhe trajtimin e gabimeve në nivel sistemi.
Mesazhet që ne po zhvillojmë nuk po përpiqen të konkurrojnë ose të zëvendësojnë rabbitmq. Karakteristikat e tij kryesore:

  • Shpërndarja.
    Pikat e shkëmbimit mund të krijohen në të gjitha nyjet e grupimit, sa më afër kodit që i përdor ato.
  • Thjeshtësi.
    Përqendrohuni në minimizimin e kodit të pllakës së bojlerit dhe lehtësinë e përdorimit.
  • Performancë më e mirë.
    Ne nuk po përpiqemi të përsërisim funksionalitetin e rabbitmq, por të nxjerrim në pah vetëm shtresën arkitekturore dhe transportuese, të cilën e vendosim në OTP sa më thjesht, duke minimizuar kostot.
  • Fleksibiliteti.
    Çdo shërbim mund të kombinojë shumë shabllone shkëmbimi.
  • Rezistenca sipas dizajnit.
  • Shkallëzueshmëria.
    Mesazhet rriten me aplikacionin. Ndërsa ngarkesa rritet, ju mund të zhvendosni pikat e shkëmbimit në makina individuale.

Komento Për sa i përket organizimit të kodit, meta-projektet janë të përshtatshme për sistemet komplekse Erlang/Elixir. I gjithë kodi i projektit ndodhet në një depo - një projekt ombrellë. Në të njëjtën kohë, mikroshërbimet janë të izoluara maksimalisht dhe kryejnë operacione të thjeshta që janë përgjegjëse për një entitet të veçantë. Me këtë qasje, është e lehtë të ruash API-në e të gjithë sistemit, është e lehtë të bësh ndryshime, është e përshtatshme të shkruash teste të njësisë dhe integrimit.

Komponentët e sistemit ndërveprojnë drejtpërdrejt ose përmes një ndërmjetësi. Nga këndvështrimi i mesazheve, çdo shërbim ka disa faza të jetës:

  • Inicializimi i shërbimit.
    Në këtë fazë, procesi i ekzekutimit të shërbimit dhe varësitë e tij konfigurohen dhe nisen.
  • Krijimi i një pike shkëmbimi.
    Shërbimi mund të përdorë një pikë shkëmbimi statike të specifikuar në konfigurimin e nyjeve, ose të krijojë pika shkëmbimi në mënyrë dinamike.
  • Regjistrimi i shërbimit.
    Në mënyrë që shërbimi të shërbejë kërkesat, duhet të regjistrohet në pikën e këmbimit.
  • Funksionim normal.
    Shërbimi prodhon punë të dobishme.
  • Fike.
    Ekzistojnë 2 lloje të fikjes së mundshme: normale dhe emergjente. Gjatë funksionimit normal, shërbimi shkëputet nga pika e shkëmbimit dhe ndalon. Në situata emergjente, mesazhet ekzekutojnë një nga skriptet e dështimit.

Duket mjaft e ndërlikuar, por kodi nuk është edhe aq i frikshëm. Shembujt e kodit me komente do të jepen në analizën e shablloneve pak më vonë.

Shkëmbimeve

Pika e shkëmbimit është një proces mesazhesh që zbaton logjikën e ndërveprimit me komponentët brenda shabllonit të mesazheve. Në të gjithë shembujt e paraqitur më poshtë, komponentët ndërveprojnë përmes pikave të shkëmbimit, kombinimi i të cilave formon mesazhe.

Modelet e shkëmbimit të mesazheve (MEP)

Në nivel global, modelet e shkëmbimit mund të ndahen në dy drejtime dhe njëkahëshe. Të parët nënkuptojnë një përgjigje ndaj një mesazhi në hyrje, të dytat jo. Një shembull klasik i një modeli të dyanshëm në arkitekturën klient-server është modeli i përgjigjes së kërkesës. Le të shohim shabllonin dhe modifikimet e tij.

Kërkesë-përgjigje ose RPC

RPC përdoret kur duhet të marrim një përgjigje nga një proces tjetër. Ky proces mund të funksionojë në të njëjtën nyje ose të gjendet në një kontinent tjetër. Më poshtë është një diagram i ndërveprimit midis klientit dhe serverit përmes mesazheve.

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Meqenëse mesazhet janë plotësisht asinkrone, për klientin shkëmbimi ndahet në 2 faza:

  1. Dërgimi i një kërkese

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

    shkëmbim ‒ emri unik i pikës së shkëmbimit
    ResponseMatchingTag ‒ etiketë lokale për përpunimin e përgjigjes. Për shembull, në rastin e dërgimit të disa kërkesave identike që u përkasin përdoruesve të ndryshëm.
    Përkufizimi i kërkesës - organi i kërkesës
    Procesi i Trajtimit ‒ PID e mbajtësit. Ky proces do të marrë një përgjigje nga serveri.

  2. Përpunimi 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:

  1. Inicializimi i pikës së shkëmbimit
  2. Përpunimi i kërkesave të pranuara

Le ta ilustrojmë këtë shabllon me kod. Le të themi se duhet të zbatojmë një shërbim të thjeshtë që ofron një metodë të vetme kohore të saktë.

Kodi i serverit

Le të përcaktojmë API-në e shërbimit në 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{}
}).

Le të përcaktojmë kontrolluesin e shërbimit në 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.

Kodi i klientit

Për të dërguar një kërkesë në shërbim, mund të telefononi API-në e kërkesës për mesazhe kudo në klient:

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

Në një sistem të shpërndarë, konfigurimi i komponentëve mund të jetë shumë i ndryshëm dhe në momentin e kërkesës, mesazhet mund të mos fillojnë ende, ose kontrolluesi i shërbimit nuk do të jetë gati për t'i shërbyer kërkesës. Prandaj, duhet të kontrollojmë përgjigjen e mesazheve dhe të trajtojmë rastin e dështimit.
Pas dërgimit të suksesshëm, klienti do të marrë një përgjigje ose gabim nga shërbimi.
Le t'i trajtojmë të dyja rastet në 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};

Përgjigje e copëtuar nga kërkesa

Është më mirë të shmangni dërgimin e mesazheve të mëdha. Përgjegjshmëria dhe funksionimi i qëndrueshëm i të gjithë sistemit varet nga kjo. Nëse përgjigja ndaj një pyetjeje merr shumë memorie, atëherë ndarja e saj në pjesë është e detyrueshme.

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Më lejoni t'ju jap disa shembuj të rasteve të tilla:

  • Komponentët shkëmbejnë të dhëna binare, siç janë skedarët. Ndarja e përgjigjes në pjesë të vogla ju ndihmon të punoni me efikasitet me skedarë të çdo madhësie dhe të shmangni tejmbushjet e memories.
  • Listimet. Për shembull, ne duhet të zgjedhim të gjitha regjistrimet nga një tabelë e madhe në bazën e të dhënave dhe t'i transferojmë ato në një komponent tjetër.

Unë i quaj këto përgjigje lokomotivë. Në çdo rast, 1024 mesazhe prej 1 MB janë më të mira se një mesazh i vetëm prej 1 GB.

Në grupin Erlang, marrim një përfitim shtesë - zvogëlojmë ngarkesën në pikën e shkëmbimit dhe rrjetin, pasi përgjigjet i dërgohen menjëherë marrësit, duke anashkaluar pikën e shkëmbimit.

Përgjigje me Kërkesë

Ky është një modifikim mjaft i rrallë i modelit RPC për ndërtimin e sistemeve të dialogut.

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Publiko-subscribe (pema e shpërndarjes së të dhënave)

Sistemet e drejtuara nga ngjarjet ua dorëzojnë ato konsumatorëve sapo të dhënat të jenë gati. Kështu, sistemet janë më të prirur ndaj një modeli shtytës sesa ndaj një modeli tërheqjeje ose sondazhi. Kjo veçori ju lejon të shmangni humbjen e burimeve duke kërkuar dhe pritur vazhdimisht të dhëna.
Figura tregon procesin e shpërndarjes së një mesazhi për konsumatorët e abonuar në një temë specifike.

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Shembuj klasikë të përdorimit të këtij modeli janë shpërndarja e gjendjes: bota e lojës në lojërat kompjuterike, të dhënat e tregut për shkëmbimet, informacioni i dobishëm në burimet e të dhënave.

Le të shohim kodin e pajtimtarit:

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.

Burimi mund të thërrasë funksionin për të publikuar një mesazh në çdo vend të përshtatshëm:

messaging:publish_message(Exchange, Key, Message).

shkëmbim - emri i pikës së shkëmbimit,
Kyç - çelësi i rrugëzimit
mesazh - ngarkesë

I përmbysur Publikimi-pajtohu

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Duke zgjeruar pub-sub, mund të merrni një model të përshtatshëm për prerje. Grupi i burimeve dhe konsumatorëve mund të jetë krejtësisht i ndryshëm. Figura tregon një rast me një konsumator dhe burime të shumta.

Modeli i shpërndarjes së detyrave

Pothuajse çdo projekt përfshin detyra të shtyra të përpunimit, të tilla si gjenerimi i raporteve, dërgimi i njoftimeve dhe marrja e të dhënave nga sistemet e palëve të treta. Rrjedha e sistemit që kryen këto detyra mund të shkallëzohet lehtësisht duke shtuar mbajtës. Gjithçka që na mbetet është të formojmë një grup procesorësh dhe të shpërndajmë në mënyrë të barabartë detyrat midis tyre.

Le të shohim situatat që lindin duke përdorur shembullin e 3 mbajtësve. Edhe në fazën e shpërndarjes së detyrave, lind çështja e drejtësisë së shpërndarjes dhe tejmbushjes së trajtuesve. Shpërndarja e rrumbullakët do të jetë përgjegjëse për drejtësinë dhe për të shmangur një situatë të tejmbushjes së mbajtësve, ne do të vendosim një kufizim kufi_prefetch. Në kushte kalimtare kufi_prefetch do të parandalojë që një mbajtës të marrë të gjitha detyrat.

Mesazhimi menaxhon radhët dhe prioritetin e përpunimit. Përpunuesit marrin detyra kur arrijnë. Detyra mund të përfundojë me sukses ose të dështojë:

  • messaging:ack(Tack) - thirret nëse mesazhi përpunohet me sukses
  • messaging:nack(Tack) - thirret në të gjitha situatat emergjente. Pasi të kthehet detyra, mesazhet do t'ia kalojnë atë një mbajtësi tjetër.

Blloqet e ndërtimit të aplikacioneve të shpërndara. Qasja e parë

Supozoni se një dështim kompleks ndodhi gjatë përpunimit të tre detyrave: procesori 1, pasi mori detyrën, u rrëzua pa pasur kohë për të raportuar asgjë në pikën e shkëmbimit. Në këtë rast, pika e shkëmbimit do ta transferojë detyrën te një mbajtës tjetër pasi të ketë skaduar afati i pranimit. Për disa arsye, trajtuesi 3 e braktisi detyrën dhe dërgoi nack; si rezultat, detyra u transferua edhe te një mbajtës tjetër që e përfundoi me sukses.

Përmbledhje paraprake

Ne kemi mbuluar blloqet bazë të ndërtimit të sistemeve të shpërndara dhe kemi fituar një kuptim bazë të përdorimit të tyre në Erlang/Elixir.

Duke kombinuar modelet bazë, ju mund të ndërtoni paradigma komplekse për të zgjidhur problemet e shfaqura.

Në pjesën e fundit të serisë, ne do të shikojmë çështjet e përgjithshme të organizimit të shërbimeve, rrugëzimit dhe balancimit, dhe gjithashtu do të flasim për anën praktike të shkallëzueshmërisë dhe tolerancës së gabimeve të sistemeve.

Fundi i pjesës së dytë.

Photo Shoot Marius Christensen
Ilustrime të përgatitura duke përdorur websequencediagrams.com

Burimi: www.habr.com

Shto një koment