Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Viimeisessä статье tarkastelimme reaktiivisen arkkitehtuurin teoreettisia perusteita. On aika puhua tietovirroista, tavoista toteuttaa reaktiivisia Erlang/Elixir-järjestelmiä ja viestintäkuvioita niihin:

  • Pyyntö-vastaus
  • Request-Cunked Response
  • Vastaa Pyynnöllä
  • Julkaise-tilaa
  • Käänteinen Publish-subscribe
  • Tehtävien jako

SOA, MSA ja Messaging

SOA, MSA ovat järjestelmäarkkitehtuureja, jotka määrittävät säännöt rakennusjärjestelmille, kun taas viestintä tarjoaa primitiivit niiden toteuttamiseen.

En halua mainostaa tätä tai toista järjestelmäarkkitehtuuria. Haluan käyttää tehokkaimpia ja hyödyllisimpiä käytäntöjä tietyssä projektissa ja liiketoiminnassa. Minkä paradigman valitsemmekin, on parempi luoda järjestelmälohkoja Unix-tietä silmällä pitäen: komponentteja, joilla on minimaalinen liitettävyys ja jotka vastaavat yksittäisistä kokonaisuuksista. API-menetelmät suorittavat yksinkertaisimpia mahdollisia toimintoja entiteettien kanssa.

Viestit on, kuten nimestä voi päätellä, viestien välittäjä. Sen päätarkoitus on vastaanottaa ja lähettää viestejä. Se vastaa tiedon lähetysrajapinnoista, loogisten kanavien muodostamisesta tiedon siirtoon järjestelmän sisällä, reitityksestä ja tasapainotuksesta sekä järjestelmätason viankäsittelystä.
Kehitämämme viestintä ei yritä kilpailla rabbitmq:n kanssa tai korvata sitä. Sen pääominaisuudet:

  • Jakelu.
    Vaihtopisteitä voidaan luoda kaikkiin klusterin solmuihin, mahdollisimman lähelle niitä käyttävää koodia.
  • Yksinkertaisuus.
    Keskity mallikoodin minimoimiseen ja helppokäyttöisyyteen.
  • Parempi suorituskyky.
    Emme yritä toistaa rabbitmq:n toimivuutta, vaan korostamme vain arkkitehtuuri- ja kuljetuskerrosta, jonka sovitamme OTP:hen mahdollisimman yksinkertaisesti, minimoiden kustannukset.
  • Joustavuus.
    Jokainen palvelu voi yhdistää useita vaihtomalleja.
  • Suunnittelun joustavuus.
  • Skaalautuvuus.
    Viestit kasvavat sovelluksen mukana. Kuorman kasvaessa voit siirtää vaihtopisteitä yksittäisiin koneisiin.

Huom. Koodin organisoinnin kannalta metaprojektit sopivat hyvin monimutkaisiin Erlang/Elixir-järjestelmiin. Kaikki projektikoodit sijaitsevat yhdessä arkistossa - sateenvarjoprojektissa. Samalla mikropalvelut ovat mahdollisimman eristettyjä ja suorittavat yksinkertaisia ​​toimintoja, jotka vastaavat erillisestä kokonaisuudesta. Tällä lähestymistavalla on helppo ylläpitää koko järjestelmän API:ta, on helppo tehdä muutoksia, on kätevää kirjoittaa yksikkö- ja integraatiotestejä.

Järjestelmän osat ovat vuorovaikutuksessa suoraan tai välittäjän kautta. Viestinvälityksen näkökulmasta jokaisella palvelulla on useita elämänvaiheita:

  • Palvelun alustus.
    Tässä vaiheessa palvelua suorittava prosessi ja riippuvuudet määritetään ja käynnistetään.
  • Vaihtopisteen luominen.
    Palvelu voi käyttää solmukonfiguraatiossa määritettyä staattista vaihtopistettä tai luoda vaihtopisteitä dynaamisesti.
  • Palvelun rekisteröinti.
    Jotta palvelu voi palvella pyyntöjä, se on rekisteröitävä vaihtopisteessä.
  • Normaali toiminta.
    Palvelu tuottaa hyödyllistä työtä.
  • Sammuttaa.
    On 2 mahdollista sammutustyyppiä: normaali ja hätäpysäytys. Normaalin toiminnan aikana palvelu katkeaa vaihtopisteestä ja pysähtyy. Hätätilanteissa viestintä suorittaa yhden vikasietokomentosarjasta.

Se näyttää melko monimutkaiselta, mutta koodi ei ole niin pelottava. Koodiesimerkkejä kommentteineen annetaan mallien analysoinnissa hieman myöhemmin.

Vaihto

Vaihtopiste on viestintäprosessi, joka toteuttaa vuorovaikutuksen logiikan viestimallin komponenttien kanssa. Kaikissa alla esitetyissä esimerkeissä komponentit ovat vuorovaikutuksessa vaihtopisteiden kautta, joiden yhdistelmä muodostaa viestinnän.

Viestinvaihtomallit (MEP)

Globaalisti vaihtomallit voidaan jakaa kaksisuuntaisiin ja yksisuuntaisiin. Edellinen tarkoittaa vastausta saapuvaan viestiin, jälkimmäiset eivät. Klassinen esimerkki kaksisuuntaisesta mallista asiakas-palvelin-arkkitehtuurissa on Request-response -malli. Katsotaanpa mallia ja sen muutoksia.

Pyyntö-vastaus tai RPC

RPC:tä käytetään, kun meidän on saatava vastaus toiselta prosessilta. Tämä prosessi voi olla käynnissä samassa solmussa tai eri mantereella. Alla on kaavio asiakkaan ja palvelimen välisestä vuorovaikutuksesta viestien kautta.

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Koska viestintä on täysin asynkronista, asiakkaalle vaihto on jaettu kahteen vaiheeseen:

  1. Pyynnön lähettäminen

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

    vaihto ‒ vaihtopisteen yksilöllinen nimi
    ResponseMatchingTag ‒ paikallinen etiketti vastauksen käsittelyä varten. Esimerkiksi, jos lähetetään useita identtisiä pyyntöjä, jotka kuuluvat eri käyttäjille.
    RequestDefinition - pyynnön elin
    Käsittelijäprosessi ‒ Käsittelijän PID. Tämä prosessi saa vastauksen palvelimelta.

  2. Vastausta käsitellään

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

    ResponsePayload - palvelimen vastaus.

Palvelimen osalta prosessi koostuu myös kahdesta vaiheesta:

  1. Vaihtopisteen alustaminen
  2. Vastaanotettujen pyyntöjen käsittely

Havainnollistetaan tämä malli koodilla. Oletetaan, että meidän on otettava käyttöön yksinkertainen palvelu, joka tarjoaa yhden tarkan aikamenetelmän.

Palvelimen koodi

Määritetään palvelun sovellusliittymä api.hrl:ssä:

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

Määritetään palvelun ohjain tiedostossa 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.

Asiakaskoodi

Pyynnön lähettämiseksi palveluun voit soittaa viestintäpyynnön API:lle missä tahansa asiakkaassa:

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

Hajautetussa järjestelmässä komponenttien konfiguraatio voi olla hyvinkin erilainen ja pyyntöhetkellä viestintä ei ehkä vielä käynnisty tai palvelunohjain ei ole valmis palvelemaan pyyntöä. Siksi meidän on tarkistettava viestivastaus ja käsiteltävä vikatapaus.
Onnistuneen lähetyksen jälkeen asiakas saa palvelulta vastauksen tai virheilmoituksen.
Käsitellään molemmat tapaukset hand_infossa:

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

Request-Cunked Response

On parasta välttää suurten viestien lähettämistä. Tästä riippuu koko järjestelmän reagointikyky ja vakaa toiminta. Jos kyselyyn vastaaminen vie paljon muistia, sen jakaminen osiin on pakollista.

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Annan teille pari esimerkkiä tällaisista tapauksista:

  • Komponentit vaihtavat binaaridataa, kuten tiedostoja. Reaktion jakaminen pieniin osiin auttaa sinua työskentelemään tehokkaasti kaikenkokoisten tiedostojen kanssa ja välttämään muistin ylivuotoja.
  • Listaukset. Meidän on esimerkiksi valittava kaikki tietueet tietokannan valtavasta taulukosta ja siirrettävä ne toiseen komponenttiin.

Kutsun näitä vastauksia vetureiksi. Joka tapauksessa 1024 1 Mt:n viestiä on parempi kuin yksittäinen 1 Gt:n viesti.

Erlang-klusterissa saamme lisäetua - vähennämme vaihtopisteen ja verkon kuormitusta, koska vastaukset lähetetään välittömästi vastaanottajalle ohittaen vaihtopisteen.

Vastaa Pyynnöllä

Tämä on melko harvinainen muunnos RPC-mallista dialogijärjestelmien rakentamiseen.

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Julkaise-tilaa (tietojen jakelupuu)

Tapahtumapohjaiset järjestelmät toimittavat ne kuluttajille heti, kun tiedot ovat valmiita. Siten järjestelmät ovat alttiimpia push-mallille kuin veto- tai kyselymallille. Tämän ominaisuuden avulla voit välttää resurssien tuhlaamisen pyytämällä ja odottamalla jatkuvasti tietoja.
Kuvassa näkyy viestin jakaminen tietyn aiheen tilaajille.

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Klassisia esimerkkejä tämän kaavan käytöstä ovat tilajakauma: pelimaailma tietokonepeleissä, markkinatiedot pörssistä, hyödyllinen tieto tietosyötteissä.

Katsotaanpa tilaajakoodia:

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.

Lähde voi kutsua toimintoa julkaistakseen viestin missä tahansa sopivassa paikassa:

messaging:publish_message(Exchange, Key, Message).

vaihto - vaihtopisteen nimi,
avain - reititysavain
Viesti - hyötykuorma

Käänteinen Publish-subscribe

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Laajentamalla pubi-alaa saat kätevän kaavan kirjaamiseen. Lähteet ja kuluttajat voivat olla täysin erilaisia. Kuvassa on tapaus, jossa on yksi kuluttaja ja useita lähteitä.

Tehtävän jakautumismalli

Lähes kaikkiin projekteihin liittyy viivästettyjen käsittelytehtävien suorittamista, kuten raporttien luomista, ilmoitusten toimittamista ja tietojen hakemista kolmannen osapuolen järjestelmistä. Näitä tehtäviä suorittavan järjestelmän suorituskykyä voidaan helposti skaalata lisäämällä käsittelijöitä. Meidän tarvitsee vain muodostaa prosessoriklusteri ja jakaa tehtävät tasaisesti niiden kesken.

Tarkastellaan tilanteita, joita syntyy 3 käsittelijän esimerkin avulla. Jo tehtäväjaon vaiheessa herää kysymys jaon oikeudenmukaisuudesta ja käsittelijöiden ylivuotosta. Round-robin-jakelu on vastuussa oikeudenmukaisuudesta, ja välttääksemme käsittelijöiden ylivuototilanteen otamme käyttöön rajoituksen prefetch_limit. Tilapäisissä olosuhteissa prefetch_limit estää yhtä käsittelijää vastaanottamasta kaikkia tehtäviä.

Viestit hallitsevat jonoja ja käsittelyprioriteettia. Prosessorit saavat tehtäviä saapuessaan. Tehtävä voidaan suorittaa onnistuneesti tai epäonnistua:

  • messaging:ack(Tack) - soitetaan, jos viesti on käsitelty onnistuneesti
  • messaging:nack(Tack) - soitetaan kaikissa hätätilanteissa. Kun tehtävä palautetaan, viestit välittävät sen toiselle käsittelijälle.

Hajautettujen sovellusten rakennuspalikoita. Ensimmäinen lähestymistapa

Oletetaan, että kolmea tehtävää käsiteltäessä tapahtui monimutkainen vika: prosessori 1 kaatui tehtävän vastaanottamisen jälkeen ilman, että olisi ehtinyt ilmoittaa mitään vaihtopisteeseen. Tässä tapauksessa vaihtopiste siirtää tehtävän toiselle käsittelijälle, kun kuittausaikakatkaisu on umpeutunut. Jostain syystä käsittelijä 3 hylkäsi tehtävän ja lähetti nackin, minkä seurauksena tehtävä siirtyi myös toiselle käsittelijälle, joka suoritti sen onnistuneesti.

Alustava yhteenveto

Olemme käsitelleet hajautettujen järjestelmien perusrakenneosat ja saaneet peruskäsityksen niiden käytöstä Erlang/Elixirissä.

Yhdistämällä perusmalleja voit rakentaa monimutkaisia ​​paradigmoja ratkaisemaan uusia ongelmia.

Sarjan viimeisessä osassa tarkastellaan yleisiä palveluiden organisoinnin, reitityksen ja tasapainotuksen kysymyksiä sekä puhutaan myös järjestelmien skaalautuvuuden ja vikasietoisuuden käytännön puolelta.

Toisen osan loppu.

Photo Shoot Marius Christensen
Kuvat on valmistettu käyttämällä websequencediagrams.com-sivustoa

Lähde: will.com

Lisää kommentti