Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

У мінулым артыкуле мы разабралі тэарэтычныя асновы рэактыўнай архітэктуры. Нетутэйша час пагаварыць аб струменях дадзеных, шляхах рэалізацыі рэактыўных Erlang/Elixir сістэм і шаблонах абмену паведамленнямі ў іх:

  • Request-response
  • Request-Chunked Response
  • Response with Request
  • Апублікаваць-падпісацца
  • Inverted Publish-subscribe
  • Task distribution

SOA, MSA і абмен паведамленнямі

SOA, MSA - сістэмныя архітэктуры, якія вызначаюць правілы пабудовы сістэм, у той час як messaging дае прымітывы для іх рэалізацыі.

Я не хачу прапагандаваць тую ці іншую архітэктуру пабудовы сістэм. Я за прымяненне максімальна эфектыўных і карысных для канкрэтнага праекта і бізнесу практык. Якую б парадыгму мы ні абралі, ствараць сістэмныя блокі лепш з аглядкай на Unix-way: кампаненты з мінімальнай складнасцю, якія адказваюць за асобныя сутнасці. Метады API выконваюць максімальна простыя дзеянні з сутнасцямі.

Messaging ‒ як зразумела з назвы ‒ брокер паведамленняў. Яго асноўная мэта - прымаць і аддаваць паведамленні. Ён адказвае за інтэрфейсы адпраўкі інфармацыі, фарміраванне лагічных каналаў перадачы інфармацыі ўнутры сістэмы, маршрутызацыю і балансаванне, а таксама апрацоўку адмоў на сістэмным узроўні.
Распрацоўваны messaging не спрабуе канкураваць з rabbitmq ці замяняць яго. Яго асноўныя рысы:

  • Размеркаванасць.
    Пункты абмену можна ствараць на ўсіх вузлах кластара, максімальна блізка да кода, які выкарыстоўвае іх.
  • Прастата.
    Скіраванасць на мінімізацыю шаблоннага кода і зручнасць выкарыстання.
  • Лепшая прадукцыйнасць.
    Мы не спрабуем паўтарыць функцыянал rabbitmq, а вылучаем толькі архітэктурны і транспартны пласт, які максімальна проста ўпісваем у OTP, мінімізуючы выдаткі.
  • Гнуткасць.
    Кожны сэрвіс можа аб'ядноўваць у сабе мноства шаблонаў абмену.
  • Адмаўстойлівасць, закладзеная ў дызайне.
  • Маштабаванасць.
    Messaging расце разам з дадаткам. Па меры павелічэння нагрузкі можна выносіць кропкі абмену на асобныя машыны.

Заўвага. З пункту гледжання арганізацыі кода, для складаных сістэм на Erlang/Elixir добра падыходзяць мета-праекты. Увесь код праекта знаходзіцца ў адным рэпазітары - парасонавай праекце. Пры гэтым мікрасэрвісы максімальна ізаляваныя і выконваюць простыя аперацыі, якія адказваюць за асобную сутнасць. Пры такім падыходзе лёгка падтрымліваць API усёй сістэмы, проста ўносіць змены, зручна пісаць юніт і інтэграцыйныя тэсты.

Кампаненты сістэмы ўзаемадзейнічаюць напрамую ці ж праз брокера. З пазіцыі messaging, кожны сэрвіс мае некалькі жыццёвых фаз:

  • Ініцыялізацыя сэрвісу.
    На дадзеным этапе адбываецца канфігурацыя і запуск выконваючага сэрвіс працэсу і залежнасцяў.
  • Стварэнне пункта абмену.
    Сэрвіс можа выкарыстоўваць статычную кропку абмену, зададзеную ў канфігурацыі вузла, альбо ж ствараць кропкі абмену дынамічна.
  • Рэгістрацыя сэрвісу.
    Каб сэрвіс мог абслугоўваць запыты, яго трэба зарэгістраваць на кропцы абмену.
  • Нармальнае функцыянаванне.
    Сэрвіс праводзіць карысную працу.
  • Завяршэнне працы.
    Магчымы 2 віды завяршэння работы: штатнае і аварыйнае. Пры штатным сэрвіс адключаецца ад кропкі абмену і спыняецца. У аварыйных выпадках messaging выконвае адзін са сцэнарыяў апрацоўкі адмоў.

Выглядае даволі складана, але ў кодзе не ўсё так страшна. Прыклады кода з каментарамі будуць прыведзены ў разборы шаблонаў крыху пазней.

Біржы

Кропка абмену ‒ працэс messaging, які рэалізуе логіку ўзаемадзеяння з кампанентамі ў рамках шаблону абмену паведамленнямі. Ва ўсіх прыкладах, прадстаўленых ніжэй, кампаненты ўзаемадзейнічаюць праз кропкі абмену, камбінацыя якіх і ўтварае messaging.

Message exchange patterns (MEPs)

Глабальна шаблоны абмену можна падзяліць на двухбаковыя і аднабаковыя. Першыя маюць на ўвазе адказ на якое паступіла паведамленне, другія няма. Класічным прыкладам двухбаковага шаблону ў кліент-сервернай архітэктуры з'яўляецца Request-response шаблон. Разгледзім шаблон і яго мадыфікацыі.

Request-response або RPC

RPC выкарыстоўваецца, калі нам трэба атрымаць адказ ад іншага працэсу. Гэты працэс можа быць запушчаны на тым жа вузле ці знаходзіцца на іншым кантыненце. Ніжэй прадстаўлена схема ўзаемадзеяння кліента і сервера праз messaging.

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Паколькі messaging цалкам асінхронны, то для кліента абмен падзяляецца на 2 фазы:

  1. Адпраўка запыту

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

    абмен ‒ унікальнае імя пункту абмену
    ResponseMatchingTag ‒ лакальная пазнака для апрацоўкі адказу. Напрыклад у выпадку адпраўкі некалькіх аднолькавых запытаў, якія належаць розным карыстальнікам.
    RequestDefinition ‒ цела запыту
    HandlerProcess ‒ PID апрацоўшчыка. Гэтаму працэсу прыйдзе адказ ад сэрвера.

  2. Апрацоўка адказу

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

    ResponsePayload ‒ адказ сервера.

Для сервера працэс таксама складаецца з 2 фаз:

  1. Ініцыялізацыя пункта абмену
  2. Апрацоўка паступілі запытаў

Праілюструем кодам дадзены шаблон. Дапусцім, што нам неабходна рэалізаваць просты сэрвіс, які прадстаўляе адзіны метад дакладнага часу.

Код сервера

Вынесем вызначэнне API сэрвісу ў 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{}
}).

Вызначым кантролер сэрвісу ў 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.

Код кліента

Для таго каб даслаць запыт сэрвісу, у любым месцы кліента можна выклікаць messaging request API:

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

У размеркаванай сістэме канфігурацыя кампанентаў можа быць самай рознай і ў момант запыту messaging можа яшчэ не запусціцца, ці ж кантролер сэрвісу не будзе готаў абслужыць запыт. Таму нам трэба праверыць адказ messaging і апрацаваць выпадак адмовы.
Пасля паспяховай адпраўкі кліенту ад сэрвісу прыйдзе адказ ці памылка.
Апрацуем абодва выпадкі ў 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};

Request-Chunked Response

Лепш не дапушчаць перадачы вялізных паведамленняў. Ад гэтага залежыць спагадлівасць і стабільная праца ўсёй сістэмы. Калі адказ на запыт займае шмат памяці, то разбіўка на часткі з'яўляецца абавязковай.

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Прывяду пару прыкладаў такіх выпадкаў:

  • Кампаненты абменьваюцца бінарнымі дадзенымі, напрыклад файламі. Разбіўка адказу на невялікія часткі дапамагае эфектыўна працаваць з файламі любога памеру і не лавіць перапаўненні памяці.
  • Лістынгі. Напрыклад, нам трэба выбраць усе запісы з вялізнай табліцы ў базе і перадаць іншаму кампаненту.

Я называю такія адказы паравозам. У любым выпадку, 1024 паведамленняў па 1 Мб лепш, чым адзінае паведамленне памерам 1 Гб.

У Erlang кластары мы атрымліваем дадатковы выйгрыш ‒ памяншэнне нагрузкі на кропку абмену і сетку, бо адказы адразу накіроўваюцца атрымальніку, абыходзячы кропку абмену.

Response with Request

Гэта даволі рэдкая мадыфікацыя патэрна RPC для пабудовы дыялогавых сістэм.

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Publish-subscribe (data distribution tree)

Падзейна-арыентаваныя сістэмы па меры гатоўнасці даных дастаўляюць іх спажыўцам. Такім чынам, сістэмы больш схільныя да push-мадэлі, чым да pull ці poll. Гэтая асаблівасць дазваляе не марнаваць марна рэсурсы, якія пастаянна запытваючы і чакаючы дадзеныя.
На малюнку прадстаўлены працэс распаўсюджвання паведамлення да спажыўцоў, падпісаным на пэўную тэму.

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Класічнымі прыкладамі выкарыстання гэтага шаблону з'яўляецца распаўсюджванне стану: гульнявога свету ў кампутарных гульнях, рынкавых дадзеных на біржах, карыснай інфармацыі ў датафідах.

Разгледзім код падпісчыка:

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.

Крыніца можа выклікаць функцыю публікацыі паведамлення ў любым зручным месцы:

messaging:publish_message(Exchange, Key, Message).

абмен ‒ назва кропкі абмену,
Ключ ‒ ключ маршрутызацыі
Паведамленне - карысная нагрузка

Inverted Publish-subscribe

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Разгарнуўшы pub-sub, можна атрымаць патэрн, зручны для лагавання. Набор крыніц і спажыўцоў можа быць зусім розным. На малюнку прадстаўлены выпадак з адным спажыўцом і мноствам крыніц.

Task distribution pattern

Амаль у кожным праекце ўзнікаюць задачы адкладзенай апрацоўкі, такія як фармаванне справаздач, дастаўка апавяшчэнняў, атрыманне дадзеных з іншых сістэм. Прапускная здольнасць сістэмы, якая выконвае гэтыя задачы, лёгка маштабуецца шляхам дадання апрацоўшчыкаў. Усё, што нам застаецца, - гэта сфарміраваць кластар з апрацоўшчыкаў і раўнамерна размяркоўваць задачы паміж імі.

Разгледзім якія ўзнікаюць сітуацыі на прыкладзе 3 апрацоўшчыкаў. Яшчэ на этапе размеркавання задач узнікае пытанне справядлівасці размеркавання і перапаўнення апрацоўшчыкаў. За справядлівасць будзе адказваць round-robin размеркаванне, а каб не ўзнікала сітуацыі перапаўнення апрацоўшчыкаў, увядзем абмежаванне prefetch_limit. У пераходных рэжымах prefetch_limit не дасць аднаму апрацоўшчыку атрымаць усе задачы.

Messaging кіруе чэргамі і прыярытэтам апрацоўкі. Апрацоўшчыкі атрымліваюць задачы па меры іх паступлення. Выкананне задачы можа завяршыцца паспяхова альбо адмовай:

  • messaging:ack(Tack) ‒ выклікаецца ў выпадку паспяховай апрацоўкі паведамлення
  • messaging:nack(Tack) ‒ выклікаецца ва ўсіх няштатных сітуацыях. Пасля вяртання задачы messaging перадасць яе на іншы апрацоўшчык.

Будаўнічыя блокі размеркаваных прыкладанняў. Першае набліжэнне

Выкажам здагадку, пры апрацоўцы трох задач здарылася складаная адмова: апрацоўшчык 1 пасля атрымання задачы зваліўся, не паспеўшы паведаміць штосьці кропцы абмену. У гэтым выпадку кропка абмену пасля заканчэння ack timeout перадасць заданне іншаму апрацоўшчыку. Апрацоўшчык 3 па нейкай прычыне адмовіўся ад задачы і адправіў nack, у выніку задача таксама перайшла іншаму апрацоўшчыку які яе паспяхова выканаў.

папярэдні вынік

Мы разабралі асноўныя цаглінкі размеркаваных сістэм і атрымалі базавую разуменне іх прымянення ў Erlang / Elixir.

Камбінуючы базавыя шаблоны, можна выбудоўваць складаныя парадыгмы для рашэння якія ўзнікаюць задач.

У заключнай частцы цыклу мы разгледзім агульныя пытанні арганізацыі сэрвісаў, маршрутызацыі і балансавання, а таксама пагаворым аб практычным баку маштабаванасці і адмоваўстойлівасці сістэм.

Канец другой часткі.

Фота Марыус Крыстэнсэн
Ілюстрацыі падрыхтаваны з дапамогай websequencediagrams.com

Крыніца: habr.com

Дадаць каментар