Универсальный узел .NET

В этой статье вы узнаете о различных шаблонах настройки и создания универсального узла .NET, доступного в пакете NuGet Microsoft.Extensions.Hosting . Универсальный узел .NET отвечает за управление запуском приложений и временем существования. Шаблоны рабочей службы создают универсальный узел .NET HostApplicationBuilder. Этот универсальный узел можно использовать в сочетании с другими типами приложений .NET, такими как консольные приложения.

Узел — это объект, который инкапсулирует все ресурсы приложения и функциональные возможности времени существования, такие как:

  • Внедрение зависимостей
  • Ведение журнала
  • Настройка
  • завершение работы приложения;
  • Реализации IHostedService

После запуска узла он вызывает IHostedService.StartAsync в каждой реализации IHostedService, зарегистрированной в коллекции размещенных служб контейнера службы. В приложении рабочей службы для всех реализаций IHostedService, которые содержат экземпляры BackgroundService, вызываются соответствующие методы BackgroundService.ExecuteAsync.

Основной причиной включения всех взаимозависимых ресурсов приложения в один объект является управление жизненным циклом: контроль запуска и корректного завершения работы приложения.

Создание узла

Узел обычно настраивается, собирается и выполняется кодом в классе Program. Метод Main:

  • Вызывает метод CreateApplicationBuilder для создания и настройки объекта построителя.
  • Вызывает метод Build() для создания экземпляра IHost.
  • Вызывает метод Run или RunAsync для объекта узла.

Шаблоны рабочей службы .NET генерируют следующий код для создания универсального узла:

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

Дополнительные сведения о рабочих службах см. в разделе "Рабочие службы" в .NET.

Параметры построителя узлов

Метод CreateApplicationBuilder:

  • В качестве корня содержимого задает путь, возвращенный методом GetCurrentDirectory().
  • Загружает конфигурацию узла из:
    • Переменные среды с префиксом DOTNET_.
    • аргументы командной строки.
  • Загружает конфигурацию приложения из:
    • appsettings.json;
    • appsettings.{Environment}.json;
    • Диспетчер секретов при запуске приложения в Development среде.
    • среды.
    • аргументы командной строки.
  • Добавляет следующие поставщики ведения журнала:
    • Консоль
    • Отладка
    • EventSource
    • Журнал событий (только при запуске в Windows)
  • Включает проверку области и проверку зависимостей, если используется среда Development.

Microsoft.Extensions.DependencyInjection.IServiceCollection Это HostApplicationBuilder.Services экземпляр. Эти службы используются для сборки IServiceProvider , которая используется с внедрением зависимостей для разрешения зарегистрированных служб.

Платформенные службы

При вызове или IHostBuilder.Build()HostApplicationBuilder.Build()автоматической регистрации следующих служб:

IHostApplicationLifetime

Внедрите службу IHostApplicationLifetime в любой класс для выполнения задач после запуска и корректного завершения работы. Три свойства этого интерфейса представляют собой токены отмены, которые служат для регистрации методов обработчика событий запуска и завершения работы приложения. Этот интерфейс также включает метод StopApplication().

Следующий пример — это IHostedService реализация, IHostedLifecycleService которая регистрирует IHostApplicationLifetime события:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("4. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("5. OnStopping has been called.");
    }

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStopped()
    {
        _logger.LogInformation("9. OnStopped has been called.");
    }
}

Шаблон рабочей службы можно изменить, чтобы добавить в него реализацию ExampleHostedService.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

Приложение запишет следующий пример выходных данных:

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.OnStarted has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application started. Press Ctrl+C to shut down.
//     info: Microsoft.Hosting.Lifetime[0]
//           Hosting environment: Production
//     info: Microsoft.Hosting.Lifetime[0]
//           Content root path: ..\app-lifetime\bin\Debug\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

Выходные данные показывают порядок всех различных событий жизненного цикла:

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

Если приложение остановлено, например с помощью CTRL+C, возникают следующие события:

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

Реализация IHostLifetime контролирует, когда узел запускается и останавливается. Используется последняя зарегистрированная реализация. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime — это реализация IHostLifetime по умолчанию. Дополнительные сведения о принципах времени существования, на которых основано завершение работы, см. в разделе Завершение работы узла.

Интерфейс IHostLifetime предоставляет IHostLifetime.WaitForStartAsync метод, который вызывается в начале IHost.StartAsync которого будет ожидать завершения, прежде чем продолжить. Можно отложить запуск до получения сигнала от внешнего события.

Кроме того, IHostLifetime интерфейс предоставляет IHostLifetime.StopAsync метод, который вызывается из IHost.StopAsync того, что узел останавливается, и пришло время завершить работу.

IHostEnvironment

Внедряет службу IHostEnvironment в класс, чтобы получить сведения о следующих параметрах.

Кроме того, IHostEnvironment служба предоставляет возможность оценивать среду с помощью этих методов расширения:

Конфигурация узла

Конфигурация узла используется для настройки свойств реализации IHostEnvironment.

Конфигурация узла доступна в IHostApplicationBuilder.Configuration свойстве, а реализация среды доступна в IHostApplicationBuilder.Environment свойстве. Чтобы настроить узел, получите доступ к свойству Configuration и вызовите любой из доступных методов расширения.

Чтобы добавить конфигурацию узла, рассмотрим следующий пример:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

Предыдущий код:

  • В качестве корня содержимого задает путь, возвращенный методом GetCurrentDirectory().
  • Загружает конфигурацию узла из:
    • hostsettings.json.
    • Переменные среды с префиксом PREFIX_.
    • аргументы командной строки.

конфигурация приложения;

Конфигурация приложения создается путем вызова ConfigureAppConfigurationIHostApplicationBuilder. ОбщедоступноеIHostApplicationBuilder.Configuration свойство позволяет потребителям считывать или вносить изменения в существующую конфигурацию с помощью доступных методов расширения.

Дополнительные сведения см. в статье Конфигурация в .NET.

Завершение работы узла

Существует несколько способов остановки размещенного процесса. Чаще всего размещенный процесс можно остановить следующим образом:

  • Если кто-то не вызывает Run или HostingAbstractionsHostExtensions.WaitForShutdown, приложение завершает работу обычным образом после выполнения Main.
  • Аварийное завершение работы приложения.
  • Принудительное завершение работы с помощью SIGKILL (или CTRL+Z).

Код размещения не отвечает за обработку этих сценариев. Владелец процесса должен иметь дело с ними так же, как и с любым другим приложением. Существует несколько других способов остановки размещенного процесса службы:

  • Если ConsoleLifetime используется (UseConsoleLifetime), он прослушивает следующие сигналы и пытается остановить узел корректно.
    • SIGINT (или CTRL+C);
    • SIGQUIT (или CTRL+BREAK в ОС Windows, CTRL+\ в ОС Unix);
    • SIGTERM (отправляется другими приложениями, например docker stop).
  • Если приложение вызывает метод Environment.Exit.

Встроенная логика размещения обрабатывает эти сценарии, в частности ConsoleLifetime класс. ConsoleLifetime пытается обработать сигналы завершения работы SIGINT, SIGQUIT и SIGTERM, чтобы обеспечить корректное завершение работы приложения.

До выхода .NET 6 не существовало способа корректной обработки SIGTERM кодом .NET. Чтобы обойти это ограничение, класс ConsoleLifetime подписывался на событие System.AppDomain.ProcessExit. При порождении события ProcessExit класс ConsoleLifetime сообщал узлу о необходимости остановиться и заблокировать поток ProcessExit, ожидая остановки узла.

Обработчик выхода процесса позволит запустить код очистки в приложении, например код IHost.StopAsync после HostingAbstractionsHostExtensions.Run выполнения метода Main .

Однако существуют и другие проблемы с этим подходом, так как SIGTERM не был единственным способом ProcessExit . SIGTERM также возникает при вызовах Environment.Exitкода приложения. Метод Environment.Exit не является корректным способом завершения работы процесса в модели приложения Microsoft.Extensions.Hosting. Он порождает событие ProcessExit, а затем завершает процесс. Конец метода Main не выполняется. Фоновые и передние потоки завершаются, а finally блоки не выполняются.

Так как блокировка ProcessExit при ожидании завершения работы узла, это поведение привело к взаимоблокировкам из Environment.Exit также блоков, ожидающих вызоваProcessExit.ConsoleLifetime Кроме того, так как при обработке SIGTERM была предпринята попытка корректного завершения процесса, ConsoleLifetime присваивает свойству ExitCode значение 0, которое затирает код выхода пользователя, переданный в метод Environment.Exit.

В .NET 6 сигналы POSIX поддерживаются и обрабатываются. Дескриптор ConsoleLifetime SIGTERM корректно и больше не участвует при Environment.Exit вызове.

Совет

Для .NET 6+ ConsoleLifetime больше не имеет логики для обработки сценария Environment.Exit. Приложения, вызывающие Environment.Exit и требуемые для выполнения логики очистки, могут подписаться на ProcessExit самостоятельно. Размещение больше не попытается остановить узел в этих сценариях.

Если приложение использует размещение, а вы хотите корректно остановить узел, вы можете вызвать IHostApplicationLifetime.StopApplication вместо Environment.Exit.

Процесс завершения работы размещения

На следующей схеме показано, как сигналы обрабатываются в коде размещения. Большинству пользователей не нужно понимать этот процесс. Но для разработчиков, которые нуждаются в глубоком понимании, хороший визуальный элемент может помочь вам приступить к работе.

После запуска узла, когда пользователь вызывает Run или WaitForShutdown, для IApplicationLifetime.ApplicationStopping регистрируется обработчик. Выполнение в WaitForShutdown приостанавливается, ожидая порождения события ApplicationStopping. Метод Main не возвращается сразу, и приложение остается запущенным до тех пор, пока Run или WaitForShutdown не возвращается.

Если в процесс отправляется сигнал, он инициирует следующую последовательность:

Схема последовательности завершения работы размещения.

  1. Управление передается от ConsoleLifetime в ApplicationLifetime для порождения события ApplicationStopping. Это дает сигнал WaitForShutdownAsync разблокировать код выполнения Main. В то же время обработчик сигнала POSIX возвращается с Cancel = true момента обработки сигнала POSIX.
  2. Код выполнения Main снова начинает выполняться и сообщает узлу выполнить метод StopAsync(), что, в свою очередь, останавливает все размещенные службы и порождает все другие остановленные события.
  3. Наконец, WaitForShutdown завершает работу, позволяя выполнять любой код очистки приложения, а Main также для того, чтобы метод завершился корректно.

Завершение работы узла в сценариях веб-сервера

Существуют различные распространенные сценарии, в которых корректное завершение работы работает в Kestrel для протоколов HTTP/1.1 и HTTP/2 и как его можно настроить в разных средах с подсистемой балансировки нагрузки для плавного очистки трафика. Хотя конфигурация веб-сервера выходит за область этой статьи, дополнительные сведения о настройке ASP.NET веб-сервера Kestrel можно найти в документации по ASP.NET Core Kestrel.

Когда узел получает сигнал завершения работы (например, CTL+C илиStopAsync), он уведомляет приложение, сигналив.ApplicationStopping Вы должны подписаться на это событие, если у вас есть длительные операции, которые должны завершиться корректно.

Затем узел вызывает IServer.StopAsync время ожидания завершения работы, которое можно настроить (по умолчанию 30s). Kestrel (и Http.Sys) закрывают привязки портов и перестают принимать новые подключения. Они также сообщают текущим подключениям прекратить обработку новых запросов. Для HTTP/2 и HTTP/3 предварительное GOAWAY сообщение отправляется клиенту. Для HTTP/1.1 они останавливают цикл подключения, так как запросы обрабатываются по порядку. СЛУЖБЫ IIS ведут себя по-разному, отклоняя новые запросы с кодом состояния 503.

Активные запросы будут иметь до завершения времени ожидания завершения работы. Если все они завершены до истечения времени ожидания, сервер возвращает управление узлу раньше. Если истекает время ожидания, ожидающие подключения и запросы прерываются принудительно, что может вызвать ошибки в журналах и клиентах.

Особенности балансировщика нагрузки

Чтобы обеспечить плавное переход клиентов в новое место назначения при работе с подсистемой балансировки нагрузки, выполните следующие действия.

  • Откройте новый экземпляр и начните балансировку трафика к нему (возможно, у вас уже есть несколько экземпляров для масштабирования).
  • Отключите или удалите старый экземпляр в конфигурации подсистемы балансировки нагрузки, чтобы он перестал получать новый трафик.
  • Сигнал о завершении работы старого экземпляра.
  • Подождите, пока он будет стекать или истекает время ожидания.

См. также