عنوان:

‫انتشار بدون قطعی (Zero-Downtime) با ترکیب Nginx Retry و Graceful Shutdown دات‌نت


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۲۷ ۲۰:۲۵
آدرس: www.dntips.ir
مقدمه و طرح مسئله
در پروژه‌های سازمانی بزرگ، پیاده‌سازی الگوهایی مانند Blue-Green Deployment یا Canary Releases به همراه کلاسترهای Kubernetes یا Load Balancerهای سخت‌افزاری امری مرسوم است. اما در اکثر پروژه‌های ساده، سیستم‌های تک‌سرور و استارتاپ‌ها، این ابزارها سربار مدیریتی و هزینه بالایی ایجاد می‌کنند. در معماری‌های تک‌سرور هنگام تعویض نسخه و ری‌استارت اپلیکیشن دو چالش بزرگ رخ می‌دهد:
  • خرابی تراکنش‌های جاری: درخواست‌های Create/Update/Delete که کاربران در همان لحظه فرستاده‌اند، با قطع شدن ناگهانی پروسس، ناقص و نیمه‌کاره رها می‌شوند.
  • خطای Bad Gateway (502/503): کاربرانی که دقیقاً در همان ۲ الی ۴ ثانیهٔ ری‌استارت شدن برنامه وارد سایت می‌شوند، با صفحه خطای وب‌سرور روبرو می‌شوند.

سازوکار راهکار ترکیبی
با ترکیب دو قابلیت درونی و سبک، بدون نیاز به هیچ ابزار اضافه‌ای، این مشکل کاملاً حل می‌شود:
[کاربران] ---> [Nginx Proxy (Retry Buffer)] ---> [.NET Kestrel (Graceful Shutdown)]
  • لایه دات‌نت: وقتی سیگنال خاموشی دریافت می‌کند، بلافاصله پورت Kestrel را برای ریکوئست‌های جدید می‌بندد اما تا ۳۰ ثانیه پردازش‌های دیتابیسی فعلی را به پایان می‌رساند.
  • لایه Nginx: در طول این ۲ تا ۴ ثانیه‌ای که Kestrel خاموش و مجدداً با کد جدید روشن می‌شود، Nginx درخواست‌های کاربران را در یک بافر نگه داشته (Retry) و به محض آماده شدن پروسس جدید، آنها را پاس می‌دهد. کاربر نهایی تنها یک تاخیر کوتاه ۲ ثانیه‌ای حس می‌کند و هیچ خطایی نمی‌بیند.


۱. تنظیمات پیشرفته Nginx
در فایل کانفیگ Nginx، به جای ارتباط ساده، قابلیت بازتلاش هوشمند را فعال می‌کنیم:
# /etc/nginx/conf.d/app.conf

upstream backend_dotnet {
    server 127.0.0.1:5000 max_fails=0;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend_dotnet;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # ----------------------------------------------------
        # هسته اصلی راهکار: مکانیزم Retry و Draining موقت
        # ----------------------------------------------------
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 3;
        proxy_next_upstream_timeout 10s;
        proxy_connect_timeout 2s;

        # مهلت مجاز برای درخواست‌های سنگین دیتابیسی
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}
تحلیل پارامترهای کلیدی:
  • proxy_next_upstream error timeout http_502 http_503: به Nginx دستور می‌دهد اگر در حین اتصال با خطای قطع شبکه، تایم‌اوت یا خطای ۵۰۲ مواجه شد، درخواست را لغو نکرده و دوباره تلاش کند.
  • proxy_connect_timeout 2s: زمان انتظار برای اتصال اولیه به Kestrel. اگر Kestrel در حال ری‌استارت باشد، حداکثر ۲ ثانیه صبر کرده و سپس تلاش بعدی را آغاز می‌کند.
  • proxy_next_upstream_tries 3: تعداد دفعات مجاز برای تلاش مجدد قبل از برگرداندن خطا به کلاینت.
  • max_fails=0: جلوگیری از اینکه Nginx سرور محلی را کلاً Unhealthy و از کار افتاده تلقی کند.

۲. تنظیمات دات‌نت (.NET Core / 8 / 9)
در فایل Program.cs، مهلت پیش‌فرض توقف Kestrel را تنظیم کرده و CancellationToken را عبور می‌دهیم:
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// تنظیم زمان مهلت خاموشی Kestrel برای اتمام عملیات جاری
builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});

var app = builder.Build();

// نمونه اندپوینت CRUD دیتابیسی
app.MapPost("/api/orders", async (OrderDto dto, AppDbContext db, CancellationToken ct) =>
{
    var order = new Order { Title = dto.Title, CreatedAt = DateTime.UtcNow };
    db.Orders.Add(order);
    
    // عبور دادن توکن لغو برای پشتیبانی از Rollback امن
    await db.SaveChangesAsync(ct);
    
    return Results.Created($"/api/orders/{order.Id}", order);
});

app.Run();

۳. مدیریت سرویس در سرور (Systemd Service)
برای مدیریت چرخه حیات نرم و ارسال سیگنال SIGTERM، فایل سرویس لینوکس را به‌صورت زیر تنظیم کنید:
# /etc/systemd/system/myapp.service
[Unit]
Description=My .NET Production App
After=network.target

[Service]
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll
Restart=always
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=35s
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

فرآیند انتشار (Deployment) در محیط عملیاتی
اکنون کل چرخه Deploy نسخه جدید به سادگی زیر خلاصه می‌شود:
  • کپی فایل‌های جدید: نسخه کامپایل‌شده در مسیر /var/www/myapp کپی می‌شود.
  • اجرای دستور ری‌استارت:
sudo systemctl restart myapp

اتفاقاتی که در پس‌زمینه رخ می‌دهد:
  • سیستم‌عامل سیگنال SIGTERM به دات‌نت می‌فرستد.
  • دات‌نت پذیرش درخواست جدید را متوقف کرده، اما تا ۳۰ ثانیه منتظر اتمام تراکنش‌های در حال پردازش می‌ماند.
  • در بازه زمانی ۲ الی ۳ ثانیه‌ای ری‌استارت، درخواست‌های جدید وارد Nginx شده و به لطف proxy_next_upstreamمنتظر می‌مانند.
  • پروسه جدید بالا می‌آید و Nginx ترافیک معلق را بدون قطعی تحویل آن می‌دهد.

جدول مقایسه روش‌ها

معیارروش عادی (Restart ساده)راهکار ترکیبی (Nginx Retry + .NET)Blue-Green کامل
هزینه و پیچیدگی زیرساختبسیار کمبسیار کم (تک‌سرور)متوسط تا زیاد (چند سرور/کانتینر)
قطعی سرویس (Downtime)۲ تا ۵ ثانیه خطای ۵۰۲صفر (تنها چند ثانیه تاخیر معلق)صفر
امنیت تراکنش‌های دیتابیسخطر لغو ناقص تراکنش۱۰۰٪ تضمین شده با Graceful Shutdown۱۰۰٪ تضمین شده

نظرات

  • وحید نصیری در ۱۴۰۵/۰۵/۲۸ ۰۸:۴۵
    سؤال: آیا می‌توان این سناریو را با IIS هم پیاده سازی کرد؟

    پاسخ: نه، متأسفانه؛ IIS (Internet Information Services) به‌صورت درونی و محلی (Native) قابلیت «بافر کردن درخواست‌ها و سعی مجدد خودکار» را هنگام خاموش شدن اپلیکیشن (Application Pool Recycle/Stop) ندارد. این یک تفاوت کلیدی معماری بین IIS و Nginx است.

    چرا IIS این قابلیت را ندارد؟
    IIS یک وب‌سرور سنتی و یکپارچه است. وقتی شما یک Application Pool را در IIS ری‌استارت یا متوقف می‌کنید، IIS سعی می‌کند فرآیند خاموشی نرم (Graceful Shutdown) را انجام دهد (ریکوئست‌های در حال اجرا را تا سقف pingResponseTime تمام کند)، اما وقتی فرآیند (w3wp.exe) بسته شد، IIS هیچ بافری برای نگهداری درخواست‌های جدیدی که در همان لحظه می‌رسند، ندارد. در واقع، اگر درخواست جدیدی در کسری از ثانیه بین بسته شدن فرآیند قدیم و بالا آمدن فرآیند جدید برسد، IIS فوراً خطای 503 (Service Unavailable) را به کاربر برمی‌گرداند، زیرا هیچ فرآیند فعالی برای تحویل درخواست وجود ندارد.

    راه‌حل‌ها برای IIS چیست؟
    اگر مجبور به استفاده از IIS هستید و هم‌زمان به Zero-Downtime Deployment نیاز دارید، ساده‌ترین راه‌حل‌ها دیگر «تک‌سرور» نیستند:
    • استفاده از IIS Application Request Routing (ARR) به عنوان Load Balancer: شما باید دو اینستنس از سایت خود (روی پورت‌های مختلف یا سرورهای مختلف) بالا بیاورید و از یک اینستنس IIS دیگر با ماژول ARR به عنوان لودبالانسر در جلوی آنها استفاده کنید. در این حالت، ARR می‌تواند هنگام دیپلوی روی یکی از نسخه‌ها، ترافیک را به نسخه دیگر هدایت کند و سپس برعکس. این روش شبیه به همان الگوی Blue-Green است که با Nginx بررسی کردیم.
    • استفاده از Nginx در جلوی IIS: اگر همچنان می‌خواهید از قدرت IIS استفاده کنید، می‌توانید از ترکیب Nginx (به عنوان Reverse Proxy) و IIS (به عنوان وب‌سرور اپلیکیشن) استفاده کنید. Nginx در جلوی IIS قرار می‌گیرد و دقیقاً همان کانفیگ proxy_next_upstream را که توضیح دادیم، روی آن اعمال می‌کنید. این ترکیب بسیار رایج است: Nginx درخواست‌های ورودی را مدیریت و بافر می‌کند، و IIS اپلیکیشن دات‌نت را اجرا می‌کند.

    مقایسه عملکرد هنگام Deploy

    ویژگیNginx (Single Server)IIS (Single Server)IIS + ARR (Two Instances)
    هزینه زیرساختبسیار کم (یک سرور)بسیار کم (یک سرور)متوسط (نیاز به چند اینستنس)
    رفتار با ریکوئست‌های جدید هنگام Restartمعلق (با سوییچ شفاف)ارسال خطای ۵۰۳انتقال شفاف به اینستنس دیگر
    مدیریت خاموشی نرم دات‌نتبله (SIGTERM)بله (IIS Shutdown)بله
    در نهایت، اگر می‌خواهید با حداقل هزینه و بدون پیچیدگی چند‌سروری، خطای ۵۰۳ را هنگام دیپلوی حذف کنید، Nginx انتخاب بهتری نسبت به IIS در حالت تک‌سرور است.