مضاد إنشاء مثيل غير صحيح

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

وصف المشكلة

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

  • System.Net.Http.HttpClient. يتواصل مع خدمة ويب باستخدام HTTP.
  • Microsoft.ServiceBus.Messaging.QueueClient. نشر الرسائل وتلقيها إلى قائمة انتظار ناقل خدمة Microsoft Azure.
  • Microsoft.Azure.Documents.Client.DocumentClient. الاتصال إلى مثيل Azure Cosmos DB.
  • StackExchange.Redis.ConnectionMultiplexer. يتصل بـ Redis، بما في ذلك ذاكرة التخزين المؤقت Azure لـ Redis.

تهدف هذه الفئات إلى إنشاء مثيل لها مرة واحدة وإعادة استخدامها طوال عمر التطبيق. ومع ذلك، فمن سوء الفهم الشائع أنه يجب الحصول على هذه الفئات فقط حسب الضرورة وإطلاقها بسرعة. (تلك المدرجة هنا هي مكتبات NET.، ولكن النمط ليس فريداً من نوعه لـ NET.) ينشئ المثال ASP.NET التالي مثيلا HttpClient للاتصال بخدمة بعيدة. يمكنك العثور على النموذج الكامل هنا.

public class NewHttpClientInstancePerRequestController : ApiController
{
    // This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        using (var httpClient = new HttpClient())
        {
            var hostName = HttpContext.Current.Request.Url.Host;
            var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
            return new Product { Name = result };
        }
    }
}

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

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

public class NewServiceInstancePerRequestController : ApiController
{
    public async Task<Product> GetProductAsync(string id)
    {
        var expensiveToCreateService = new ExpensiveToCreateService();
        return await expensiveToCreateService.GetProductByIdAsync(id);
    }
}

public class ExpensiveToCreateService
{
    public ExpensiveToCreateService()
    {
        // Simulate delay due to setup and configuration of ExpensiveToCreateService
        Thread.SpinWait(Int32.MaxValue / 100);
    }
    ...
}

كيفية إصلاح مضادات إنشاء مثيل غير صحيحة

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

يستخدم المثال التالي مثيل HttpClient ثابتاً، وبالتالي يشارك الاتصال عبر جميع الطلبات.

public class SingleHttpClientInstanceController : ApiController
{
    private static readonly HttpClient httpClient;

    static SingleHttpClientInstanceController()
    {
        httpClient = new HttpClient();
    }

    // This method uses the shared instance of HttpClient for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        var hostName = HttpContext.Current.Request.Url.Host;
        var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
        return new Product { Name = result };
    }
}

الاعتبارات

  • يتمثل العنصر الأساسي للنمط المضاد هذا في إنشاء مثيلات وإتلافها بشكل متكرر لعنصر قابل للمشاركة. إذا لم يكن الفصل قابلاً للمشاركة (غير آمن للخيط)، فلن يتم تطبيق هذا النمط المضاد.

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

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

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

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

  • في NET Framework. يتم إنشاء العديد من العناصر التي تنشئ اتصالات بالموارد الخارجية باستخدام أساليب المصنع الثابت للفئات الأخرى التي تدير هذه الاتصالات. تهدف هذه العناصر إلى حفظها وإعادة استخدامها، بدلاً من التخلص منها وإعادة إنشائها. على سبيل المثال، في ناقل خدمة Microsoft Azure، يتم إنشاء العنصر QueueClient من خلال عنصر MessagingFactory. داخلياً، يدير MessagingFactory الاتصالات. لمزيد من المعلومات، راجع أفضل الممارسات لتحسين الأداء باستخدام ناقل خدمة Microsoft Azure Messaging .

كيفية الكشف عن Antipattern غير الصحيح

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

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

يمكنك متابعة الخطوات التالية للمساعدة في تحديد هذه المشكلة:

  1. أداء عملية مراقبة نظام الإنتاج، لتحديد النقاط عندما تتباطأ أوقات الاستجابة أو يفشل النظام بسبب نقص الموارد.
  2. افحص بيانات القياس عن بعد التي تم التقاطها في هذه النقاط لتحديد العمليات التي قد تقوم بإنشاء عناصر مستهلكة للموارد وتدميرها.
  3. اختبار التحميل لكل عملية مشتبه بها، في بيئة اختبار خاضعة للرقابة بدلاً من نظام الإنتاج.
  4. راجع التعليمة البرمجية المصدر وافحص كيفية إدارة عناصر الوسيط.

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

مثال التشخيص

تطبق الأقسام التالية هذه الخطوات على نموذج التطبيق الموصوف سابقاً.

تحديد نقاط التباطؤ أو الفشل

تعرض الصورة التالية النتائج التي تم إنشاؤها باستخدام New Relic APM، وتعرض العمليات التي لها وقت استجابة ضعيف. في هذه الحالة، GetProductAsync الأسلوب في NewHttpClientInstancePerRequest وحدة التحكم يستحق التحقيق بشكل أكبر. لاحظ أن معدل الخطأ يزيد أيضاً عند تشغيل هذه العمليات.

The New Relic monitor dashboard showing the sample application creating a new instance of an HttpClient object for each request

فحص بيانات تتبع الاستخدام والبحث عـن الارتباطات

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

The New Relic thread profiler showing the sample application creating a new instance of an HttpClient object for each request

إجراء اختبار التحميل

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

Throughput of the sample application creating a new instance of an HttpClient object for each request

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

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

يعرض الرسم البياني التالي اختباراً مشابهاً لوحدة التحكم التي تقوم بإنشاء عنصر ExpensiveToCreateService المخصص.

Throughput of the sample application creating a new instance of the ExpensiveToCreateService for each request

هذه المرة، لا تُنشئ وحدة التحكم أي استثناءات، ولكن لا يزال معدل النقل يصل إلى مستوى ثابت، بينما يزيد متوسط ​​وقت الاستجابة بعامل 20. (يستخدم الرسم البياني مقياس لوغاريتمي لوقت الاستجابة ومعدل النقل.) أظهرت بيانات تتبع الاستخدام أن إنشاء مثيلات جديدة من ExpensiveToCreateService هو السبب الرئيسي للمشكلة.

نفذ الحل وتحقق من النتيجة

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

Throughput of the sample application reusing the same instance of an HttpClient object for each request

للمقارنة، تُظهر الصورة التالية تتبع المكدس. هذه المرة، يقضي النظام معظم وقته في أداء عمل حقيقي، بدلاً من فتح وإغلاق مآخذ التوصيل.

The New Relic thread profiler showing the sample application creating single instance of an HttpClient object for all requests

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

Graph showing a similar load test using a shared instance of the ExpensiveToCreateService object.