Az elosztott alkalmazások építőkövei. Első megközelítés

Az elosztott alkalmazások építőkövei. Első megközelítés

Az utolsóban cikk Megvizsgáltuk a reaktív építészet elméleti alapjait. Itt az ideje, hogy beszéljünk az adatfolyamokról, a reaktív Erlang/Elixir rendszerek megvalósításának módjairól és az üzenetküldési mintákról:

  • Kérés-válasz
  • Kérésre darabolt válasz
  • Válasz kéréssel
  • Közzététel-előfizetés
  • Fordított Publish-subscribe
  • Feladat elosztás

SOA, MSA és üzenetkezelés

A SOA, MSA olyan rendszerarchitektúrák, amelyek meghatározzák az épületrendszerekre vonatkozó szabályokat, míg az üzenetkezelés primitíveket biztosít ezek megvalósításához.

Nem ezt vagy azt a rendszerarchitektúrát akarom népszerűsíteni. Azért vagyok, hogy a leghatékonyabb és leghasznosabb gyakorlatokat alkalmazzam egy adott projekthez és vállalkozáshoz. Bármelyik paradigmát is választjuk, jobb, ha rendszerblokkokat készítünk a Unix-út szem előtt tartásával: minimális kapcsolattal rendelkező, az egyes entitásokért felelős összetevőket. Az API-metódusok a lehető legegyszerűbb műveleteket hajtják végre az entitásokkal.

Az üzenetküldés, ahogy a neve is sugallja, egy üzenetközvetítő. Fő célja az üzenetek fogadása és küldése. Feladata az információküldő felületek, a rendszeren belüli információtovábbítás logikai csatornáinak kialakítása, az útválasztás és a kiegyenlítés, valamint a rendszerszintű hibakezelés.
Az általunk kifejlesztett üzenetküldés nem próbál versenyezni a rabbitmq-val, vagy azt lecserélni. Fő jellemzői:

  • Terjesztés.
    A cserepontok minden fürtcsomóponton létrehozhatók, a lehető legközelebb az azokat használó kódhoz.
  • Egyszerűség.
    Összpontosítson az alapkód minimalizálására és a könnyű használatra.
  • Jobb teljesítmény.
    Nem próbáljuk megismételni a rabbitmq funkcionalitását, hanem csak az építészeti és közlekedési réteget emeljük ki, amit a költségek minimalizálásával a lehető legegyszerűbben illesztünk az OTP-be.
  • Rugalmasság.
    Mindegyik szolgáltatás számos cseresablont kombinálhat.
  • Rugalmasság a tervezés által.
  • Méretezhetőség.
    Az üzenetküldés az alkalmazással bővül. A terhelés növekedésével a cserepontokat áthelyezheti az egyes gépekre.

Megjegyzés. A kódszervezés szempontjából a meta-projektek jól illeszkednek az összetett Erlang/Elixir rendszerekhez. Az összes projektkód egyetlen tárolóban található - egy ernyőprojektben. Ugyanakkor a mikroszolgáltatások maximálisan elszigeteltek, és egyszerű műveleteket hajtanak végre, amelyek egy külön entitásért felelősek. Ezzel a megközelítéssel könnyen karbantartható a teljes rendszer API-ja, egyszerű a változtatások elvégzése, kényelmes egység- és integrációs teszteket írni.

A rendszerelemek közvetlenül vagy közvetítőn keresztül lépnek kapcsolatba egymással. Üzenetkezelési szempontból minden szolgáltatásnak több életszakasza van:

  • Szolgáltatás inicializálása.
    Ebben a szakaszban a szolgáltatást végrehajtó folyamat és függőségek konfigurálva és elindulva.
  • Cserepont létrehozása.
    A szolgáltatás használhat a csomópont konfigurációjában megadott statikus cserepontokat, vagy dinamikusan hozhat létre cserepontokat.
  • Szolgáltatás regisztráció.
    Ahhoz, hogy a szolgáltatás kiszolgálja a kéréseket, regisztrálnia kell a csereponton.
  • Normális működés.
    A szolgáltatás hasznos munkát végez.
  • Leállitás.
    Kétféle leállítás lehetséges: normál és vészleállítás. Normál működés közben a szolgáltatás lekapcsol a cserepontról és leáll. Vészhelyzetekben az üzenetküldés végrehajtja az egyik feladatátvételi parancsfájlt.

Elég bonyolultnak tűnik, de a kód nem annyira ijesztő. A megjegyzésekkel ellátott kódpéldákat a sablonok elemzésekor adjuk meg kicsit később.

Feltételek

Az Exchange pont egy üzenetküldési folyamat, amely megvalósítja az üzenetküldési sablonon belüli összetevőkkel való interakció logikáját. Az alábbiakban bemutatott összes példában az összetevők cserepontokon keresztül lépnek kapcsolatba egymással, amelyek kombinációja üzenetküldést eredményez.

Üzenetváltási minták (EP-ek)

Globálisan a csereminták kétirányúra és egyirányúra oszthatók. Az előbbi egy bejövő üzenetre adott választ jelent, az utóbbi nem. A kliens-szerver architektúra kétirányú mintájának klasszikus példája a Request-response minta. Nézzük meg a sablont és annak módosításait.

Kérelem-válasz vagy RPC

Az RPC-t akkor használjuk, ha választ kell kapnunk egy másik folyamattól. Ez a folyamat futhat ugyanazon a csomóponton vagy egy másik kontinensen. Az alábbiakban a kliens és a szerver közötti kölcsönhatás diagramja látható üzenetküldés útján.

Az elosztott alkalmazások építőkövei. Első megközelítés

Mivel az üzenetküldés teljesen aszinkron, az ügyfél számára a csere 2 fázisra oszlik:

  1. kérés küldése

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

    csere ‒ a cserepont egyedi neve
    ResponseMatchingTag ‒ helyi címke a válasz feldolgozásához. Például több azonos kérés küldése esetén, amelyek különböző felhasználókhoz tartoznak.
    RequestDefinition - kérelmező szerv
    HandlerProcess ‒ A kezelő PID-je. Ez a folyamat választ fog kapni a szervertől.

  2. A válasz feldolgozása

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

    ResponsePayload - szerver válasz.

A szerver esetében a folyamat szintén 2 fázisból áll:

  1. A cserepont inicializálása
  2. Beérkezett kérelmek feldolgozása

Illusztráljuk ezt a sablont kóddal. Tegyük fel, hogy egy egyszerű szolgáltatást kell megvalósítanunk, amely egyetlen pontos időmódszert biztosít.

Szerver kód

Határozzuk meg a szolgáltatás API-t az api.hrl-ben:

%% =====================================================
%%  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{}
}).

Határozzuk meg a szolgáltatásvezérlőt a time_controller.erl fájlban

%% В примере показан только значимый код. Вставив его в шаблон 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.

Ügyfélkód

Ha kérést szeretne küldeni a szolgáltatásnak, az üzenetküldési kérés API-t bárhol a kliensben hívhatja:

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

Egy elosztott rendszerben az összetevők konfigurációja nagyon eltérő lehet, és előfordulhat, hogy a kérés időpontjában az üzenetküldés még nem indul el, vagy a szolgáltatásvezérlő nem áll készen a kérés kiszolgálására. Ezért ellenőriznünk kell az üzenetküldési választ, és kezelnünk kell a hibaesetet.
Sikeres küldés után a kliens választ vagy hibaüzenetet kap a szolgáltatástól.
Mindkét esetet kezeljük a handle_info-ban:

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};

Kérésre darabolt válasz

A legjobb elkerülni a hatalmas üzenetek küldését. Ezen múlik a teljes rendszer érzékenysége és stabil működése. Ha egy lekérdezésre adott válasz sok memóriát foglal el, akkor részekre bontása kötelező.

Az elosztott alkalmazások építőkövei. Első megközelítés

Hadd mondjak néhány példát ilyen esetekre:

  • Az összetevők bináris adatokat, például fájlokat cserélnek. A válasz apró részekre bontása segít hatékonyan dolgozni bármilyen méretű fájllal, és elkerülheti a memória túlcsordulását.
  • Listákat. Például ki kell választanunk az összes rekordot egy hatalmas táblából az adatbázisban, és át kell vinnünk őket egy másik komponensbe.

Ezeket a válaszokat mozdonynak nevezem. Mindenesetre 1024 1 MB-os üzenet jobb, mint egyetlen 1 GB-os üzenet.

Az Erlang klaszterben további előnyhöz jutunk - csökkentjük a cserepont és a hálózat terhelését, mivel a válaszok azonnal elküldésre kerülnek a címzettnek, megkerülve a cserepontot.

Válasz kéréssel

Ez egy meglehetősen ritka módosítása az RPC mintának a párbeszédes rendszerek építéséhez.

Az elosztott alkalmazások építőkövei. Első megközelítés

Közzététel-előfizetés (adatelosztási fa)

Az eseményvezérelt rendszerek eljuttatják azokat a fogyasztókhoz, amint az adatok készen állnak. Így a rendszerek hajlamosabbak a push modellre, mint a pull vagy poll modellre. Ezzel a funkcióval elkerülhető az erőforrások pazarlása az adatok folyamatos kérésével és várakozásával.
Az ábra egy adott témára feliratkozott fogyasztóknak szóló üzenet terjesztésének folyamatát mutatja.

Az elosztott alkalmazások építőkövei. Első megközelítés

Klasszikus példái ennek a mintának az állapoteloszlás: a játékvilág a számítógépes játékokban, a tőzsdék piaci adatai, az adatfolyamokban hasznos információk.

Nézzük az előfizetői kódot:

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.

A forrás meghívhatja a függvényt, hogy üzenetet tegyen közzé bármilyen kényelmes helyen:

messaging:publish_message(Exchange, Key, Message).

csere - a cserepont neve,
Kulcs - útválasztó kulcs
Üzenet - hasznos teher

Fordított Publish-subscribe

Az elosztott alkalmazások építőkövei. Első megközelítés

A pub-sub bővítésével a naplózáshoz kényelmes mintát kaphat. A források és a fogyasztók köre teljesen eltérő lehet. Az ábra egy fogyasztó és több forrás esetét mutatja.

Feladatelosztási minta

Szinte minden projekt tartalmaz halasztott feldolgozási feladatokat, mint például jelentések generálása, értesítések kézbesítése és adatok lekérése harmadik féltől származó rendszerekből. Az ezeket a feladatokat ellátó rendszer átviteli sebessége könnyen skálázható kezelők hozzáadásával. Nem marad más hátra, mint a processzorok klaszterének kialakítása és a feladatok egyenletes elosztása közöttük.

Nézzük meg a felmerülő helyzeteket 3 kezelő példáján. Már a feladatelosztás szakaszában is felmerül a méltányos elosztás és a kezelők túlcsordulása. A körmérkőzéses elosztás felelős lesz a méltányosságért, és a kezelők túlcsordulásának elkerülése érdekében korlátozást vezetünk be. prefetch_limit. Átmeneti körülmények között prefetch_limit megakadályozza, hogy egy kezelő megkapja az összes feladatot.

Az üzenetkezelés kezeli a sorokat és a feldolgozási prioritást. A processzorok a megérkezésükkor kapják meg a feladatokat. A feladat sikeresen vagy sikertelenül befejeződhet:

  • messaging:ack(Tack) - hívják, ha az üzenet sikeresen feldolgozásra került
  • messaging:nack(Tack) - minden vészhelyzetben hívják. A feladat visszaküldése után az üzenetküldés továbbadja azt egy másik kezelőnek.

Az elosztott alkalmazások építőkövei. Első megközelítés

Tegyük fel, hogy három feladat feldolgozása közben összetett hiba történt: az 1. processzor a feladat fogadása után összeomlott anélkül, hogy bármit is jelentett volna a cserepontnak. Ebben az esetben a cserepont átadja a feladatot egy másik kezelőnek, miután az átvételi időtúllépés lejárt. Valamilyen oknál fogva a 3. kezelő felhagyott a feladattal, és nack-et küldött, ennek eredményeként a feladatot egy másik kezelőhöz is áthelyezték, aki sikeresen teljesítette.

Előzetes összefoglalás

Áttekintettük az elosztott rendszerek alapvető építőköveit, és alapvető ismereteket szereztünk használatukról az Erlang/Elixirben.

Az alapvető minták kombinálásával összetett paradigmákat építhet a felmerülő problémák megoldására.

A sorozat utolsó részében a szolgáltatások megszervezésének, az útválasztásnak és a kiegyenlítésnek általános kérdéseit tekintjük át, és szót ejtünk a rendszerek skálázhatóságának és hibatűrésének gyakorlati oldaláról is.

A második rész vége.

fénykép Marius Christensen
Az illusztrációk a websequencediagrams.com segítségével készültek

Forrás: will.com

Hozzászólás