یک API که در محیط staging به خوبی کار میکند، میتواند درست در لحظهی برخورد با ترافیک واقعی از هم بپاشد. این شکست به ندرت دراماتیک است؛ بلکه مرگ با هزار جراحت کوچک است. یک ترد (thread) مسدود شده در اینجا، یک کوئری (query) اضافی در آنجا. تحت بار کم، این ناکارآمدیهای کوچک پنهان میمانند. اما تحت فشار محیط production، آنها به مشکلاتی چون گرسنگی استخر ترد (thread pool starvation)، خفگی پایگاه داده و زمان پاسخدهیهایی تبدیل میشوند که اعتماد کاربر را از بین میبرند. بهینهسازی عملکرد (Performance tuning) به معنای یافتن یک راهکار جادویی نیست؛ بلکه به معنای ساخت سیستمی است که در آن هر لایه به محدودیت منابع تردها، حافظه و اتصالات پایگاه داده احترام میگذارد. وقتی با این منابع به عنوان منابع محدود برخورد میکنید، از حالت واکنش به قطعیها خارج شده و شروع به پیشگیری از آنها میکنید.
قبل از اینکه Sync-over-Async شما را از پا درآورد، آن را از بین ببرید
مخربترین الگوی موجود در اپلیکیشنهای ASP.NET Core با ترافیک بالا، الگوی sync-over-async است. این الگو زمانی دیده میشود که کسی متد .Result یا .Wait() را روی یک متد ناهمگام (asynchronous) فراخوانی میکند، چون به مقدار آن در همان لحظه نیاز دارد و نمیخواهد Call Stack را بازنویسی (refactor) کند. این تصمیم باعث مسدود شدن تردِ فراخوان میشود. ترد در حالت بیکاری میماند و منتظر کاری است که در جای دیگری در حال انجام است، اما Runtime نمیتواند از آن برای درخواست دیگری استفاده کند.
وقتی درخواستهای کافی این کار را انجام دهند، استخر ترد دچار گرسنگی (starvation) میشود. نمودار CPU شما سالم به نظر میرسد چون پردازندهها مشغول نیستند، اما تأخیر (latency) شما به شدت بالا میرود. درخواستها در صف میمانند و منتظر تردهایی هستند که هرگز آزاد نخواهند شد. راه حل مکانیکی است اما نیاز به انضباط دارد: از await در تمام طول Call Stack استفاده کنید. اگر متدی یک API ناهمگام را فراخوانی میکند، خودش نیز باید ناهمگام باشد. هیچ میانبری در اینجا وجود ندارد. هر مسدودسازی همگام (synchronous) که حذف کنید، فضای بیشتری برای ترافیک واقعی آزاد میکند.
از هدر دادن منابع برای کلاینتهای قطع شده دست بردارید
کلاینتها قطع میشوند. مرورگرها تبها را میبندند. اپلیکیشنهای موبایل سیگنال را از دست میدهند. اگر سرور شما نداند که کلاینت قطع شده است، به اجرای کوئریهای پایگاه داده، تجزیه (parsing) JSON و هدر دادن تردها برای پاسخی که هیچکس دریافت نخواهد کرد، ادامه میدهد. یک CancellationToken را به هر عملیات ناهمگامی که از آن پشتیبانی میکند، پاس بدهید. این شامل کوئریهای Entity Framework، فراخوانیهای HTTP با HttpClient و هر کار پسزمینه طولانیمدتی میشود.
وقتی اتصال قطع میشود، توکن باعث لغو عملیات شده و کار بلافاصله متوقف میشود. این کار باعث حفظ چرخههای CPU پایگاه داده شده و تردها را سریعتر به استخر بازمیگرداند. این یک تغییر کوچک در امضای متدها (method signatures) است که تحت بار زیاد، نتایج درخشانی دارد.
آنچه تغییر نمیکند را کش کنید
کاتالوگ محصولات شما احتمالاً بین هر درخواست تغییر نمیکند. پرچمهای پیکربندی (configuration flags) شما که قطعاً تغییر نمیکنند. با این حال، بسیاری از APIها برای دریافت دادههای استاتیک یکسان، مکرراً به پایگاه داده درخواست میفرستند. قابلیت Output caching در ASP.NET Core به شما اجازه میدهد پاسخهای رندر شده را ذخیره کنید و آنها را مستقیماً از حافظه ارائه دهید، بدون اینکه دوباره به Controllerها یا پایگاه داده مراجعه کنید.
از ابطال مبتنی بر تگ (tag-based invalidation) با دقت استفاده کنید. وقتی محصولی را بهروزرسانی میکنید، فقط تگ مرتبط با آن دسته یا آیتم را باطل کنید. نیازی نیست کل کش را پاک کنید. این کار باعث میشود نرخ برخورد با کش (cache hit ratio) بالا و تعداد کوئریهای پایگاه داده شما پایین بماند.
ابتدا دسترسی به دادهها را اصلاح کنید
فراخوانیهای پایگاه داده در اکثر APIها بخش اعظم زمان درخواست را مصرف میکنند. قبل از اینکه هر چیز دیگری را بهینه کنید، اینجا را بررسی کنید.
اگر دادهها را فقط برای نمایش کوئری میکنید و هرگز قصد بهروزرسانی آنها را ندارید، عبارت AsNoTracking() را به کوئریهای Entity Framework خود اضافه کنید. EF Core از فرآیند ردیابی تغییرات (change tracking) و ایجاد اسنپشات (snapshot creation) صرفنظر میکند.
