إن واجهة برمجة تطبيقات (API) تعمل بسلاسة في بيئة الاختبار (staging) قد تنهار في اللحظة التي يتدفق فيها الزوار الحقيقيون. نادراً ما يكون الفشل دراماتيكياً؛ بل هو "موت بألف طعنة". خيط معالجة (thread) محجوز هنا، واستعلام مكرر هناك. في ظل الأحمال الخفيفة، تختبئ هذه الإخفاقات الصغيرة، ولكن تحت ضغط بيئة الإنتاج (production)، تتراكم لتتحول إلى استنزاف لمجمع الخيوط (thread pool starvation)، واختناق في قاعدة البيانات، وأوقات استجابة تدمر ثقة المستخدم. لا يتعلق ضبط الأداء بإيجاد حل سحري واحد، بل يتعلق ببناء نظام تحترم فيه كل طبقة ندرة الخيوط (threads)، والذاكرة، واتصالات قاعدة البيانات. عندما تتعامل مع هذه الموارد على أنها محدودة، ستتوقف عن مجرد الاستجابة للانقطاعات وتبدأ في منع حدوثها.
تخلص من نمط Sync-over-Async قبل أن يقضي عليك
النمط الأكثر تدميراً في تطبيقات ASP.NET Core ذات الحركة المرورية العالية هو sync-over-async. تلاحظ ذلك عندما يستدعي شخص ما .Result أو .Wait() على دالة غير متزامنة (asynchronous method) لأنه يحتاج إلى القيمة فوراً ولا يريد إعادة هيكلة مكدس الاستدعاءات (call stack). هذا القرار يحجز خيط الاستدعاء (calling thread)؛ حيث يظل الخيط خاملاً، ينتظر عملاً يحدث بالفعل في مكان آخر، لكن وقت التشغيل (runtime) لا يمكنه إعادة استخدامه لطلب آخر.
عندما تقوم طلبات كافية بهذا الفعل، يحدث استنزاف لمجمع الخيوط (thread pool). قد يبدو رسم المعالج البياني (CPU graph) سليماً لأن المعالجات ليست مشغولة، ومع ذلك ينفجر زمن الاستجابة (latency). تتراكم الطلبات في قائمة الانتظار، بانتظار خيوط لن تتحرر أبداً. الحل ميكانيكي ولكنه يتطلب انضباطاً: استخدم await في كامل مكدس الاستدعاءات. إذا كانت الدالة تستدعي async API، فيجب أن تكون هي نفسها async. لا توجد طرق مختصرة هنا؛ فكل عائق متزامن (synchronous blockage) تزيله يمنحك مساحة إضافية للتعامل مع الحركة المرورية الحقيقية.
توقف عن إهدار العمل على العملاء المنفصلين
ينقطع اتصال العملاء، وتُغلق المتصفحات علامات التبويب، وتفقد تطبيقات الهاتف الإشارة. إذا لم يكن خادمك على علم برحيل العميل، فسيستمر في تشغيل استعلامات قاعدة البيانات، وتحليل JSON، واستهلاك الخيوط (threads) في استجابة لن يتلقاها أحد. قم بتمرير CancellationToken إلى كل عملية async تدعم ذلك؛ وهذا يشمل استعلامات Entity Framework، واستدعاءات HTTP باستخدام HttpClient ، وأي عمل خلفي طويل الأمد.
عندما ينقطع الاتصال، يقوم الرمز (token) بتفعيل الإلغاء، ويتوقف العمل فوراً. هذا يحافظ على دورات وحدة المعالجة المركزية (CPU cycles) لقاعدة البيانات ويعيد الخيوط إلى المجمع بشكل أسرع. إنه تغيير بسيط في توقيعات الدوال (method signatures) يؤتي ثماره تحت الضغط.
قم بتخزين ما لا يتغير مؤقتاً (Cache)
من المحتمل أن كتالوج المنتجات الخاص بك لا يتغير بين كل طلب وآخر، وبالتأكيد لا تتغير أعلام الإعدادات (configuration flags). ومع ذلك، تقوم العديد من واجهات برمجة التطبيقات (APIs) بالاتصال بقاعدة البيانات بشكل متكرر للحصول على نفس البيانات الثابتة. يتيح لك التخزين المؤقت للمخرجات (Output caching) في ASP.NET Core تخزين الاستجابات التي تم عرضها وتقديمها مباشرة من الذاكرة دون الحاجة إلى المساس بالمتحكمات (controllers) أو قاعدة البيانات مرة أخرى.
استخدم الإبطال القائم على الوسوم (tag-based invalidation) بعناية. عندما تقوم بتحديث منتج، قم بإبطال الوسم المرتبط بتلك الفئة أو العنصر فقط. لست بحاجة إلى مسح التخزين المؤقت بالكامل؛ فهذا يحافظ على نسبة عالية من نجاح التخزين المؤقت (cache hit ratio) ويقلل عدد استعلامات قاعدة البيانات.
أصلح طريقة الوصول إلى البيانات أولاً
تستهلك استدعاءات قاعدة البيانات الجزء الأكبر من وقت الطلب في معظم واجهات برمجة التطبيقات. قبل أن تقوم بتحسين أي شيء آخر، ابحث هنا.
إذا كنت تستعلم عن البيانات لعرضها فقط ولا تخطط لتحديثها أبداً، فقم بإضافة AsNoTracking() إلى استعلامات Entity Framework الخاصة بك. حيث يتخطى EF Core تتبع التغييرات وإنشاء اللقطات (snapshot creation)
