.NET 中的辅助角色服务

创建长时间运行的服务有很多原因,例如:

  • 处理 CPU 密集型数据。
  • 在后台对工作项进行排队。
  • 按计划执行基于时间的操作。

后台服务处理通常不涉及用户界面(UI),但 UI 可以围绕它们构建。 在 .NET Framework 的早期,Windows 开发人员可以出于这些目的创建 Windows 服务。 现在,在 .NET 环境下,你可以使用 BackgroundService,它是 IHostedService 的一种实现,或自行实现一个。

使用 .NET 时,不再局限于 Windows。 可以开发跨平台后台服务。 托管服务已准备好日志记录、配置和依赖项注入(DI)。 它们是库扩展套件的一部分,这意味着它们对使用 泛型主机的所有 .NET 工作负载至关重要。

重要

安装 .NET SDK 还会安装 Microsoft.NET.Sdk.Worker 和辅助角色模板。 换句话说,安装 .NET SDK 后,可以使用 dotnet new worker 命令创建新的辅助角色 。 如果使用 Visual Studio,则模板将隐藏,直到安装可选的 ASP.NET 和 Web 开发工作负载。

术语

许多术语被错误地同义词使用。 本部分定义了其中一些术语,以使本文中的意图更加明显。

  • 后台服务:类型 BackgroundService
  • 托管服务IHostedService 的实现,或 IHostedService 服务本身。
  • 长时间运行的服务: 持续运行的任何服务。
  • Windows 服务Windows 服务 基础结构,最初以 .NET Framework 为中心的,但现在可通过 .NET 访问。
  • 工作服务工作服务 模板。

辅助角色服务模板

Worker服务模板在 .NET CLI 和 Visual Studio 中可用。 有关详细信息,请参阅 .NET CLI- dotnet new worker 模板。 该模板由一个Program和一个Worker类组成。

using App.WorkerService;

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

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

前面的 Program 类:

模板默认值

默认情况下,工作者模板不会启用服务器垃圾回收(GC),因为有许多因素影响其必要性。 需要长时间运行的服务的所有方案都应考虑此默认值的性能影响。 若要启用服务器 GC,请将 ServerGarbageCollection 节点添加到项目文件:

<PropertyGroup>
    <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

权衡和注意事项

已启用 已禁用
高效的内存管理:自动回收未使用的内存,以防止内存泄漏并优化资源使用情况。 改进了实时性能:避免延迟敏感应用程序中垃圾回收导致的潜在暂停或中断。
长期稳定性:通过长时间管理内存,帮助保持长期运行的服务中的稳定性能。 资源效率:可以节省资源受限环境中的 CPU 和内存资源。
减少维护:最大程度地减少手动内存管理的需求,简化维护。 手动内存控制:为专用应用程序提供对内存的精细控制。
可预测行为:有助于一致且可预测的应用程序行为。 适用于生存期较短的进程:最大程度地减少短期或临时进程的垃圾回收开销。

有关性能注意事项的详细信息,请参阅 服务器 GC。 有关配置服务器 GC 的详细信息,请参阅 服务器 GC 配置示例

辅助角色类

至于Worker,该模板提供了一个简单的实现。

namespace App.WorkerService;

public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

前面的 Worker 类是 BackgroundService 的子类,用于实现 IHostedServiceBackgroundService 是一个 abstract class,需要子类来实现 BackgroundService.ExecuteAsync(CancellationToken)。 在模板实现中,每秒 ExecuteAsync 循环一次,记录当前日期和时间,直到向进程发出取消信号。

项目文件

辅助角色模板依赖于以下项目文件 Sdk

<Project Sdk="Microsoft.NET.Sdk.Worker">

有关详细信息,请参阅 .NET 项目 SDK

NuGet 包

基于辅助角色模板的应用使用 Microsoft.NET.Sdk.Worker SDK,并且具有对 Microsoft.Extensions.Hosting 包的显式包引用。

容器和云适应性

使用大多数新式 .NET 工作负载时,容器是一个可行的选项。 在 Visual Studio 中使用 Worker 服务模板创建长运行服务时,可以选择 Docker 支持。 这样做会创建容器化 .NET 应用的 Dockerfile Dockerfile 是用于构建镜像的一组指令。 对于 .NET 应用, Dockerfile 通常位于解决方案文件旁边的目录的根目录中。

# See https://aka.ms/containerfastmode to understand how Visual Studio uses this
# Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:e6b552fd7a0302e4db30661b16537f7efcdc0b67790a47dbf67a5e798582d3a5 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build
WORKDIR /src
COPY ["background-service/App.WorkerService.csproj", "background-service/"]
RUN dotnet restore "background-service/App.WorkerService.csproj"
COPY . .
WORKDIR "/src/background-service"
RUN dotnet build "App.WorkerService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "App.WorkerService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "App.WorkerService.dll"]

前面的 Dockerfile 步骤包括:

  • mcr.microsoft.com/dotnet/runtime:8.0 中的基映像设置为别名 base
  • 将工作目录更改为 /app
  • 设置 build 映像中的 mcr.microsoft.com/dotnet/sdk:8.0 别名。
  • 将工作目录更改为 /src
  • 复制内容并发布 .NET 应用:
  • mcr.microsoft.com/dotnet/runtime:8.0base 别名)中继 .NET SDK 映像。
  • /publish 复制已发布的生成输出。
  • 定义委托给 dotnet App.BackgroundService.dll 的入口点。

小窍门

mcr.microsoft.com 中的 MCR 代表“Microsoft Container Registry”,是 Microsoft 官方 Docker 中心的联合容器目录。 Microsoft syndicates 容器目录文章包含更多详细信息。

将 Docker 作为 .NET 辅助角色服务的部署策略时,项目文件中有几个注意事项:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WorkerService</RootNamespace>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
  </ItemGroup>
</Project>

在前面的项目文件中, <DockerDefaultTargetOS> 元素指定 Linux 为其目标。 若要面向 Windows 容器,请改用 Windows 。 从模板中选择 Microsoft.VisualStudio.Azure.Containers.Tools.Targets时,NuGet 包会自动添加为包引用。

有关使用 .NET 的 Docker 的详细信息,请参阅 教程:容器化 .NET 应用。 有关部署到 Azure 的详细信息,请参阅 教程:将辅助角色服务部署到 Azure

重要

如果要将用户机密与 Worker 模板结合使用,则必须明确引用 Microsoft.Extensions.Configuration.UserSecrets NuGet 包。

托管服务扩展性

IHostedService 接口定义两种方法:

这两种方法充当 生命周期 方法 - 它们分别在主机启动和停止事件期间调用。

注释

如果重写 StartAsyncStopAsync 方法,必须调用(和 awaitbase 类方法,以确保服务正常启动和/或关闭。

重要

该接口充当扩展方法的 AddHostedService<THostedService>(IServiceCollection) 泛型类型参数约束,这意味着只允许实现。 你可以随意将提供的 BackgroundService 与子类一起使用,或完全实现自己的版本。

信号完成

在大多数常见情况下,无需显式指示托管服务的完成。 当主机启动服务时,它们设计为在主机停止之前运行。 但是,在某些情况下,可能需要在服务完成后发出整个主机应用程序的完成信号。 若要发出完成信号,请考虑以下 Worker 类:

namespace App.SignalCompletionService;

public sealed class Worker(
    IHostApplicationLifetime hostApplicationLifetime,
    ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // TODO: implement single execution logic here.
        logger.LogInformation(
            "Worker running at: {Time}", DateTimeOffset.Now);

        await Task.Delay(1_000, stoppingToken);

        // When completed, the entire app host will stop.
        hostApplicationLifetime.StopApplication();
    }
}

在前面的代码中, BackgroundService.ExecuteAsync(CancellationToken) 该方法不会循环,并在完成时调用 IHostApplicationLifetime.StopApplication()

重要

这会向主机发出信号,指示主机应停止,并且如果没有对 StopApplication 的调用,主机将继续无限期地运行。 如果打算运行生存期较短的托管服务(运行一次方案),并且想要使用 Worker 模板,则必须调用 StopApplication 以通知主机停止。

有关详细信息,请参见:

替代方法

对于需要依赖项注入、日志记录和配置的短寿命应用程序,请使用 .NET 通用主机 而不是 Worker 模板。 这样就可以在没有 Worker 类的情况下使用这些功能。 使用泛型主机的短生存期应用的简单示例可以定义如下所示的项目文件:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ShortLived.App</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
  </ItemGroup>
</Project>

Program 类可能如下所示:

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

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<JobRunner>();

using var host = builder.Build();

try
{
    var runner = host.Services.GetRequiredService<JobRunner>();

    await runner.RunAsync();

    return 0; // success
}
catch (Exception ex)
{
    var logger = host.Services.GetRequiredService<ILogger<Program>>();
    
    logger.LogError(ex, "Unhandled exception occurred during job execution.");

    return 1; // failure
}

前面的代码将创建一个服务,该服务是一个 JobRunner 自定义类,其中包含要运行的作业的逻辑。 在JobRunner上调用RunAsync方法,如果成功完成,应用将返回0。 如果发生未经处理的异常,它将记录错误并返回 1

在此简单方案中, JobRunner 类可能如下所示:

using Microsoft.Extensions.Logging;

internal sealed class JobRunner(ILogger<JobRunner> logger)
{
    public async Task RunAsync()
    {
        logger.LogInformation("Starting job...");

        // Simulate work
        await Task.Delay(1000);

        // Simulate failure
        // throw new InvalidOperationException("Something went wrong!");

        logger.LogInformation("Job completed successfully.");
    }
}

你显然需要向 RunAsync 方法添加真正的逻辑,但此示例演示了如何使用泛型主机用于短暂运行的应用程序,而无需 Worker 类,并且无需显式发出主机完成的信号。

另请参阅