ممارسة استخدام نموذج الممثل في منصة الواجهة الخلفية لأبطال Quake

أواصل تحميل التقارير مع Pixonic DevGAMM Talks ، اجتماعنا لشهر سبتمبر لمطوري الأنظمة المحملة بشكل كبير. لقد تبادلوا الكثير من الخبرات والحالات ، واليوم أنشر نصًا لخطاب المطور الخلفي من Sabre Interactive Roman Rogozin. تحدث عن ممارسة تطبيق نموذج الممثل باستخدام مثال إدارة اللاعبين وحالاتهم (يمكن العثور على تقارير أخرى في نهاية المقالة ، القائمة مكملة).


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

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



في الوقت الحالي ، نستضيف خدماتنا في Azure. هناك بعض البدائل المثيرة للاهتمام للغاية التي لا نريد التخلي عنها ، مثل Table Storage و Cosmos DB (لكننا نحاول ألا نتحكم عليهم بشدة من أجل المشروع عبر الأنظمة الأساسية).

الآن أود أن أخبركم قليلاً عن نموذج الممثل. وبادئ ذي بدء ، ظهر كمبدأ منذ أكثر من 40 عامًا.



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

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

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



يتم عزل الممثلين عن بعضهم البعض ، والتواصل من خلال المكالمات غير المتزامنة ، وداخل أنفسهم ، الممثلون آمنون.

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



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

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

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



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

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



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



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

ثانيًا ، تتعامل أورليانز مع أعطال الخادم.



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

لإضفاء مزيد من الوضوح ، دعنا نلقي نظرة على مثال صغير لكيفية قراءة المستخدم لبعض حالاته.



قد تكون الدولة هي حالته الاقتصادية التي تخزن الدروع أو الأسلحة أو العملة أو أبطال ذلك المستخدم. من أجل الحصول على هذه الولايات ، اتصل بـ PublicUserService ، التي تتحول إلى أورليانز من أجل الولاية. ما يحدث: يرى أورليانز أنه لا يوجد مثل هذا الممثل (أي الحبوب) حتى الآن ، يقوم بإنشائه على خادم مجاني ، وتقرأ الحبوب حالتها من بعض متاجر المثابرة.

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

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



نفس التدفق على التحديث. عندما يريد العميل تحديث حالة ، فإنه سينقل هذه المسؤولية إلى الحبوب ، أي سيخبره: "امنح هذا المستخدم 10 ذهب" ، وترتفع الحبوب ، وتعالج هذه الحالة بنوع من منطق الأعمال داخل الحبوب. ثم يأتي تحديث ذاكرة التخزين المؤقت ، وإذا لزم الأمر ، فإن المثابرة في المثابرة.



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

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

مثال صغير لكيفية ظهوره.



كما قلت بالفعل ، تتكون الحبوب من نوع وبعض المفتاح (في هذه الحالة ، النوع هو IPlayerState ، المفتاح هو IGrainWithGuidKey ، مما يعني أنه هو الإرشاد). ولدينا واجهة ننفذها ، أي إرجاع GetStates بعض قائمة الحالات و ApplyState ، التي تنطبق بعض الحالات. عودة أساليب أورليانز المهمة. معنى ذلك: المهمة هي وعد يخبرنا أنه عندما تعود الدولة ، سيكون الوعد في حالة حل. لدينا أيضًا بعض PlayerState التي نحصل عليها مع GrainFactory. على سبيل المثال هنا نحصل على رابط ، ولا نعرف أي شيء عن الموقع الفعلي لهذه الحبوب. عند استدعاء GetStates ، ستقوم أورليانز برفع الحبوب لدينا ، وقراءة الحالة من متجر المثابرة في ذاكرتها ، وعندما تطبق ApplyState حالة جديدة ، فإنها ستقوم أيضًا بتحديث هذه الحالة في الذاكرة وفي المثابرة.

أرغب في تقديم مثال أكثر تعقيدًا قليلاً على البنية عالية المستوى لخدمة UserStates.



لدينا نوع من عميل اللعبة يحصل على حالاته من خلال OfferSevice. لدينا خدمة GameConfigurationService ، المسؤولة عن النموذج الاقتصادي لمجموعة من المستخدمين ، في هذه الحالة ، مستخدمنا. ولدينا عامل تغيير هذا النموذج الاقتصادي. وفقًا لذلك ، يطلب المستخدم من OfferSevice استلام حالاته. وتقوم OfferSevice بالفعل بالوصول إلى خدمة UserOrleans ، والتي تتكون من هذه الحبوب ، فهي ترفع حالة المستخدم هذه في ذاكرتها ، وربما تنفذ نوعًا من منطق الأعمال ، وتعيد البيانات مرة أخرى إلى المستخدم من خلال OfferService.

بشكل عام ، أود أن ألفت الانتباه إلى حقيقة أن أورليانز جيدة لقدرتها العالية على التوازي بسبب حقيقة أن الحبوب مستقلة عن بعضها البعض. ومن ناحية أخرى ، داخل graine ، لا نحتاج إلى استخدام بدائل المزامنة ، لأننا نعلم أن كل استدعاء لهذا graine سيكون متسقًا إلى حد ما.

هنا أود أن أعرض بعض المزالق لهذا النموذج.



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

المشكلة الثانية ليست واضحة - هذا هو ما يسمى رد الفعل المتسلسل. عندما يرفع المستخدم بعض الحبوب ، ويمكنه ، بدوره ، رفع الحبوب الأخرى ضمن النظام. كيف يحدث هذا: يتلقى المستخدم حالاته ، والمستخدم لديه أصدقاء ويتلقى حالة أصدقائه. وبالتالي ، فإن النظام بأكمله يحافظ على جميع حبوبه في الذاكرة ، وإذا كان لدينا 1000 مستخدم ، ولكل منهم 100 صديق ، فإن 100.000 حبة يمكن أن تكون نشطة بهذه الطريقة. يجب أيضًا تجنب هذه الحالة - تخزين حالات الأصدقاء بطريقة أو بأخرى في نوع من الذاكرة المشتركة.



حسنا ، ما هي التقنيات الموجودة لتنفيذ نموذج الممثل. ولعل أشهرها عكا ، التي أتت إلينا بجاوة. هناك شوكة تسمى Akka.NET for .NET. هناك أورليانز ، وهي مفتوحة المصدر ومتوفرة بلغات أخرى ، كتطبيق. هناك بدائل Azure مثل Service Fabric Actor - هناك الكثير من التقنيات.

أسئلة من الجمهور


- كيف تحل المشاكل الكلاسيكية مثل CICD ، تحديث هؤلاء الممثلين ، هل تستخدم Docker وهل هناك حاجة إليه على الإطلاق؟

- نحن لا نستخدم عامل الميناء حتى الآن. بشكل عام ، يشارك DevOps في النشر ؛ ينشرون خدماتنا في خدمة Azure السحابية.

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

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

"كيف يفهم أورليانز أن الخادم تعطل؟" قلت أنه يرمي الممثلين بسرعة إلى خادم آخر ...

- لديه pingator يفهم بشكل دوري أي من الخوادم على قيد الحياة.

- هل ping ممثل أو خادم على وجه التحديد؟

- على وجه التحديد ، الخادم.

- مثل هذا السؤال: حدث خطأ داخل الممثل ، تقول أنه يذهب خطوة بخطوة ، كل تعليمات. لكن حدث خطأ وماذا حدث للممثل؟ افترض خطأ لم تتم معالجته. هل الممثل يموت فقط؟

- لا ، تطرح أورليانز استثناءً في مخطط .NET القياسي.

- انظر ، لم نتعامل مع الاستثناء ، على ما يبدو مات الممثل. اللاعب الذي لا أعرف كيف سيبدو ، ولكن ماذا سيحدث بعد ذلك؟ هل تحاول إعادة تشغيل هذا الممثل بطريقة أو بأخرى أو القيام بشيء آخر مثل هذا؟

- يعتمد ذلك على الحالة التي يعتمد عليها. على سبيل المثال يمكن استرجاعها أو لا يمكن استرجاعها.

- على سبيل المثال هل كل هذا قابل للتكوين؟

- بدلا من ذلك ، مبرمجة. نحن نتعامل مع بعض الاستثناءات. على سبيل المثال نرى بوضوح أن رمز الخطأ هذا ، وبعضها ، مثل الاستثناءات غير المعالجة ، تم دفعه بالفعل أكثر.

- هل لديك عدة مثابرات - هل هي مثل قاعدة بيانات؟

- المثابرة ، نعم ، قاعدة بيانات مع التخزين المستمر.

- لنفترض أن قاعدة بيانات قد حددت فيها (بشكل مشروط) أموال اللعبة. ماذا يحدث إذا لم يتمكن الممثل من الوصول إليها؟ كيف تتعامل معها؟

- أولاً ، التخزين. في الوقت الحالي ، نستخدم Azure Table Storage ، وتحدث مثل هذه المشاكل بالفعل - تعطل التخزين. عادة في هذه الحالة عليك إعادة تكوينه.

- إذا لم يتمكن الممثل من الحصول على شيء في التخزين ، كيف يبدو اللاعب؟ هو ببساطة ليس لديه هذا المال أو أنه أغلق اللعبة على الفور؟

- هذه تغييرات حاسمة للمستخدم. نظرًا لأن كل خدمة لها شدتها الخاصة ، في هذه الحالة ، تكون خدمة المستخدم حالة طرفية ، ويتعطل العميل فقط.

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

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

- وكيف سيؤثر ذلك على اللاعب؟

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

- على سبيل المثال هل لديك تهديد DDoS؟ يمكن لعدد كبير من الإجراءات الصغيرة وضع لاعب؟ لنفترض أن شخصًا ما بدأ بسرعة في دعوة الأصدقاء ، إلخ.

- لا ، هناك محدد للطلب لن يسمح لك بالوصول إلى الخدمات كثيرًا.

- كيف تتعامل مع تناسق البيانات؟ لنفترض أن لدينا اثنين من المستخدمين ، نحتاج إلى أخذ شيء من أحدهما ، وفرض رسوم على الآخر ، بحيث تكون المعاملات.

- سؤال جيد. أولاً ، تدعم شركة Orleans 2.0 معاملة الممثل الموزع - هذا هو الإصدار الأول. بتعبير أدق ، من الضروري بالفعل التحدث عن الاقتصاد. وكأسهل طريقة - في آخر أورليانز ، يتم تنفيذ المعاملات بين الجهات الفاعلة دون مشاكل.

- على سبيل المثال هل تعرف بالفعل كيفية ضمان استمرار البيانات في النزاهة؟

- نعم.

المزيد من المحادثات مع محادثات Pixonic DevGAMM


Source: https://habr.com/ru/post/ar426115/


All Articles