اللبنات الأساسية للتطبيقات الموزعة. النهج الأول

اللبنات الأساسية للتطبيقات الموزعة. النهج الأول

في الماضي مقالة درسنا الأسس النظرية للهندسة التفاعلية. حان الوقت للحديث عن تدفقات البيانات، وطرق تنفيذ أنظمة Erlang/Elixir التفاعلية وأنماط المراسلة فيها:

  • استجابة للطلب
  • استجابة مقسمة للطلب
  • الرد مع الطلب
  • نشر الاشتراك
  • مقلوب نشر الاشتراك
  • توزيع المهام

SOA وMSA والمراسلة

SOA وMSA عبارة عن بنيات نظام تحدد قواعد بناء الأنظمة، بينما توفر المراسلة أساسيات لتنفيذها.

لا أريد الترويج لبنية النظام هذه أو تلك. أنا أؤيد استخدام الممارسات الأكثر فعالية وإفادة لمشروع أو عمل محدد. مهما كان النموذج الذي نختاره، فمن الأفضل إنشاء كتل النظام مع التركيز على طريقة يونكس: مكونات ذات الحد الأدنى من الاتصال، مسؤولة عن الكيانات الفردية. تقوم أساليب واجهة برمجة التطبيقات (API) بتنفيذ أبسط الإجراءات الممكنة مع الكيانات.

المراسلة هي، كما يوحي اسمها، وسيط رسائل. والغرض الرئيسي منه هو تلقي وإرسال الرسائل. وهي مسؤولة عن واجهات إرسال المعلومات، وتشكيل القنوات المنطقية لنقل المعلومات داخل النظام، والتوجيه والموازنة، وكذلك معالجة الأخطاء على مستوى النظام.
إن الرسائل التي نقوم بتطويرها لا تحاول التنافس مع Rabbitmq أو استبداله. ميزاته الرئيسية:

  • توزيع.
    يمكن إنشاء نقاط التبادل على جميع عقد المجموعة، في أقرب وقت ممكن من الكود الذي يستخدمها.
  • البساطة.
    ركز على تقليل التعليمات البرمجية النمطية وسهولة الاستخدام.
  • أفضل أداء.
    نحن لا نحاول تكرار وظيفة Rabbitmq، ولكننا نسلط الضوء فقط على الطبقة المعمارية وطبقة النقل، والتي ندمجها في OTP ببساطة قدر الإمكان، مما يقلل من التكاليف.
  • المرونة.
    يمكن لكل خدمة الجمع بين العديد من قوالب التبادل.
  • المرونة حسب التصميم.
  • قابلية التوسع.
    الرسائل تنمو مع التطبيق. مع زيادة الحمل، يمكنك نقل نقاط التبادل إلى الأجهزة الفردية.

تعليق. فيما يتعلق بتنظيم التعليمات البرمجية، تعد المشاريع الوصفية مناسبة تمامًا لأنظمة Erlang/Elixir المعقدة. كل كود المشروع موجود في مستودع واحد - مشروع شامل. في الوقت نفسه، يتم عزل الخدمات الصغيرة إلى الحد الأقصى وتنفيذ عمليات بسيطة مسؤولة عن كيان منفصل. باستخدام هذا النهج، من السهل الحفاظ على واجهة برمجة التطبيقات (API) للنظام بأكمله، ومن السهل إجراء التغييرات، ومن السهل كتابة اختبارات الوحدة والتكامل.

تتفاعل مكونات النظام بشكل مباشر أو من خلال وسيط. من منظور الرسائل، كل خدمة لها عدة مراحل في الحياة:

  • تهيئة الخدمة.
    في هذه المرحلة، يتم تكوين وإطلاق العملية والتبعيات التي تنفذ الخدمة.
  • إنشاء نقطة تبادل.
    يمكن للخدمة استخدام نقطة تبادل ثابتة محددة في تكوين العقدة، أو إنشاء نقاط تبادل ديناميكيًا.
  • تسجيل الخدمة.
    لكي تتمكن الخدمة من تلبية الطلبات، يجب التسجيل في نقطة الصرف.
  • الأداء الطبيعي.
    تنتج الخدمة عملاً مفيدًا.
  • اغلق.
    هناك نوعان ممكنان من إيقاف التشغيل: عادي وطارئ. أثناء التشغيل العادي، يتم فصل الخدمة عن نقطة التبادل وتتوقف. في حالات الطوارئ، تقوم المراسلة بتنفيذ أحد البرامج النصية لتجاوز الفشل.

يبدو الأمر معقدًا للغاية، لكن الكود ليس مخيفًا إلى هذا الحد. سيتم تقديم أمثلة التعليمات البرمجية مع التعليقات في تحليل القوالب بعد ذلك بقليل.

الاستبدال

نقطة التبادل هي عملية مراسلة تنفذ منطق التفاعل مع المكونات داخل قالب المراسلة. في جميع الأمثلة المعروضة أدناه، تتفاعل المكونات من خلال نقاط التبادل، والتي يشكل الجمع بينها الرسائل.

أنماط تبادل الرسائل (MEPs)

على الصعيد العالمي، يمكن تقسيم أنماط التبادل إلى اتجاهين وأحادي الاتجاه. الأول يعني الرد على رسالة واردة، والثاني لا يفعل ذلك. المثال الكلاسيكي للنمط ثنائي الاتجاه في بنية خادم العميل هو نمط الطلب والاستجابة. دعونا نلقي نظرة على القالب وتعديلاته.

استجابة الطلب أو RPC

يتم استخدام RPC عندما نحتاج إلى تلقي استجابة من عملية أخرى. قد تكون هذه العملية قيد التشغيل على نفس العقدة أو موجودة في قارة مختلفة. يوجد أدناه رسم تخطيطي للتفاعل بين العميل والخادم عبر المراسلة.

اللبنات الأساسية للتطبيقات الموزعة. النهج الأول

نظرًا لأن المراسلة غير متزامنة تمامًا، فإن التبادل ينقسم بالنسبة للعميل إلى مرحلتين:

  1. إرسال الطلب

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

    البورصة - الاسم الفريد لنقطة التبادل
    علامة مطابقة الاستجابة - التسمية المحلية لمعالجة الاستجابة. على سبيل المثال، في حالة إرسال عدة طلبات متطابقة تخص مستخدمين مختلفين.
    تعريف الطلب - هيئة الطلب
    HandlerProcess - معرف PID للمعالج. ستتلقى هذه العملية استجابة من الخادم.

  2. معالجة الرد

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

    استجابة الحمولة - استجابة الخادم.

بالنسبة للخادم، تتكون العملية أيضًا من مرحلتين:

  1. تهيئة نقطة التبادل
  2. معالجة الطلبات المستلمة

دعونا نوضح هذا القالب بالكود. لنفترض أننا بحاجة إلى تنفيذ خدمة بسيطة توفر طريقة زمنية محددة واحدة.

كود الخادم

لنحدد واجهة برمجة تطبيقات الخدمة في 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.

رمز العميل

لإرسال طلب إلى الخدمة، يمكنك الاتصال بواجهة برمجة تطبيقات طلب المراسلة في أي مكان في العميل:

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، يمكنك الحصول على نمط مناسب للتسجيل. يمكن أن تكون مجموعة المصادر والمستهلكين مختلفة تمامًا. يوضح الشكل حالة مع مستهلك واحد ومصادر متعددة.

نمط توزيع المهام

يتضمن كل مشروع تقريبًا مهام معالجة مؤجلة، مثل إنشاء التقارير وتسليم الإشعارات واسترداد البيانات من أنظمة الطرف الثالث. يمكن بسهولة قياس إنتاجية النظام الذي يؤدي هذه المهام عن طريق إضافة معالجات. كل ما تبقى لنا هو تشكيل مجموعة من المعالجات وتوزيع المهام بينهم بالتساوي.

دعونا نلقي نظرة على المواقف التي تنشأ باستخدام مثال المعالجات الثلاثة. حتى في مرحلة توزيع المهام، تنشأ مسألة عدالة التوزيع وتجاوز المعالجين. سيكون التوزيع الدوري مسؤولاً عن العدالة، ولتجنب حالة تجاوز عدد المعالجين، سنفرض قيودًا الجلب المسبق_الحد. في ظروف عابرة الجلب المسبق_الحد سيمنع معالجًا واحدًا من تلقي جميع المهام.

تدير المراسلة قوائم الانتظار وأولوية المعالجة. تتلقى المعالجات المهام فور وصولها. يمكن أن تكتمل المهمة بنجاح أو تفشل:

  • messaging:ack(Tack) - يتم الاتصال به إذا تمت معالجة الرسالة بنجاح
  • messaging:nack(Tack) - يتم استدعاؤه في جميع حالات الطوارئ. بمجرد إرجاع المهمة، ستقوم المراسلة بتمريرها إلى معالج آخر.

اللبنات الأساسية للتطبيقات الموزعة. النهج الأول

لنفترض أن فشلًا معقدًا قد حدث أثناء معالجة ثلاث مهام: المعالج 1، بعد استلام المهمة، تعطل دون أن يكون لديه الوقت لإبلاغ نقطة التبادل بأي شيء. في هذه الحالة، ستقوم نقطة التبادل بنقل المهمة إلى معالج آخر بعد انتهاء مهلة الاستلام. لسبب ما، تخلى المعالج 3 عن المهمة وأرسل nack؛ ونتيجة لذلك، تم نقل المهمة أيضًا إلى معالج آخر أكملها بنجاح.

نتيجة أولية

لقد قمنا بتغطية اللبنات الأساسية للأنظمة الموزعة واكتسبنا فهمًا أساسيًا لاستخدامها في Erlang/Elixir.

ومن خلال الجمع بين الأنماط الأساسية، يمكنك بناء نماذج معقدة لحل المشكلات الناشئة.

في الجزء الأخير من السلسلة، سنلقي نظرة على القضايا العامة المتعلقة بتنظيم الخدمات والتوجيه والموازنة، وسنتحدث أيضًا عن الجانب العملي لقابلية التوسع والتسامح مع الأخطاء في الأنظمة.

نهاية الجزء الثاني.

صور ماريوس كريستنسن
الرسوم التوضيحية المعدة باستخدام websequencediagrams.com

المصدر: www.habr.com

إضافة تعليق