mcpField
W
worker-services

worker-services

github.com/managedcode开发49639 次安装今天更新
代码仓库

关于 worker-services

用于实现 .NET 长时间运行后台服务和定时 Worker 的开发技能,涵盖托管服务生命周期、取消处理、作用域依赖、队列处理、健康检查和优雅关闭。

主要功能

  1. 01使用 BackgroundService 管理 Worker 生命周期
  2. 02为作用域依赖创建服务作用域
  3. 03传递取消令牌并实现优雅关闭
  4. 04使用 PeriodicTimer 安排任务
  5. 05添加健康检查与结构化日志

适用场景

  1. 01构建定时后台 Worker
  2. 02使用作用域服务处理队列任务
  3. 03为 Worker 配置健康监控和关闭流程

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/managedcode/dotnet-skills --skill worker-services

安装后在 Claude Code 中输入/worker-services确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

manifest.json
references/
anti-patterns.md
patterns.md

详细介绍

.NET Worker Services

触发条件

  • 构建长时间运行的后台服务或计划任务 Worker
  • 向应用添加托管服务,或将其提取为 Worker 进程
  • 检查优雅关闭、取消、队列处理或运行状况行为

文档

参考资料
  • patterns.md - BackgroundService 模式、优雅关闭和运行状况检查实现
  • anti-patterns.md - Worker 服务中的常见错误及避免方法

工作流

  1. 使用 BackgroundService 作为基类:

    • 提供标准的 StartAsync/StopAsync 处理
    • 只需专注于实现 ExecuteAsync
    • 内置适当的取消令牌管理
  2. 正确处理作用域依赖项:

    • 为作用域服务创建服务作用域
    • 托管服务默认不会创建作用域
  3. 实现优雅关闭:

    • 传递取消令牌
    • 令牌触发后及时完成工作
    • 避免超时时发生非优雅关闭
  4. 保持执行循环精简:

    • 将业务逻辑移至可测试的服务中
    • 处理异常以避免服务崩溃
    • 使用 PeriodicTimer 执行计划任务
  5. 添加可观测性:

    • 使用运行状况检查确认就绪状态和存活状态
    • 暴露指标并使用结构化日志记录
    • 对于多实例部署,可考虑使用分布式锁

当前上游说明

  • .NET runtime v10.0.11 正在提供维护更新。对于 Worker,升级后应重新检查 cgroup-v2 内存限制检测、取消、优雅关闭、长时间运行线程的线程静态数据和 GC 路径、相关场景下的 NativeAOT/WASM 启动,以及 Windows 目录枚举;默认情况下不要更改架构。
  • 添加新的 Worker 入口点时,请使用更新后的 Worker Services 和托管服务 Learn 页面,确认当前准确的托管和运行状况检查 API。

基本 BackgroundService 模式

简单 Worker
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Worker starting");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Worker running at: {Time}", DateTimeOffset.Now);
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                // Graceful shutdown, not an error
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in worker iteration");
                // Continue or break based on error severity
            }
        }

        _logger.LogInformation("Worker stopping");
    }

    private async Task DoWorkAsync(CancellationToken cancellationToken)
    {
        // Business logic here
    }
}
使用 PeriodicTimer(推荐)
public class TimedWorker : BackgroundService
{
    private readonly ILogger<TimedWorker> _logger;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly TimeSpan _period = TimeSpan.FromMinutes(1);

    public TimedWorker(ILogger<TimedWorker> logger, IServiceScopeFactory scopeFactory)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(_period);

        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                await using var scope = _scopeFactory.CreateAsyncScope();
                var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>();
                await processor.ProcessAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing scheduled task");
            }
        }
    }
}

处理作用域依赖项

使用 Scope Factory 的正确模式
public class ScopedWorker : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<ScopedWorker> _logger;

    public ScopedWorker(IServiceScopeFactory scopeFactory, ILogger<ScopedWorker> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Create scope for each unit of work
            await using var scope = _scopeFactory.CreateAsyncScope();

            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var service = scope.ServiceProvider.GetRequiredService<IScopedService>();

            await service.ProcessAsync(dbContext, stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
}

队列处理模式

消息队列 Worker
public class QueueWorker : BackgroundService
{
    private readonly ILogger<QueueWorker> _logger;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IBackgroundTaskQueue _taskQueue;

    public QueueWorker(
        ILogger<QueueWorker> logger,
        IServiceScopeFactory scopeFactory,
        IBackgroundTaskQueue taskQueue)
    {
        _logger = logger;
        _scopeFactory = scopeFactory;
        _taskQueue = taskQueue;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queue Worker started");

        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = await _taskQueue.DequeueAsync(stoppingToken);

            try
            {
                await using var scope = _scopeFactory.CreateAsyncScope();
                await workItem(scope.ServiceProvider, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing queued work item");
                // Handle poison message - retry, dead-letter, etc.
            }
        }
    }
}

// Task queue interface
public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(
        Func<IServiceProvider, CancellationToken, ValueTask> workItem);

    ValueTask<Func<IServiceProvider, CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

Worker 的运行状况检查

添加运行状况检查端点
// Program.cs
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();

// Add health checks
builder.Services.AddHealthChecks()
    .AddCheck<WorkerHealthCheck>("worker_health")
    .AddResourceUtilizationHealthCheck();

// Add HTTP endpoint for health checks
builder.Services.AddHealthChecksUI();

// Or use simple TCP listener for Kubernetes
builder.Services.AddSingleton<TcpHealthProbeService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TcpHealthProbeService>());

var host = builder.Build();
host.Run();
自定义运行状况检查
public class WorkerHealthCheck : IHealthCheck
{
    private readonly WorkerState _workerState;

    public WorkerHealthCheck(WorkerState workerState)
    {
        _workerState = workerState;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        if (_workerState.LastSuccessfulRun > DateTime.UtcNow.AddMinutes(-5))
        {
            return Task.FromResult(HealthCheckResult.Healthy(
                $"Last successful run: {_workerState.LastSuccessfulRun}"));
        }

        return Task.FromResult(HealthCheckResult.Unhealthy(
            $"No successful run since: {_workerState.LastSuccessfulRun}"));
    }
}

// Shared state
public class WorkerState
{
    public DateTime LastSuccessfulRun { get; set; } = DateTime.UtcNow;
    public bool IsProcessing { get; set; }
}

优雅关闭模式

正确处理关闭
public class GracefulWorker : BackgroundService
{
    private readonly ILogger<GracefulWorker> _logger;
    private int _currentWorkItemId;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Worker starting");

        while (!stoppingToken.IsCancellationRequested)
        {
            _currentWorkItemId = GetNextWorkItemId();

            try
            {
                // Pass cancellation token to all async operations
                await ProcessWorkItemAsync(_currentWorkItemId, stoppingToken);
            }
            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation(
                    "Shutdown requested, stopping after work item {Id}", _currentWorkItemId);
                break;
            }
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Worker stopping gracefully");
        await base.StopAsync(cancellationToken);
        _logger.LogInformation("Worker stopped");
    }
}

Windows Service 部署

配置为 Windows Service
// Program.cs
var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "My Worker Service";
});

builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();
项目文件设置
<Project Sdk="Microsoft.NET.Sdk.Worker">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
  </PropertyGroup>
</Project>

最佳实践

  1. 使用 BackgroundService 作为基类 - 处理 StartAsync/StopAsync 的样板代码和取消管理
  2. 为作用域依赖项创建作用域 - 使用 IServiceScopeFactory 解析 DbContext 等作用域服务
  3. 在所有地方传递取消令牌 - 将其传递给所有异步方法,以便快速关闭
  4. 使用 try-catch 包裹工作 - 未处理的异常会导致服务完全停止
  5. 使用 PeriodicTimer 执行定时任务 - 相比 Task.Delay 更简洁,并且能正确支持取消
  6. 添加运行状况检查 - 这是 Kubernetes 存活/就绪探测的必要条件
  7. 避免阻塞 StartAsync - 长时间的初始化会延迟其他托管服务的启动
  8. 重写方法时调用基类方法 - 始终调用 await base.StartAsync() 和 await base.StopAsync()
  9. 将 Windows Service 发布为单文件 - 降低部署的复杂性和出错概率
  10. 考虑扩缩容要求 - 如果需要独立扩缩容,请拆分工作进程项目

应避免的反模式

反模式不良影响更好的做法
临时编写的 while(true) 循环无法平稳关闭,生命周期管理不佳使用 BackgroundService
忽略取消令牌无法平稳关闭,导致资源泄漏将令牌传递给所有异步调用
直接注入作用域服务依赖项被长期持有,导致内存泄漏使用 IServiceScopeFactory
ExecuteAsync 中出现未处理的异常工作进程会静默停止使用 try-catch 包裹,记录日志并继续
StartAsync 长时间运行阻止其他服务启动将工作移至 ExecuteAsync
async void 方法异常会导致进程崩溃使用 async Task
缺少运行状况检查无法了解工作进程状态实现 IHealthCheck
使用紧密循环轮询浪费 CPU,响应迟缓使用 PeriodicTimer 或事件驱动方式
未重写 StopAsync错过清理机会重写该方法以执行平稳清理
单例 DbContext非线程安全,数据过时每次操作都创建作用域

交付内容

  • 行为良好的工作进程和托管服务
  • 可预测的启动和关闭行为
  • 正确处理作用域依赖项
  • 用于生产环境可观测性的运行状况检查
  • 针对队列工作的重试和毒消息处理

验证

  • 已传递取消令牌,并遵循关闭请求
  • 在正确的作用域内解析作用域服务
  • 异常处理可避免服务崩溃
  • 运行状况检查准确报告工作进程状态
  • 可通过日志或遥测查看运行时行为
  • 异步上下文中没有阻塞调用

评论

支持 Markdown