بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

در آخر مقاله ما مبانی نظری معماری واکنشی را تحلیل کرده‌ایم. زمان آن رسیده است که در مورد جریان داده ها، راه های پیاده سازی سیستم های واکنش پذیر Erlang/Elixir و الگوهای پیام رسانی در آنها صحبت کنیم:

  • درخواست-پاسخ
  • پاسخ تکه تکه درخواست
  • پاسخ با درخواست
  • انتشار-اشتراک
  • اشتراک وارونه انتشار
  • توزیع وظیفه

SOA، MSA و پیام رسانی

SOA، MSA معماری های سیستمی هستند که قوانینی را برای سیستم های ساختمانی تعریف می کنند، در حالی که پیام رسانی اصول اولیه را برای اجرای آنها فراهم می کند.

من نمی خواهم این یا آن معماری سیستم را تبلیغ کنم. من طرفدار به کارگیری مؤثرترین و مفیدترین شیوه ها برای یک پروژه و تجارت خاص هستم. هر پارادایم را که انتخاب می کنیم، بهتر است بلوک های سیستم را با توجه به راه یونیکس ایجاد کنیم: مؤلفه هایی با حداقل اتصال، مسئول واحدهای جداگانه. متدهای API ساده ترین اقدامات را با موجودیت ها انجام می دهند.

پیام رسانی - همانطور که از نامش پیداست - یک کارگزار پیام. هدف اصلی آن دریافت و ارسال پیام است. مسئول رابط های ارسال اطلاعات، تشکیل کانال های منطقی برای انتقال اطلاعات در داخل سیستم، مسیریابی و متعادل سازی و همچنین مدیریت خرابی در سطح سیستم است.
پیام‌های توسعه‌یافته تلاشی برای رقابت یا جایگزینی با rabbitmq نمی‌کنند. ویژگی های اصلی آن:

  • توزیع.
    نقاط تبادل را می توان در تمام گره های خوشه، تا حد امکان نزدیک به کدی که از آنها استفاده می کند، ایجاد کرد.
  • سادگی
    روی به حداقل رساندن کد دیگ بخار و سهولت استفاده تمرکز کنید.
  • عملکرد بهتر
    ما سعی نمی کنیم عملکرد rabbitmq را تکرار کنیم، بلکه فقط لایه معماری و حمل و نقل را انتخاب می کنیم که به سادگی ممکن را در OTP قرار می دهیم و هزینه ها را به حداقل می رساند.
  • انعطاف پذیری
    هر سرویس می تواند چندین قالب مبادله را ترکیب کند.
  • انعطاف پذیری از طریق طراحی
  • مقیاس پذیری
    پیام رسانی با برنامه رشد می کند. با افزایش بار، می توانید نقاط مبادله را به ماشین های جداگانه منتقل کنید.

سخنان از نظر سازماندهی کد، متا پروژه ها برای سیستم های پیچیده Erlang/Elixir مناسب هستند. تمام کد پروژه در یک مخزن است - یک پروژه چتر. در عین حال، میکروسرویس ها تا حد امکان ایزوله می شوند و عملیات ساده ای را انجام می دهند که مسئول یک موجودیت جداگانه است. با این رویکرد، حفظ API کل سیستم آسان است، ایجاد تغییرات آسان است، نوشتن تست های واحد و ادغام راحت است.

اجزای سیستم به طور مستقیم یا از طریق یک کارگزار تعامل دارند. از موقعیت پیام رسانی، هر سرویس دارای چندین مرحله حیاتی است:

  • راه اندازی سرویس
    در این مرحله پیکربندی و راه اندازی فرآیند اجرای سرویس و وابستگی ها انجام می شود.
  • یک نقطه تبادل ایجاد کنید.
    این سرویس می تواند از یک نقطه تبادل ثابت مشخص شده در پیکربندی میزبان استفاده کند یا نقاط تبادل را به صورت پویا ایجاد کند.
  • ثبت خدمات.
    برای اینکه سرویس بتواند درخواست ها را ارائه دهد، باید در نقطه مبادله ثبت شود.
  • عملکرد عادی.
    سرویس کار مفیدی انجام می دهد.
  • اتمام کار.
    2 نوع خاموشی وجود دارد: معمولی و اضطراری. با سرویس معمولی از نقطه تبادل قطع می شود و می ایستد. در موارد اضطراری، پیام رسانی یکی از سناریوهای شکست را اجرا می کند.

به نظر می رسد بسیار پیچیده است، اما کد آنقدر ترسناک نیست. نمونه های کد با نظرات کمی بعد در تجزیه و تحلیل قالب ها آورده می شود.

مبادلات

نقطه تبادل یک فرآیند پیام رسانی است که منطق تعامل با اجزای درون قالب پیام را پیاده سازی می کند. در تمام مثال‌های زیر، اجزا از طریق نقاط مبادله با هم تعامل دارند که ترکیب آنها پیام‌رسانی را تشکیل می‌دهد.

الگوهای تبادل پیام (MEP)

در سطح جهانی، الگوهای مبادله را می توان به دو طرفه و یک طرفه تقسیم کرد. اولی به معنای پاسخ به پیام دریافتی است، دومی نه. یک مثال کلاسیک از یک الگوی دو طرفه در معماری مشتری-سرور، الگوی درخواست پاسخ است. قالب و تغییرات آن را در نظر بگیرید.

درخواست پاسخ یا RPC

RPC زمانی استفاده می شود که نیاز به دریافت پاسخ از فرآیند دیگری داشته باشیم. این فرآیند می تواند در یک میزبان یا در یک قاره متفاوت اجرا شود. در زیر نموداری از تعامل بین کلاینت و سرور از طریق پیام رسانی است.

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

از آنجایی که پیام رسانی کاملا ناهمزمان است، تبادل برای مشتری به 2 مرحله تقسیم می شود:

  1. در حال ارسال درخواست

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

    تبادل ‒ نام نقطه مبادله منحصر به فرد
    ResponseMatchingTag ‒ برچسب محلی برای پردازش پاسخ. به عنوان مثال، در مورد ارسال چندین درخواست یکسان متعلق به کاربران مختلف.
    تعریف درخواست ‒ بدن درخواست
    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.

کد مشتری

برای ارسال درخواست به یک سرویس، می‌توانید با API درخواست پیام در هر نقطه از مشتری تماس بگیرید:

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

در یک سیستم توزیع‌شده، پیکربندی اجزا می‌تواند بسیار متفاوت باشد و در زمان درخواست، ممکن است پیام‌رسانی هنوز شروع نشود یا کنترل‌کننده سرویس برای ارائه درخواست آماده نباشد. بنابراین، ما باید پاسخ پیام را بررسی کنیم و پرونده شکست را رسیدگی کنیم.
پس از ارسال موفقیت آمیز به مشتری، سرویس پاسخ یا خطا دریافت می کند.
بیایید هر دو مورد را در 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};

پاسخ تکه تکه درخواست

بهتر است از ارسال پیام های بزرگ خودداری کنید. پاسخگویی و عملکرد پایدار کل سیستم به این بستگی دارد. اگر پاسخ به یک پرس و جو حافظه زیادی را اشغال کند، تقسیم کردن اجباری است.

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

در اینجا چند نمونه از این موارد وجود دارد:

  • کامپوننت ها داده های باینری مانند فایل ها را مبادله می کنند. شکستن پاسخ به بخش‌های کوچک کمک می‌کند تا با فایل‌هایی با هر اندازه‌ای کارآمد کار کنید و از سرریز حافظه جلوگیری کنید.
  • لیست ها به عنوان مثال، ما باید تمام رکوردها را از یک جدول بزرگ در پایگاه داده انتخاب کنیم و آن را به مؤلفه دیگری منتقل کنیم.

من چنین پاسخ هایی را لوکوموتیو می نامم. در هر صورت 1024 پیام 1 مگابایتی بهتر از یک پیام 1 گیگابایتی است.

در خوشه Erlang، ما یک مزیت اضافی دریافت می کنیم - کاهش بار در نقطه تبادل و شبکه، زیرا پاسخ ها بلافاصله با دور زدن نقطه مبادله به گیرنده ارسال می شوند.

پاسخ با درخواست

این یک تغییر نسبتاً نادر از الگوی RPC برای ساختن سیستم‌های مکالمه است.

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

انتشار-اشتراک (درخت توزیع داده)

سیستم های رویداد محور، داده ها را به محض آماده شدن به مصرف کنندگان تحویل می دهند. بنابراین، سیستم ها بیشتر مستعد مدل فشار هستند تا مدل کششی یا نظرسنجی. این ویژگی به شما این امکان را می دهد که با درخواست مداوم و انتظار برای داده، منابع را هدر ندهید.
شکل، روند توزیع یک پیام به مصرف کنندگانی که در یک موضوع خاص مشترک شده اند را نشان می دهد.

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

نمونه‌های کلاسیک استفاده از این الگو عبارتند از توزیع حالت: دنیای بازی در بازی‌های رایانه‌ای، داده‌های بازار در مبادلات، اطلاعات مفید در خوراک داده‌ها.

کد مشترک را در نظر بگیرید:

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).

تبادل - نام نقطه مبادله،
کلید ‒ کلید مسیریابی
پیام - ظرفیت ترابری

اشتراک وارونه انتشار

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

با استقرار pub-sub، می توانید الگویی را دریافت کنید که برای ورود به سیستم راحت است. مجموعه منابع و مصرف کنندگان می توانند کاملاً متفاوت باشند. شکل موردی را با یک مصرف کننده و چندین منبع نشان می دهد.

الگوی توزیع وظایف

تقریباً در هر پروژه، وظایف پردازش معوق وجود دارد، مانند تولید گزارش، ارائه اعلان‌ها و دریافت داده‌ها از سیستم‌های شخص ثالث. توان عملیاتی سیستمی که این وظایف را انجام می دهد به راحتی با افزودن پردازنده ها مقیاس می شود. تنها چیزی که برای ما باقی می ماند تشکیل خوشه ای از پردازنده ها و توزیع یکنواخت وظایف بین آنهاست.

موقعیت هایی را که با استفاده از مثال 3 کنترل کننده ایجاد می شود در نظر بگیرید. حتی در مرحله توزیع وظیفه، سوال عادلانه بودن توزیع و سرریز شدن گرداننده ها مطرح می شود. توزیع چرخشی مسئولیت انصاف را بر عهده خواهد داشت و به منظور جلوگیری از سرریز شدن گردانندگان، محدودیتی را در نظر خواهیم گرفت. prefetch_limit. در حالت های انتقالی prefetch_limit به یک کنترل کننده اجازه نمی دهد همه وظایف را دریافت کند.

پیام‌رسانی صف‌ها و اولویت پردازش را مدیریت می‌کند. پردازشگرها به محض ورود وظایف را دریافت می کنند. کار می تواند با موفقیت انجام شود یا شکست بخورد:

  • messaging:ack(Tack) ‒ در صورت پردازش موفقیت آمیز پیام تماس گرفته می شود
  • messaging:nack(Tack) - در تمام شرایط اضطراری تماس گرفت. پس از بازگشت کار، پیام آن را به کنترل کننده دیگری ارسال می کند.

بلوک های سازنده برنامه های کاربردی توزیع شده رویکرد اول

بیایید فرض کنیم که در حین پردازش سه کار، یک شکست پیچیده رخ داده است: کنترلر 1، پس از دریافت وظیفه، بدون داشتن زمان برای گزارش چیزی به نقطه تبادل، از کار افتاد. در این حالت، نقطه مبادله پس از اتمام زمان اعتبار، کار را به کنترل کننده دیگری منتقل می کند. Handler 3 به دلایلی این کار را رها کرد و یک ناک ارسال کرد، در نتیجه، این کار به کنترل کننده دیگری نیز منتقل شد که آن را با موفقیت انجام داد.

خلاصه مقدماتی

ما بلوک‌های اصلی سیستم‌های توزیع‌شده را تجزیه کرده‌ایم و درک اساسی از کاربرد آنها در Erlang/Elixir به دست آورده‌ایم.

با ترکیب الگوهای اساسی، می توان پارادایم های پیچیده ای را برای حل مشکلات نوظهور ساخت.

در بخش پایانی چرخه، به مسائل کلی سازماندهی سرویس ها، مسیریابی و تعادل می پردازیم و همچنین در مورد جنبه عملی مقیاس پذیری و تحمل خطا سیستم ها صحبت خواهیم کرد.

پایان قسمت دوم.

عکس ماریوس کریستنسن
تصاویر ارائه شده توسط websequencediagrams.com

منبع: www.habr.com

اضافه کردن نظر