Универсальный узел .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()автоматической регистрации следующих служб:
Дополнительные построители узлов на основе сценариев
Если вы создаете веб-приложение или пишете распределенное приложение, может потребоваться использовать другой построитель узлов. Рассмотрим следующий список дополнительных построителей узлов:
- DistributedApplicationBuilder: построитель для создания распределенных приложений. Дополнительные сведения см. в статье .NET Aspire.
- WebApplicationBuilder: построитель веб-приложений и служб. Дополнительные сведения см. в статье об ASP.NET .
- WebHostBuilder: построитель для
IWebHost
. Дополнительные сведения см. на веб-узле ASP.NET Core.
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.
Выходные данные показывают порядок всех различных событий жизненного цикла:
IHostedLifecycleService.StartingAsync
IHostedService.StartAsync
IHostedLifecycleService.StartedAsync
IHostApplicationLifetime.ApplicationStarted
Если приложение остановлено, например с помощью CTRL+C, возникают следующие события:
IHostApplicationLifetime.ApplicationStopping
IHostedLifecycleService.StoppingAsync
IHostedService.StopAsync
IHostedLifecycleService.StoppedAsync
IHostApplicationLifetime.ApplicationStopped
IHostLifetime
Реализация IHostLifetime контролирует, когда узел запускается и останавливается. Используется последняя зарегистрированная реализация. Microsoft.Extensions.Hosting.Internal.ConsoleLifetime
— это реализация IHostLifetime
по умолчанию. Дополнительные сведения о принципах времени существования, на которых основано завершение работы, см. в разделе Завершение работы узла.
Интерфейс IHostLifetime
предоставляет IHostLifetime.WaitForStartAsync метод, который вызывается в начале IHost.StartAsync
которого будет ожидать завершения, прежде чем продолжить. Можно отложить запуск до получения сигнала от внешнего события.
Кроме того, IHostLifetime
интерфейс предоставляет IHostLifetime.StopAsync метод, который вызывается из IHost.StopAsync
того, что узел останавливается, и пришло время завершить работу.
IHostEnvironment
Внедряет службу IHostEnvironment в класс, чтобы получить сведения о следующих параметрах.
- IHostEnvironment.ApplicationName
- IHostEnvironment.ContentRootFileProvider
- IHostEnvironment.ContentRootPath
- IHostEnvironment.EnvironmentName
Кроме того, IHostEnvironment
служба предоставляет возможность оценивать среду с помощью этих методов расширения:
- HostingEnvironmentExtensions.IsDevelopment
- HostingEnvironmentExtensions.IsEnvironment
- HostingEnvironmentExtensions.IsProduction
- HostingEnvironmentExtensions.IsStaging
Конфигурация узла
Конфигурация узла используется для настройки свойств реализации IHostEnvironment.
Конфигурация узла доступна в HostApplicationBuilderSettings.Configuration свойстве, а реализация среды доступна в IHostApplicationBuilder.Environment свойстве. Чтобы настроить узел, получите доступ к свойству Configuration
и вызовите любой из доступных методов расширения.
Чтобы добавить конфигурацию узла, рассмотрим следующий пример:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
HostApplicationBuilderSettings settings = new()
{
Args = args,
Configuration = new ConfigurationManager(),
ContentRootPath = Directory.GetCurrentDirectory(),
};
settings.Configuration.AddJsonFile("hostsettings.json", optional: true);
settings.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
settings.Configuration.AddCommandLine(args);
HostApplicationBuilder builder = Host.CreateApplicationBuilder(settings);
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
Предыдущий код:
- В качестве корня содержимого задает путь, возвращенный методом GetCurrentDirectory().
- Загружает конфигурацию узла из:
- hostsettings.json.
- Переменные среды с префиксом
PREFIX_
. - аргументы командной строки.
конфигурация приложения;
Конфигурация приложения создается путем вызова ConfigureAppConfiguration IHostApplicationBuilder. ОбщедоступноеIHostApplicationBuilder.Configuration свойство позволяет потребителям считывать или вносить изменения в существующую конфигурацию с помощью доступных методов расширения.
Дополнительные сведения см. в статье Конфигурация в .NET.
Завершение работы узла
Существует несколько способов остановки размещенного процесса. Чаще всего размещенный процесс можно остановить следующим образом:
- Если кто-то не вызывает Run или HostingAbstractionsHostExtensions.WaitForShutdown, приложение завершает работу обычным образом после выполнения
Main
. - Аварийное завершение работы приложения.
- Принудительное завершение работы с помощью SIGKILL (или CTRL+Z).
Код размещения не отвечает за обработку этих сценариев. Владелец процесса должен иметь дело с ними так же, как и с любым другим приложением. Существует несколько других способов остановки размещенного процесса службы:
- Если
ConsoleLifetime
используется (UseConsoleLifetime), он прослушивает следующие сигналы и пытается остановить узел корректно. - Если приложение вызывает метод 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
не возвращается.
Если в процесс отправляется сигнал, он инициирует следующую последовательность:
- Управление передается от
ConsoleLifetime
вApplicationLifetime
для порождения событияApplicationStopping
. Это дает сигналWaitForShutdownAsync
разблокировать код выполненияMain
. В то же время обработчик сигнала POSIX возвращается сCancel = true
момента обработки сигнала POSIX. - Код выполнения
Main
снова начинает выполняться и сообщает узлу выполнить методStopAsync()
, что, в свою очередь, останавливает все размещенные службы и порождает все другие остановленные события. - Наконец,
WaitForShutdown
завершает работу, позволяя выполнять любой код очистки приложения, аMain
также для того, чтобы метод завершился корректно.
Завершение работы узла в сценариях веб-сервера
Существуют различные распространенные сценарии, в которых корректное завершение работы работает в Kestrel для протоколов HTTP/1.1 и HTTP/2 и как его можно настроить в разных средах с подсистемой балансировки нагрузки для плавного очистки трафика. Хотя конфигурация веб-сервера выходит за рамки этой статьи, дополнительные сведения о настройке параметров для документации по веб-серверу ASP.NET Core Kestrel.
Когда узел получает сигнал завершения работы (например, CTL+C илиStopAsync
), он уведомляет приложение, сигналив.ApplicationStopping Вы должны подписаться на это событие, если у вас есть длительные операции, которые должны завершиться корректно.
Затем узел вызывает IServer.StopAsync время ожидания завершения работы, которое можно настроить (по умолчанию 30s). Kestrel (и Http.Sys) закрывают привязки портов и перестают принимать новые подключения. Они также сообщают текущим подключениям прекратить обработку новых запросов. Для HTTP/2 и HTTP/3 предварительное GOAWAY
сообщение отправляется клиенту. Для HTTP/1.1 они останавливают цикл подключения, так как запросы обрабатываются по порядку. СЛУЖБЫ IIS ведут себя по-разному, отклоняя новые запросы с кодом состояния 503.
Активные запросы будут иметь до завершения времени ожидания завершения работы. Если все они завершены до истечения времени ожидания, сервер возвращает управление узлу раньше. Если истекает время ожидания, ожидающие подключения и запросы прерываются принудительно, что может вызвать ошибки в журналах и клиентах.
Особенности балансировщика нагрузки
Чтобы обеспечить плавное переход клиентов в новое место назначения при работе с подсистемой балансировки нагрузки, выполните следующие действия.
- Откройте новый экземпляр и начните балансировку трафика к нему (возможно, у вас уже есть несколько экземпляров для масштабирования).
- Отключите или удалите старый экземпляр в конфигурации подсистемы балансировки нагрузки, чтобы он перестал получать новый трафик.
- Сигнал о завершении работы старого экземпляра.
- Подождите, пока он будет стекать или истекает время ожидания.
См. также
- Внедрение зависимостей в .NET
- Ведение журнала в .NET
- Конфигурация в .NET
- Службы рабочей роли в .NET
- веб-узел ASP.NET Core
- ASP.NET конфигурация веб-сервера Kestrel Core
- Универсальные ошибки узла должны быть созданы в репозитории github.com/dotnet/runtime