.NET 通用主机

在本文中,你将学习用于配置和生成 Microsoft.Extensions.Hosting NuGet 包中提供的 .NET 通用主机的各种模式。 .NET 通用主机负责应用启动和生存期管理。 辅助角色服务模板会创建一个 .NET 通用主机 HostApplicationBuilder。 通用主机可用于其他类型的 .NET 应用程序,如控制台应用。

主机是封装应用资源和生存期功能的对象,例如:

  • 依赖关系注入 (DI)
  • Logging
  • Configuration
  • 应用关闭
  • IHostedService 实现

当主机启动时,它将对在托管服务的服务容器集合中注册的 IHostedService 的每个实现调用 IHostedService.StartAsync。 在辅助角色服务应用中,包含 BackgroundService 实例的所有 IHostedService 实现都调用其 BackgroundService.ExecuteAsync 方法。

一个对象中包含所有应用的相互依赖资源的主要原因是生存期管理:控制应用启动和正常关闭。

设置主机

主机通常由 Program 类中的代码配置、生成和运行。 Main 方法:

.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
    • EventLog(仅当在 Windows 上运行时)
  • 当环境为 Development 时,启用范围验证和依赖关系验证

HostApplicationBuilder.Services 是一个 Microsoft.Extensions.DependencyInjection.IServiceCollection 实例。 这些服务用于生成与依赖项注入一起使用的 IServiceProvider,以解析已注册的服务。

框架提供的服务

调用 IHostBuilder.Build()HostApplicationBuilder.Build() 时,自动注册以下服务:

IHostApplicationLifetime

IHostApplicationLifetime 服务注入任何类以处理启动后和正常关闭任务。 接口上的三个属性是用于注册应用启动和应用停止事件处理程序方法的取消令牌。 该接口还包括 StopApplication() 方法。

以下示例是注册 IHostApplicationLifetime 事件的 IHostedServiceIHostedLifecycleService 实现:

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_ 的环境变量。
    • 命令行参数。

应用配置

通过对 IHostApplicationBuilder 调用 ConfigureAppConfiguration 来创建应用配置。 公共属性 IHostApplicationBuilder.Configuration 允许使用者通过可用的扩展方法读取或更改现有配置。

有关详细信息,请参阅 .NET 中的配置

主机关闭

可以通过多种方式停止托管的进程。 停止托管进程的最常见方法有以下几种:

托管代码不负责处理这些方案。 进程所有者需要以处理任何其他应用的相同方式来处理它们。 可以通过几种其他方式来停止托管服务进程:

  • 如果使用 ConsoleLifetimeUseConsoleLifetime),它将侦听以下信号,并尝试正常停止主机。
    • SIGINT(或 CTRL+C)。
    • SIGQUIT(或 Windows 上的 CTRL+BREAK,Unix 上的 CTRL+\)。
    • SIGTERM(由其他应用发送,如 docker stop)。
  • 应用调用 Environment.Exit

这些方案由内置的托管逻辑处理,特别是 ConsoleLifetime 类。 ConsoleLifetime 尝试处理“关闭”信号 SIGINT、SIGQUIT 和 SIGTERM,以支持正常退出应用程序。

在 .NET 6 之前,.NET 代码无法正常地处理 SIGTERM。 为了绕开此限制,ConsoleLifetime 需要订阅 System.AppDomain.ProcessExit。 如果引发了 ProcessExitConsoleLifetime 会发出信号通知主机停止并阻止 ProcessExit 线程,等待主机停止。

进程退出处理程序将允许应用程序的清理代码运行 - 例如,这样,应用程序中的清理代码便可运行,例如 Main 方法中 HostingAbstractionsHostExtensions.Run 之后的 IHost.StopAsync 和代码。

但此方法还存在其他问题,因为 SIGTERM 不是提出的唯一 ProcessExit 方法。 当应用代码调用 Environment.Exit 时,也会引发 SIGTERM。 Environment.Exit 不是在 Microsoft.Extensions.Hosting 应用模型中正常关闭进程的方法。 它将引发 ProcessExit 事件,并退出该进程。 Main 方法的末尾不会执行。 后台和前台线程将会终止,并且不会执行 finally 块。

由于 ConsoleLifetime 在等待主机关闭期间阻止 ProcessExit,此行为导致 Environment.Exit死锁,也阻止等待对 ProcessExit 的调用。 此外,由于 SIGTERM 处理尝试正常关闭进程,因此 ConsoleLifetime 会将 ExitCode 设置为 0,这会强制改写传递给 Environment.Exit 的用户退出代码。

在 .NET 6 中,支持 POSIX 信号并可对其进行处理。 ConsoleLifetime 会正常处理 SIGTERM,并且在调用 Environment.Exit 时将不再参与。

提示

对于 .NET 6+,ConsoleLifetime 不再具有处理场景 Environment.Exit 的逻辑。 调用 Environment.Exit 并需要执行清理逻辑的应用可以自行订阅 ProcessExit。 在这些方案中,Hosting 将不再尝试正常停止主机。

如果应用程序使用主机代码,并且你希望正常停止主机,可调用 IHostApplicationLifetime.StopApplication 而不是 Environment.Exit

主机代码关闭过程

下面的顺序图演示了如何在主机代码中内部处理信号。 大多数用户不需要了解此过程。 但对于需要深入了解的开发人员而言,良好的视觉对象会有助于入门。

启动主机后,用户调用 RunWaitForShutdown 时,处理程序会注册 IApplicationLifetime.ApplicationStopping。 执行在 WaitForShutdown 中暂停,等待引发 ApplicationStopping 事件。 Main 方法将不会立即返回,并且应用在 RunWaitForShutdown 返回之前将会保持运行。

信号发送到进程时,它将启动以下序列:

主机代码关闭顺序图。

  1. 控制从 ConsoleLifetime 流向 ApplicationLifetime,引发 ApplicationStopping 事件。 这会指示 WaitForShutdownAsync 解除阻止 Main 执行代码。 同时,由于 POSIX 信号已经过处理,因此 POSIX 信号处理程序会返回 Cancel = true
  2. Main 执行代码将再次开始执行,并指示主机 StopAsync(),进而停止所有托管服务,并引发任何其他已停止的事件。
  3. 最后,WaitForShutdown 会退出,从而允许任何应用程序清理代码执行,Main 方法正常退出。

Web 服务器方案中的主机关闭

对于 HTTP/1.1 和 HTTP/2 协议,Kestrel 中还存在各种其他常见的正常关机方案,以及如何在不同环境中使用负载均衡器对其进行配置以平稳排出流量。 虽然 Web 服务器配置超出了本文的范围,但你可以在《配置 ASP.NET Core Kestrel Web 服务器的选项》文档中查找详细信息。

当主机收到关闭信号(例如 CTL+CStopAsync)时,它会通过发出 ApplicationStopping 信号通知应用程序。 如有任何长期运行的操作需要正常完成,则应订阅此事件。

接下来,主机会使用可配置的关闭超时(默认为 30 秒)来调用 IServer.StopAsync。 Kestrel(和 Http.Sys)将会关闭其端口绑定并停止接受新的连接。 它们还会告知当前连接停止处理新的请求。 对于 HTTP/2 和 HTTP/3,将会向客户端发送初步 GOAWAY 消息。 对于 HTTP/1.1,它们会停止连接循环,因为请求是按顺序处理的。 IIS 的行为不同,它在拒绝新的请求时会显示 503 状态代码。

活动请求会一直持续到关闭超时前才会完成。 如果它们在超时前全部完成,则服务器将更快地将控制权返回到主机。 如果超时过期,则会强制中止挂起的连接和请求,这可能会导致日志和客户端出错。

负载均衡器注意事项

要确保在使用负载均衡器时客户端顺利转换为新目标,可以执行以下步骤:

  • 启动新实例并开始均衡流向它的流量(你可能已有多个实例用于缩放)。
  • 禁用或移除负载均衡器配置中的旧实例,以便其停止接收新流量。
  • 向旧实例发出关闭信号。
  • 等待它耗尽或超时。

另请参阅