Zero-downtime деплой C#: обновляем сервис без простоев

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваш сервис не должен «ложиться» на обед?

Представьте: пятница, 18:45. Вы только что запустили долгожданное обновление C# сервиса. И тут же в чат поддержки летят сообщения: «сайт не работает», «ошибка 503», «потерял данные корзины». Знакомо? Это классический сценарий, когда деплой «с ноги» убивает пользовательский опыт. В современном мире, где каждый час простоя может стоить миллионы, zero-downtime деплой C# сервисов — это не роскошь, а базовая гигиена разработки.

В этой статье мы не будем читать скучные лекции. Мы разберем реальные стратегии обновления .NET приложений без остановки работы, напишем код health checks и graceful shutdown, и покажем, как настроить rolling update в Kubernetes. Если вы устали от «магических» падений продакшена после каждого коммита — этот материал для вас. Поехали!

Что такое zero-downtime deployment и зачем он C# разработчику?

Zero-downtime deployment (деплой с нулевым временем простоя) — это методика обновления приложения, при которой старая версия продолжает обслуживать запросы до тех пор, пока новая версия полностью не станет работоспособной. Пользователь не замечает переключения: ни ошибок, ни таймаутов, ни потери сессии.

Основные проблемы при обновлении C# сервисов

  • Graceful shutdown работает не всегда. Стандартный app.StopAsync() может оборвать активные запросы, если не настроить таймауты.
  • Зависшие соединения. Старая версия держит открытые WebSocket или длинные HTTP-запросы.
  • Миграции БД. Новая версия требует изменений схемы, а старая с ними несовместима.
  • Кеш и сессии. После переключения пользователь теряет данные в памяти.

Хорошая новость: C# и экосистема .NET предоставляют все инструменты, чтобы решить эти проблемы. Наша задача — собрать их в работающую систему.

Стратегия №1: Blue-Green Deployment для C#

Это самая простая и надежная схема. У вас есть два идентичных окружения: Blue (текущая версия) и Green (новая версия). Вы разворачиваете новую версию на Green, прогоняете smoke-тесты, а затем мгновенно переключаете балансировщик с Blue на Green. Если что-то пошло не так — откатываетесь за секунду.

Как это реализовать для .NET?

Нам понадобится балансировщик (NGINX, HAProxy или облачный Load Balancer) и два инстанса вашего C# сервиса. Пример конфигурации NGINX:

upstream backend {
    server blue-server:5000 weight=100;
    server green-server:5000 weight=0;  # Резервный
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

После развертывания Green версии, меняем веса: blue:0, green:100. Пользователи незаметно перетекают на новую версию.

Плюсы: простота, мгновенный откат.
Минусы: нужно в 2 раза больше ресурсов (серверов).

Стратегия №2: Rolling Update с Kubernetes (идеально для микросервисов)

Kubernetes умеет делать rolling update «из коробки». Он постепенно заменяет старые Pod'ы новыми, не прерывая трафик. Но чтобы это работало с C# правильно, нужно настроить health probes и graceful shutdown.

Health Checks в .NET 6/8

Добавьте в свой Program.cs эндпоинт для проверки готовности (readiness) и живости (liveness):

using Microsoft.AspNetCore.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<CacheHealthCheck>("redis");

var app = builder.Build();

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});

app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // Только проверка, что процесс жив
});

app.Run();

В Deployment Kubernetes укажите:

livenessProbe:
  httpGet:
    path: /health/live
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 5

readinessProbe:
  httpGet:
    path: /health/ready
    port: 80
  initialDelaySeconds: 5
  periodSeconds: 10

K8s не будет отправлять трафик на Pod, пока readiness probe не вернет 200. Это гарантирует, что новый экземпляр C# сервиса полностью инициализировался (подключил БД, прогрел кеш) и готов принимать запросы.

Graceful Shutdown для .NET Core

Чтобы не обрывать запросы при остановке Pod'а, настройте корректное завершение:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
        webBuilder.ConfigureKestrel(options =>
        {
            options.Limits.MaxConcurrentConnections = 100;
        });
    })
    .UseShutdownTimeout(TimeSpan.FromSeconds(30))
    .Build();

// Обработчик SIGTERM (Kubernetes отправляет его при остановке)
AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) =>
{
    Console.WriteLine("Shutting down gracefully...");
    host.StopAsync().GetAwaiter().GetResult();
};

await host.RunAsync();

Важно: установите terminationGracePeriodSeconds в Kubernetes больше, чем ваш таймаут shutdown (например, 45 секунд).

Стратегия №3: Feature Flags и Canary Releases

Иногда вы хотите обновить логику, но не готовы отдавать ее всем пользователям сразу. Тут помогают feature toggles (библиотека Microsoft.FeatureManagement). Выкатываете новую версию сервиса, но новая функциональность включена только для 5% пользователей. Если всё стабильно — постепенно увеличиваете процент.

Пример использования Feature Flags в C#

// appsettings.json
{
  "FeatureManagement": {
    "NewPaymentGateway": false
  }
}

// Startup.cs
using Microsoft.FeatureManagement;

builder.Services.AddFeatureManagement();

// В контроллере:
[ApiController]
public class PaymentController : ControllerBase
{
    private readonly IFeatureManager _featureManager;

    public PaymentController(IFeatureManager featureManager)
    {
        _featureManager = featureManager;
    }

    [HttpPost]
    public async Task<IActionResult> ProcessPayment()
    {
        if (await _featureManager.IsEnabledAsync("NewPaymentGateway"))
        {
            return Ok(await NewPaymentFlow());
        }
        return Ok(await OldPaymentFlow());
    }                

Блоги

Рекомендаций по книгам