البحث عن الاستخدام العالي لوحدة المعالجة المركزية وتحليلها في تطبيقات .NET
نشرت: 2022-03-11يمكن أن يكون تطوير البرامج عملية معقدة للغاية. نحن كمطورين نحتاج إلى مراعاة الكثير من المتغيرات المختلفة. بعضها ليس تحت سيطرتنا ، والبعض الآخر غير معروف لنا في لحظة تنفيذ الكود الفعلي ، وبعضها نتحكم فيه بشكل مباشر. ومطوري NET ليسوا استثناء من ذلك.
بالنظر إلى هذا الواقع ، عادة ما تسير الأمور كما هو مخطط لها عندما نعمل في بيئات خاضعة للرقابة. مثال على ذلك آلة التطوير الخاصة بنا ، أو بيئة التكامل التي لدينا وصول كامل إليها. في هذه المواقف ، لدينا أدوات لتحليل المتغيرات المختلفة التي تؤثر على التعليمات البرمجية والبرامج الخاصة بنا. في هذه الحالات ، لا يتعين علينا أيضًا التعامل مع الأحمال الثقيلة من الخادم ، أو المستخدمين المتزامنين الذين يحاولون القيام بنفس الشيء في نفس الوقت.
في المواقف الموصوفة والآمنة ، سيعمل الكود الخاص بنا بشكل جيد ، ولكن في الإنتاج تحت الحمل الثقيل أو بعض العوامل الخارجية الأخرى ، قد تحدث مشاكل غير متوقعة. من الصعب تحليل أداء البرمجيات في الإنتاج. في معظم الأوقات ، يتعين علينا التعامل مع المشكلات المحتملة في سيناريو نظري: نعلم أن المشكلة يمكن أن تحدث ، لكن لا يمكننا اختبارها. لهذا السبب نحتاج إلى بناء تطويرنا على أفضل الممارسات والتوثيق للغة التي نستخدمها ، وتجنب الأخطاء الشائعة.
كما ذكرنا ، عندما يتم تشغيل البرنامج ، يمكن أن تسوء الأمور ، ويمكن أن تبدأ التعليمات البرمجية في التنفيذ بطريقة لم نخطط لها. قد ينتهي بنا الأمر في الموقف عندما يتعين علينا التعامل مع المشكلات دون القدرة على تصحيح الأخطاء أو معرفة ما يحدث على وجه اليقين. ماذا يمكننا أن نفعل في هذه الحالة؟
سنقوم في هذه المقالة بتحليل سيناريو حالة حقيقية لاستخدام CPU عالي لتطبيق ويب .NET على الخادم المستند إلى Windows ، والعمليات المتضمنة لتحديد المشكلة ، والأهم من ذلك ، لماذا حدثت هذه المشكلة في المقام الأول وكيف نقوم بذلك. حلها.
يتم مناقشة استخدام وحدة المعالجة المركزية واستهلاك الذاكرة على نطاق واسع. عادةً ما يكون من الصعب جدًا معرفة الكمية المناسبة من الموارد (وحدة المعالجة المركزية ، ذاكرة الوصول العشوائي ، الإدخال / الإخراج) التي يجب أن تستخدمها عملية معينة ، ولأي فترة زمنية. على الرغم من أن هناك شيئًا واحدًا مؤكدًا - إذا كانت العملية تستخدم أكثر من 90٪ من وحدة المعالجة المركزية لفترة طويلة من الوقت ، فإننا نواجه مشكلة لمجرد حقيقة أن الخادم لن يكون قادرًا على معالجة أي طلب آخر في ظل هذه الظروف.
هل هذا يعني أن هناك مشكلة في العملية نفسها؟ ليس بالضرورة. قد تكون العملية بحاجة إلى مزيد من قوة المعالجة ، أو أنها تتعامل مع الكثير من البيانات. كبداية ، الشيء الوحيد الذي يمكننا القيام به هو محاولة تحديد سبب حدوث ذلك.
تحتوي جميع أنظمة التشغيل على العديد من الأدوات المختلفة لمراقبة ما يجري في الخادم. تحتوي خوادم Windows على وجه التحديد على مدير المهام أو مراقب الأداء أو في حالتنا استخدمنا New Relic Servers وهي أداة رائعة لمراقبة الخوادم.
الأعراض الأولى وتحليل المشكلة
بعد نشر تطبيقنا ، خلال فترة زمنية من الأسبوعين الأولين ، بدأنا نرى أن الخادم يحتوي على ذروة استخدام وحدة المعالجة المركزية ، مما جعل الخادم لا يستجيب. كان علينا إعادة تشغيله لإتاحته مرة أخرى ، وحدث هذا الحدث ثلاث مرات خلال هذا الإطار الزمني. كما ذكرت من قبل ، استخدمنا New Relic Servers كشاشة خادم ، وأظهر أن عملية w3wp.exe
كانت تستخدم 94٪ من وحدة المعالجة المركزية في وقت تعطل الخادم.
عملية العامل الخاصة بخدمات معلومات الإنترنت (IIS) هي عملية Windows ( w3wp.exe
) تقوم بتشغيل تطبيقات الويب ، وهي مسؤولة عن معالجة الطلبات المرسلة إلى خادم الويب لتجمع تطبيقات معين. يمكن أن يحتوي خادم IIS على العديد من تجمعات التطبيقات (والعديد من عمليات w3wp.exe
المختلفة) والتي يمكن أن تولد المشكلة. استنادًا إلى المستخدم الذي خضعت له العملية (تم عرض ذلك في تقارير New Relic) ، حددنا أن المشكلة تكمن في تطبيقنا القديم لنموذج الويب .NET C #.
تم دمج .NET Framework بإحكام مع أدوات تصحيح أخطاء Windows ، لذا فإن أول شيء حاولنا القيام به هو إلقاء نظرة على عارض الأحداث وملفات سجل التطبيق للعثور على بعض المعلومات المفيدة حول ما كان يحدث. سواء كان لدينا بعض الاستثناءات المسجلة في عارض الأحداث ، فإنها لم تقدم بيانات كافية لتحليلها. لهذا السبب قررنا اتخاذ خطوة إلى الأمام وجمع المزيد من البيانات ، لذلك عندما يظهر الحدث مرة أخرى سنكون مستعدين.
جمع البيانات
أسهل طريقة لتجميع عمليات تفريغ عملية وضع المستخدم هي باستخدام Debug Diagnostic Tools v2.0 أو ببساطة DebugDiag. يحتوي DebugDiag على مجموعة من الأدوات لجمع البيانات (مجموعة DebugDiag) وتحليل البيانات (تحليل DebugDiag).
لذلك ، لنبدأ في تحديد قواعد جمع البيانات باستخدام أدوات تشخيص التصحيح:
افتح مجموعة DebugDiag وحدد
Performance
.- حدد
Performance Counters
وانقر فوقNext
. - انقر فوق
Add Perf Triggers
. - قم بتوسيع
Processor
(وليسProcess
) الكائن وحدد% Processor Time
. لاحظ أنه إذا كنت تستخدم Windows Server 2008 R2 ولديك أكثر من 64 معالجًا ، فالرجاء اختيار كائنProcessor Information
بدلاً من كائنProcessor
. - في قائمة المثيلات ، حدد
_Total
. - انقر فوق
Add
ثم انقر فوقOK
. حدد المشغل المضاف حديثًا وانقر فوق
Edit Thresholds
.- حدد
Above
في القائمة المنسدلة. - قم بتغيير العتبة إلى
80
. أدخل
20
لعدد الثواني. يمكنك ضبط هذه القيمة إذا لزم الأمر ، ولكن احرص على عدم تحديد عدد قليل من الثواني لمنع المشغلات الخاطئة.- انقر فوق
OK
. - انقر فوق
Next
. - انقر فوق
Add Dump Target
. - حدد
Web Application Pool
من القائمة المنسدلة. - حدد تجمع التطبيقات الخاص بك من قائمة تجمعات التطبيقات.
- انقر فوق
OK
. - انقر فوق
Next
. - انقر فوق
Next
مرة أخرى. - أدخل اسمًا لقاعدتك إذا كنت ترغب في ذلك وقم بتدوين الموقع حيث سيتم حفظ مقالب النفايات. يمكنك تغيير هذا الموقع إذا رغبت في ذلك.
- انقر فوق
Next
. - حدد
Activate the Rule Now
وانقر فوقFinish
.
ستنشئ القاعدة الموصوفة مجموعة من ملفات التفريغ المصغر والتي ستكون صغيرة الحجم إلى حد ما. سيكون التفريغ النهائي عبارة عن تفريغ بذاكرة ممتلئة ، وستكون عمليات التفريغ هذه أكبر بكثير. الآن ، نحتاج فقط إلى انتظار حدوث حدث CPU عالي مرة أخرى.

بمجرد حصولنا على ملفات التفريغ في المجلد المحدد ، سنستخدم أداة تحليل DebugDiag لتحليل البيانات التي تم جمعها:
حدد محللات الأداء.
أضف ملفات التفريغ.
ابدأ التحليل.
سيستغرق DebugDiag بضع دقائق (أو عدة) لتحليل عمليات التفريغ وتقديم تحليل. عند اكتمال التحليل ، سترى صفحة ويب بها ملخص والكثير من المعلومات المتعلقة بسلاسل الرسائل ، على غرار الصفحة التالية:
كما ترى في الملخص ، هناك تحذير يقول "تم اكتشاف استخدام مرتفع لوحدة المعالجة المركزية بين ملفات التفريغ في واحد أو أكثر من مؤشرات الترابط." إذا نقرنا على التوصية ، فسنبدأ في فهم مكان المشكلة في تطبيقنا. يبدو تقرير المثال الخاص بنا كما يلي:
كما نرى في التقرير ، هناك نمط يتعلق باستخدام وحدة المعالجة المركزية. جميع سلاسل الرسائل التي تحتوي على استخدام عالٍ لوحدة المعالجة المركزية مرتبطة بنفس الفئة. قبل القفز إلى الكود ، دعنا نلقي نظرة على الأولى.
هذه هي تفاصيل الخيط الأول مع مشكلتنا. الجزء المثير للاهتمام بالنسبة لنا هو ما يلي:
هنا لدينا مكالمة إلى الكود الخاص بنا GameHub.OnDisconnected()
والذي تسبب في العملية الإشكالية ، ولكن قبل هذا الاستدعاء لدينا استدعائان في القاموس ، والتي قد تعطي فكرة عما يجري. دعنا نلقي نظرة على كود .NET لمعرفة ما تفعله هذه الطريقة:
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
من الواضح أن لدينا مشكلة هنا. قالت مجموعة الاستدعاءات في التقارير أن المشكلة كانت في قاموس ، وفي هذا الرمز نقوم بالوصول إلى قاموس ، وعلى وجه التحديد السطر الذي تسبب في حدوث المشكلة هو هذا:
if (onlineSessions.TryGetValue(userId, out connId))
هذا هو إعلان القاموس:
static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();
ما هي مشكلة كود NET هذا؟
يعرف كل من لديه خبرة في البرمجة كائنية التوجه أن المتغيرات الثابتة ستتم مشاركتها بواسطة جميع مثيلات هذه الفئة. دعنا نلقي نظرة أعمق على معنى ثابت في عالم .NET.
وفقًا لمواصفات .NET C #:
استخدم المُعدِّل الثابت للإعلان عن عضو ثابت ينتمي إلى النوع نفسه بدلاً من كائن معين.
هذا ما تنص عليه مواصفات .NET C # langunge فيما يتعلق بالفئات الثابتة والأعضاء:
كما هو الحال مع جميع أنواع الفئات ، يتم تحميل معلومات النوع لفئة ثابتة بواسطة .NET Framework وقت تشغيل اللغة العامة (CLR) عندما يتم تحميل البرنامج الذي يشير إلى الفئة. لا يمكن للبرنامج تحديد وقت تحميل الفصل بالضبط. ومع ذلك ، فمن المضمون تحميله وتهيئة حقوله واستدعاء مُنشئه الثابت قبل الإشارة إلى الفصل لأول مرة في برنامجك. يُطلق على المُنشئ الثابت مرة واحدة فقط ، وتبقى فئة ثابتة في الذاكرة طوال عمر مجال التطبيق الذي يوجد فيه برنامجك.
يمكن أن تحتوي الفئة غير الثابتة على طرق أو حقول أو خصائص أو أحداث ثابتة. يمكن استدعاء العضو الثابت في فئة حتى في حالة عدم إنشاء مثيل للفئة. يتم الوصول إلى العضو الثابت دائمًا من خلال اسم الفئة ، وليس اسم المثيل. توجد نسخة واحدة فقط من عضو ثابت ، بغض النظر عن عدد مثيلات الفئة التي تم إنشاؤها. لا يمكن للطرق والخصائص الثابتة الوصول إلى الحقول والأحداث غير الثابتة في نوعها المحتوي ، ولا يمكنها الوصول إلى متغير مثيل لأي كائن ما لم يتم تمريره صراحةً في معلمة أسلوب.
هذا يعني أن العناصر الثابتة تنتمي إلى النوع نفسه ، وليس الكائن. يتم تحميلها أيضًا في مجال التطبيق بواسطة CLR ، وبالتالي ينتمي الأعضاء الثابتون إلى العملية التي تستضيف التطبيق وليس سلاسل رسائل محددة.
بالنظر إلى حقيقة أن بيئة الويب هي بيئة متعددة مؤشرات الترابط ، لأن كل طلب عبارة عن سلسلة محادثات جديدة يتم إنتاجها بواسطة عملية w3wp.exe
؛ وبالنظر إلى أن الأعضاء الساكنين هم جزء من العملية ، فقد يكون لدينا سيناريو تحاول فيه عدة خيوط مختلفة الوصول إلى بيانات المتغيرات الثابتة (المشتركة بواسطة عدة خيوط) ، والتي قد تؤدي في النهاية إلى مشاكل تعدد مؤشرات الترابط.
تنص وثائق القاموس تحت أمان الخيط على ما يلي:
يمكن أن يدعم
Dictionary<TKey, TValue>
العديد من القراء في نفس الوقت ، طالما لم يتم تعديل المجموعة. ومع ذلك ، فإن التعداد من خلال مجموعة ليس في جوهره إجراءً آمنًا. في الحالة النادرة التي يتعامل فيها التعداد مع الوصول للكتابة ، يجب تأمين المجموعة أثناء عملية التعداد بأكملها. للسماح بالوصول إلى المجموعة بواسطة سلاسل رسائل متعددة للقراءة والكتابة ، يجب عليك تنفيذ المزامنة الخاصة بك.
هذا البيان يشرح لماذا قد يكون لدينا هذه المشكلة. بناءً على معلومات التفريغ ، كانت المشكلة في طريقة FindEntry في القاموس:
إذا نظرنا إلى تطبيق FindEntry في القاموس ، يمكننا أن نرى أن الطريقة تتكرر من خلال البنية الداخلية (المجموعات) من أجل العثور على القيمة.
لذا فإن كود .NET التالي يعدد المجموعة ، وهي ليست عملية آمنة لمؤشر الترابط.
public override Task OnDisconnected() { try { var userId = GetUserId(); string connId; if (onlineSessions.TryGetValue(userId, out connId)) onlineSessions.Remove(userId); } catch (Exception) { // ignored } return base.OnDisconnected(); }
خاتمة
كما رأينا في عمليات التفريغ ، هناك العديد من السلاسل التي تحاول تكرار وتعديل مورد مشترك (قاموس ثابت) في نفس الوقت ، مما تسبب في النهاية في دخول التكرار إلى حلقة لا نهائية ، مما تسبب في أن يستهلك الخيط أكثر من 90٪ من وحدة المعالجة المركزية .
هناك العديد من الحلول الممكنة لهذه المشكلة. أول ما قمنا بتطبيقه هو قفل ومزامنة الوصول إلى القاموس على حساب فقدان الأداء. كان الخادم يتعطل كل يوم في ذلك الوقت ، لذلك كنا بحاجة إلى إصلاح هذا في أسرع وقت ممكن. حتى لو لم يكن هذا هو الحل الأمثل ، فقد حل المشكلة.
ستكون الخطوة التالية في حل هذه المشكلة هي تحليل الكود وإيجاد الحل الأمثل لذلك. لإعادة بناء الكود يعد خيارًا: يمكن لفئة ConcurrentDictionary الجديدة أن تحل هذه المشكلة لأنها تقفل فقط على مستوى الحاوية مما سيحسن الأداء العام. على الرغم من أن هذه خطوة كبيرة ، وستكون هناك حاجة إلى مزيد من التحليل.