Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

En l'últim article Hem examinat els fonaments teòrics de l'arquitectura reactiva. És hora de parlar sobre els fluxos de dades, les maneres d'implementar sistemes Erlang/Elixir reactius i els patrons de missatgeria en ells:

  • Sol·licitud-resposta
  • Sol·licitud de resposta fragmentada
  • Resposta amb Petició
  • Publicar-subscriure
  • Publicació-subscripció invertida
  • Distribució de tasques

SOA, MSA i missatgeria

SOA, MSA són arquitectures de sistemes que defineixen les regles per construir sistemes, mentre que la missatgeria proporciona primitives per a la seva implementació.

No vull promoure aquesta o aquella arquitectura del sistema. Estic a favor d'utilitzar les pràctiques més efectives i útils per a un projecte i negoci concrets. Sigui quin sigui el paradigma que triem, és millor crear blocs de sistema amb la mirada posada en la manera Unix: components amb una connectivitat mínima, responsables de les entitats individuals. Els mètodes API realitzen les accions més senzilles possibles amb les entitats.

La missatgeria és, com el seu nom indica, un intermediari de missatges. La seva finalitat principal és rebre i enviar missatges. S'encarrega de les interfícies d'enviament d'informació, la formació de canals lògics per a la transmissió d'informació dins del sistema, l'encaminament i l'equilibri, així com la gestió de fallades a nivell del sistema.
El missatge que estem desenvolupant no intenta competir ni substituir rabbitmq. Les seves principals característiques:

  • Distribució.
    Els punts d'intercanvi es poden crear a tots els nodes del clúster, el més a prop possible del codi que els utilitza.
  • Senzillesa.
    Centra't a minimitzar el codi normal i la facilitat d'ús.
  • Millor rendiment.
    No estem intentant repetir la funcionalitat de rabbitmq, sinó destacar només la capa arquitectònica i de transport, que encaixem a l'OTP de la manera més senzilla possible, minimitzant els costos.
  • Flexibilitat.
    Cada servei pot combinar moltes plantilles d'intercanvi.
  • Resiliència per disseny.
  • Escalabilitat.
    La missatgeria creix amb l'aplicació. A mesura que augmenta la càrrega, podeu moure els punts d'intercanvi a màquines individuals.

Observació Pel que fa a l'organització del codi, els metaprojectes són adequats per a sistemes complexos Erlang/Elixir. Tot el codi del projecte es troba en un dipòsit: un projecte paraigua. Al mateix temps, els microserveis s'aïllen al màxim i realitzen operacions senzilles que són responsables d'una entitat separada. Amb aquest enfocament, és fàcil mantenir l'API de tot el sistema, és fàcil fer canvis, és convenient escriure proves d'unitat i integració.

Els components del sistema interactuen directament o mitjançant un corredor. Des d'una perspectiva de missatgeria, cada servei té diverses fases de vida:

  • Inicialització del servei.
    En aquesta etapa, es configuren i s'inicien el procés i les dependències que executen el servei.
  • Creació d'un punt d'intercanvi.
    El servei pot utilitzar un punt d'intercanvi estàtic especificat a la configuració del node o crear punts d'intercanvi dinàmicament.
  • Registre del servei.
    Perquè el servei pugui atendre les sol·licituds, s'ha d'inscriure al punt d'intercanvi.
  • Funcionament normal.
    El servei produeix un treball útil.
  • Finalització de l'obra.
    Hi ha 2 tipus d'aturada possibles: normal i d'emergència. Durant el funcionament normal, el servei es desconnecta del punt d'intercanvi i s'atura. En situacions d'emergència, la missatgeria executa un dels scripts de failover.

Sembla bastant complicat, però el codi no fa tanta por. Exemples de codi amb comentaris es donaran una mica més endavant en l'anàlisi de plantilles.

Intercanvis

El punt d'intercanvi és un procés de missatgeria que implementa la lògica d'interacció amb components dins de la plantilla de missatgeria. En tots els exemples que es presenten a continuació, els components interactuen mitjançant punts d'intercanvi, la combinació dels quals forma missatgeria.

Patrons d'intercanvi de missatges (MEP)

A nivell mundial, els patrons d'intercanvi es poden dividir en bidireccionals i unidireccionals. Els primers impliquen una resposta a un missatge entrant, els segons no. Un exemple clàssic d'un patró bidireccional en l'arquitectura client-servidor és el patró de petició-resposta. Vegem la plantilla i les seves modificacions.

Sol·licitud-resposta o RPC

RPC s'utilitza quan necessitem rebre una resposta d'un altre procés. Aquest procés pot estar executant-se al mateix node o situat en un continent diferent. A continuació es mostra un diagrama de la interacció entre client i servidor mitjançant missatgeria.

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Com que la missatgeria és completament asíncrona, per al client l'intercanvi es divideix en 2 fases:

  1. Enviar sol·licitud

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

    Exchange ‒ nom únic del punt d'intercanvi
    ResponseMatchingTag ‒ etiqueta local per processar la resposta. Per exemple, en el cas d'enviar diverses peticions idèntiques pertanyents a diferents usuaris.
    Definició de petició - òrgan de la sol·licitud
    HandlerProcess ‒ PID del gestor. Aquest procés rebrà una resposta del servidor.

  2. Processament de la resposta

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

    Càrrega útil de resposta - Resposta del servidor.

Per al servidor, el procés també consta de 2 fases:

  1. Inicialització del punt d'intercanvi
  2. Tramitació de les sol·licituds rebudes

Il·lustrem aquesta plantilla amb codi. Suposem que necessitem implementar un servei senzill que proporcioni un únic mètode de temps exacte.

Codi del servidor

Definim l'API del servei a 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{}
}).

Definim el controlador de servei a 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.

Codi de client

Per enviar una sol·licitud al servei, podeu trucar a l'API de sol·licitud de missatgeria en qualsevol lloc del client:

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

En un sistema distribuït, la configuració dels components pot ser molt diferent i, en el moment de la sol·licitud, és possible que la missatgeria encara no s'iniciï o que el controlador del servei no estigui preparat per atendre la sol·licitud. Per tant, hem de comprovar la resposta de missatgeria i gestionar el cas d'error.
Després de l'enviament satisfactori, el client rebrà una resposta o error del servei.
Tractem els dos casos a 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};

Sol·licitud de resposta fragmentada

El millor és evitar enviar grans missatges. D'això depèn la capacitat de resposta i el funcionament estable de tot el sistema. Si la resposta a una consulta ocupa molta memòria, és obligatori dividir-la en parts.

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Permeteu-me donar-vos un parell d'exemples d'aquests casos:

  • Els components intercanvien dades binàries, com ara fitxers. Dividir la resposta en parts petites us ajuda a treballar de manera eficient amb fitxers de qualsevol mida i a evitar desbordaments de memòria.
  • Llistats. Per exemple, hem de seleccionar tots els registres d'una taula enorme de la base de dades i transferir-los a un altre component.

A aquestes respostes les anomeno locomotora. En qualsevol cas, 1024 missatges d'1 MB són millors que un sol missatge d'1 GB.

Al clúster Erlang, obtenim un avantatge addicional: reduir la càrrega del punt d'intercanvi i de la xarxa, ja que les respostes s'envien immediatament al destinatari, sense passar pel punt d'intercanvi.

Resposta amb Petició

Aquesta és una modificació força rara del patró RPC per construir sistemes de diàleg.

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Publicació-subscripció (arbre de distribució de dades)

Els sistemes basats en esdeveniments els entreguen als consumidors tan aviat com les dades estiguin a punt. Per tant, els sistemes són més propensos a un model push que a un model pull o poll. Aquesta característica us permet evitar el malbaratament de recursos sol·licitant i esperant dades constantment.
La figura mostra el procés de distribució d'un missatge als consumidors subscrits a un tema concret.

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Exemples clàssics d'ús d'aquest patró són la distribució de l'estat: el món del joc en els jocs d'ordinador, les dades del mercat sobre els intercanvis, la informació útil en les fonts de dades.

Vegem el codi de subscriptor:

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.

La font pot trucar a la funció per publicar un missatge en qualsevol lloc convenient:

messaging:publish_message(Exchange, Key, Message).

Exchange - nom del punt d'intercanvi,
Clau - clau d'encaminament
Missatge - càrrega útil

Publicació-subscripció invertida

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

En expandir pub-sub, podeu obtenir un patró convenient per al registre. El conjunt de fonts i consumidors pot ser completament diferent. La figura mostra un cas amb un consumidor i diverses fonts.

Patró de distribució de tasques

Gairebé tots els projectes impliquen tasques de processament ajornat, com ara generar informes, enviar notificacions i recuperar dades de sistemes de tercers. El rendiment del sistema que realitza aquestes tasques es pot escalar fàcilment afegint controladors. Només ens queda formar un clúster de processadors i distribuir uniformement les tasques entre ells.

Vegem les situacions que es presenten fent servir l'exemple de 3 controladors. Fins i tot en l'etapa de distribució de tasques, sorgeix la qüestió de l'equitat de la distribució i el desbordament dels gestors. La distribució de rondes serà responsable de l'equitat, i per evitar una situació de desbordament dels manipuladors, introduirem una restricció prefetch_limit. En condicions transitories prefetch_limit impedirà que un gestor rebi totes les tasques.

La missatgeria gestiona les cues i la prioritat de processament. Els processadors reben tasques a mesura que arriben. La tasca es pot completar correctament o fallar:

  • messaging:ack(Tack) - cridat si el missatge s'ha processat correctament
  • messaging:nack(Tack) - cridat en totes les situacions d'emergència. Un cop retornada la tasca, la missatgeria la passarà a un altre gestor.

Blocs de construcció d'aplicacions distribuïdes. Primera aproximació

Suposem que s'ha produït una fallada complexa durant el processament de tres tasques: el processador 1, després de rebre la tasca, es va estavellar sense tenir temps per informar de res al punt d'intercanvi. En aquest cas, el punt d'intercanvi transferirà la tasca a un altre gestor després que hagi expirat el temps d'espera de la confirmació. Per alguna raó, el gestor 3 va abandonar la tasca i va enviar nack; com a resultat, la tasca també es va transferir a un altre gestor que la va completar amb èxit.

Resum preliminar

Hem tractat els blocs bàsics dels sistemes distribuïts i hem aconseguit una comprensió bàsica del seu ús a Erlang/Elixir.

En combinar patrons bàsics, podeu construir paradigmes complexos per resoldre problemes emergents.

A la part final de la sèrie, analitzarem els problemes generals d'organització dels serveis, l'encaminament i l'equilibri, i també parlarem de la part pràctica de l'escalabilitat i la tolerància a errors dels sistemes.

Final de la segona part.

Sessió De Fotos Marius Christensen
Il·lustracions preparades mitjançant websequencediagrams.com

Font: www.habr.com

Afegeix comentari