این مقاله بهطور تخصصی (تا حدود ۱۸۰۰ کلمه) به بررسی ریشه مشکل، دلایل فنی تداخل (thread pool starvation، فشار حافظه، connection leak)، علائم در لاگها و راهحلهای عملی میپردازد. از تجربیات واقعی، مستندات مایکروسافت و بهترین شیوههای ۲۰۲۵–۲۰۲۶ استفاده شده است.
اپلیکیشن در بهروزرسانی خبر (news update service) بعد از ذخیره تغییرات، سایتمپ را بازسازی میکرد:
await sitemapFacade.CreateOrUpdateSitemap();
var output = await dbContext.SaveChangesAsync();
ابتدا با .Take(10) کار میکرد، اما با داده کامل (۴۰۰۰+ رکورد خبر) کرش میکرد. حتی با Dapper + stored procedure، یکبار کار میکرد و بعد متوقف میشد.
لاگهای کلیدی (از Output window یا Application Insights):
فایل سایتمپ خالی میماند () یا اصلاً ذخیره نمیشد. هیچ exception واضحی در try-catch اصلی دیده نمیشد، چون مشکل در worker thread رخ میداد.
لاگهای EF نشاندهنده اجرای دستورات عادی بود (CommandTimeout=۳۰ ثانیه پیشفرض). سپس thread pool workerها ناگهان خارج میشدند. این الگو کلاسیک است:
در مقاله Microsoft Learn (Debug ThreadPool Starvation) تأکید شده که sync-over-async یا عملیات طولانی synchronous روی thread pool، اصلیترین دلیل است.
الف) Thread Pool Starvation در ASP.NET Core
ASP.NET Core از thread pool مشترک برای مدیریت درخواستها استفاده میکند (Kestrel + worker threads). هر درخواست یک thread میگیرد. اگر عملیاتی synchronous و طولانی (مانند لود ۴۰۰۰ رکورد + ساخت XDocument در حافظه) روی همان thread اجرا شود:
مستندات بهترین شیوههای ASP.NET Core (۲۰۲۵): «Many synchronous blocking calls lead to Thread Pool starvation and degraded response times.»
ب) فشار حافظه و buffering در EF Core + XDocument
در دیتابیسهای بزرگ، این کار باعث GC pressure طولانی، pauseهای ثانیهای و در نهایت OOM یا exception در SqlClient میشود.
ج) Connection leak و pool exhaustion
در نسخههای اولیه با Dapper، SqlConnection بدون using درست مدیریت نمیشد → اتصالات برنمیگشتند به pool. اولین اجرا (pool تازه) موفق بود، اجرای بعدی timeout یا exhaustion.
CommandTimeout پیشفرض ۳۰ ثانیه برای کوئریهای سنگین کافی نبود.
د) اجرای sync در مسیر درخواست
سایتمپ داخل request handler اجرا میشد → اگر ۱۰–۶۰ ثانیه طول میکشید، thread بلاک میماند و کل اپلیکیشن تحت تأثیر قرار میگرفت.
گام ۱: انتقال به Background (مهمترین تغییر)
هرگز عملیات سنگین را داخل request اصلی await نکنید. از fire-and-forget یا background job استفاده کنید:
// در سرویس بهروزرسانی خبر، بعد از SaveChangesAsync()
_ = Task.Run(async () =>
{
try
{
await sitemapFacade.CreateOrUpdateSitemap();
}
catch (Exception ex)
{
_logger.LogError(ex, "Background sitemap generation failed");
}
});
بهتر: استفاده از Hangfire (ساده، داشبورد، retry خودکار) یا Quartz.NET (زمانبندی پیچیده).
نصب Hangfire:
// Program.cs
builder.Services.AddHangfire(config => config.UseSqlServerStorage(connString));
builder.Services.AddHangfireServer();
// فراخوانی
BackgroundJob.Enqueue(() => sitemapService.CreateOrUpdateSitemap());
مزایا Hangfire نسبت به Quartz (بر اساس مقالات ۲۰۲۵): داشبرد retry، persistence در DB، آسانتر برای .NET.
گام ۲: Streaming با IAsyncEnumerable (حل فشار حافظه)
EF Core از C# 8 به بعد AsAsyncEnumerable() را پشتیبانی میکند → دادهها row-by-row stream میشوند بدون buffering کامل.
در سرویس خبر:
public async IAsyncEnumerable GetAllNewsStreamingAsync()
{
var query = _context.News
.AsNoTracking()
.Where(n => n.Active && n.FutureDateTime <= DateTime.UtcNow)
.Select(x => new GetAllNewsForSitemapServiceDto { ... })
.OrderByDescending(x => x.FutureDateTime)
.AsAsyncEnumerable();
await foreach (var item in query)
{
yield return item;
}
}
در SitemapService (با XmlWriter streaming):
await foreach (var item in newsFacade.GetAllNewsStreamingAsync())
{
await WriteUrlAsync(writer, loc: $"https://.../{item.UniqueCode}", ...);
}
حافظه ثابت میماند (چند مگابایت). مقالات متعدد (مانند Medium و elmah.io) نشان میدهند IAsyncEnumerable برای دیتاستهای بزرگ، memory را تا ۹۰٪ کاهش میدهد.
گام ۳: بهبودهای جانبی
۵. بهترین شیوهها برای کارهای سنگین در ASP.NET Core (۲۰۲۵–۲۰۲۶)
| ویژگی | Hangfire | Quartz.NET |
|---|---|---|
| سادگی استفاده | بسیار بالا (داشبورد آماده) | متوسط (نیاز به داشبورد جدا) |
| Retry / Persistence | - | نیاز به پیادهسازی دستی |
| زمانبندی پیچیده | خوب | عالی (cron، calendar) |
| مناسب برای | اکثر پروژههای وب | سیستمهای enterprise |
این مشکل نشان داد که «کار میکند با Take(10)» همیشه نشانه سلامت نیست — فقط نشانه buffer کوچک است. ریشه اصلی، ترکیب thread pool محدود، عملیات synchronous سنگین و اجرای داخل request path بود. با انتقال به background + streaming، اپلیکیشن پایدار شد، حافظه کنترل شد و سایتمپ با هزاران رکورد بدون مشکل تولید میشود.
درس کلیدی: در ASP.NET Core، thread pool را مثل یک منبع محدود ببینید. هر thread بلاکشده، ظرفیت کل اپلیکیشن را کاهش میدهد. همیشه فکر کنید: «اگر این عملیات ۳۰ ثانیه طول بکشد، چه اتفاقی برای ۱۰۰ درخواست همزمان میافتد؟»
با این رویکرد، پروژههای بزرگ نهتنها کرش نمیکنند، بلکه تحت بار واقعی هم scalable میمانند.
این مقاله بر اساس تجربه واقعی حل مشکل نوشته شده و امیدوارم برای توسعهدهندگان دیگر مفید باشد. اگر سؤال یا سناریوی مشابهی داشتید، خوشحال از قسمت کامنت ها درمیان بگذارید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.