ASP.NET核心背景服务----第一部分:方法 (中文 (Chinese Simplified))

ASP.NET核心背景服务----第一部分:方法

Thursday, 27 November 2025

//

15 minute read

每一个现代网络应用程序都有工作,不能阻止 HTTP 发送电子邮件、处理文件、与外部服务同步、运行计划维护、运行计划维护。 ASP.NET Core 提供了处理这一背景工作的多种方法,从简单到简单 IHostedService 执行到复杂的框架, 比如“ 燃火” 。 在第一部分, 我们将探索基本模式, 以及何时使用每个模式 。

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

这个网站(BLOG站点!)有超过一半的人从事讽刺性的背景工作, 但和他们拥有的事物一样, 背景服务是现代网络应用程序的隐形英雄。 虽然您的控制器在前景中处理 HTTP 请求, 但背景服务会悄悄处理排队的电子邮件、 搜索索引内容、 检查外部 API 、 清理临时文件, 并处理无数其它任务, 否则会阻碍您的请求管道 。

在这个由两部分组成的系列系列中, 我们将探索不同方法 来实施 ASP.NET核心的背景服务, IHostedServiceBackgroundService 在第一部分中,我们将研究基本方法及其特性。在第一部分中,我们将研究这些基本方法及其特性。 第二部分 第二部分,我们将潜入现实世界执行 从生产代码库。

重要: 我们将特别关注生命周期管理 特别是常被忽略的 StopAsync 许多开发商在关闭应用程序时 遇到隐秘的例外。

为什么是背景服务?

在潜入“如何”之前,让我们简单考虑一下“为什么”。背景服务允许你:

  1. 卸载慢速操作 - 不要让用户在发送电子邮件或生成 PDF 时等待
  2. 附表所列经常性任务 - 每天凌晨2点清理旧记录
  3. 进程队列 - 处理渠道或信息经纪人发来的信息或信息经纪人发来的信息
  4. 监视外部状态 - 投票单或监视文件系统,以进行修改
  5. 协调复杂的工作流程 - 管理跨越分钟或小时的多步骤进程

ASP.NET核心提供几种执行这些服务的办法,每种办法的权衡取舍不同。

历史背景:为什么背景服务现在可以实现

在“旧时”(2010年以前),在您的网络应用程序中运行背景工作通常被认为是一个坏主意。 常规智慧是 : “ 网络服务器处理网络请求。 背景工作属于一个单独的服务器 。 ”

这不仅仅是货物技术的智慧, 而是基于真正的技术限制:

单一核心时代

早期网络服务器(现在诚实地说,“便宜”的Azure服务)通常使用 单一核心或双核心CPU。如果您执行CPU密集的背景任务,它与同一核心的网络请求直接竞争:

Single Core (2005):
┌─────────────────────┐
│  Background Task    │  ← Uses 80% CPU
│  (80% of core)      │
├─────────────────────┤
│  Web Requests       │  ← Only 20% left!
│  (20% of core)      │  ← Slow responses
└─────────────────────┘

结果:你的网站在背景工作启动的那一刻变得迟钝。

串线池( Tread 集合) 饥饿

使用的经典ASP.NET 要求的线串线索库相对较小(通常是25-100线索),背景任务将窃取应处理网络请求的线索:

// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
    // This steals a thread from the pool!
    ProcessLongRunningTask();
});

// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable

IIS 应用池回收利用

IIS将根据内存限制、请求计数或时间表,积极回收应用池(重新启动应用软件)。

00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
      - Background task killed
      - Work lost, must start again

Async有限公司/等待支助

在.NET4.5(2012)之前,同步编程(相对而言)很痛苦,背景任务往往不必要地堵住线条:

// Pre-async (2008)
void ProcessEmails()
{
    foreach (var email in GetEmails())
    {
        smtp.Send(email);  // Blocks thread for 500ms per email
    }
}
// 100 emails = 50 seconds of blocked thread time

现代时代

今日的风景与众不同:

1. 多价可负担

经济学已经翻转了。 含有多个核心的云层VM 价格合理,而光金属服务器却价格低得令人惊讶。 这个博客在专用的8核心服务器上运行,其成本低于可比的Azure VM — — 而且所有这些核心都交给我自己,没有吵闹的邻居。 一个核心的背景任务不会极大地影响网络对其他核心的要求:

8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task

2. 阿斯辛/等待无处不在

现代. NET 使非同步编程变得微不足道。 背景任务可以在 I/ O 上等待, 而不屏蔽线索 :

// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
    await foreach (var email in GetEmailsAsync(ct))
    {
        await smtp.SendAsync(email, ct);  // Doesn't block thread!
    }
}
// 100 emails processed efficiently, thread returns to pool during I/O

3. 更好的程序托管

  • 嵌嵌入器 - 集装箱内的背景服务不会被任意回收
  • 库贝涅茨 - 适当优雅的关闭处理 SIGTERM
  • 系统d - 可靠地重新启动 Linux 服务的 Linux 服务
  • 视窗服务 - 适当的长期过程模型

4. 海峡和现代原始设施

NET现在有头等支持。 System.Threading.Channels:

// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);

// Producer (web request)
await channel.Writer.WriteAsync(email);  // Fast, non-blocking

// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(email);  // Efficient, async
}

5. 资源限额和分组

现代集装箱管弦手让你 限制资源使用:

# Kubernetes resource limits
resources:
  limits:
    cpu: "500m"        # Background task can't use more than 0.5 CPU
    memory: "512Mi"    # Or more than 512 MB RAM

这意味着一个离家出走的背景任务 不能饿死你的网络层。

问题不再是“我们能否在网络应用程序中提供背景服务?”**我们应该吗?**稍后我们会在“何时不使用背景服务”部分探讨这一决定。

内建选项

住宿服务:本基金会

ASP.NET核心的每一项背景服务的核心是其核心内容。 IHostedService。这个界面非常简单:

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

就是这样 两种方法 StartAsync 您的应用程序启动时会被调用,并且 StopAsync 当它关闭的时候,

登记您在 Program.cs:

builder.Services.AddHostedService<MyBackgroundService>();

以下是生命周期的可视化:

graph LR
    A[Application Starts] --> B[StartAsync Called]
    B --> C[Service Running]
    C --> D[Application Shutting Down]
    D --> E[StopAsync Called]
    E --> F[Application Stopped]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style F stroke:#dc2626,stroke-width:3px,color:#ef4444

开始同步: 同步和同步开始

执行时的一项关键决定 IHostedService 是否是您的 StartAsync 方法应该立即屏蔽或返回。

同步( 阻塞) 开始 :

public class BlockingStartService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // This blocks application startup until complete
        await InitializeDatabaseAsync(cancellationToken);
        await LoadConfigurationAsync(cancellationToken);

        // Only now will the application continue starting
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}

非同步( 非阻塞) 开始 :

public class NonBlockingStartService : IHostedService
{
    private Task _backgroundTask;
    private readonly CancellationTokenSource _cts = new();

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start background work but return immediately
        _backgroundTask = Task.Run(async () =>
        {
            // Give other services time to initialise
            await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
            await DoLongRunningWorkAsync(_cts.Token);
        }, _cts.Token);

        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _cts.Cancel();
        await _backgroundTask; // Wait for completion
    }
}

何时使用每种方针:

  • 屏蔽 : 当服务必须在应用程序能够处理请求之前完成初始化(例如,装载关键配置、变暖缓存)
  • 无阻塞: 当服务可在背景中初始化时, 当其他服务开始启动时( 例如, 索引化现有内容, 与外部服务同步)

StopAsync: 共同瀑布

这里就是事情变得有趣的地方 许多开发商遇到问题的地方。当你的应用程序关闭时, ASP.NET Core calls StopAsync 所有主机服务。 您有一个有限的窗口( 默认 5 秒) , 可以优雅地清理。 您可以在 Program.cs:

builder.Services.Configure<HostOptions>(options =>
{
    options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});

最常见的错误是:

public class BrokenService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // WRONG: The channel is still open, ProcessMessagesAsync
        // will hang on WaitToReadAsync forever!
        return Task.CompletedTask;
    }

    private async Task ProcessMessagesAsync()
    {
        // This will never exit because the channel is never completed
        await foreach (var message in _channel.Reader.ReadAllAsync())
        {
            await ProcessAsync(message);
        }
    }
}

当您运行此服务并停止您的应用程序时, 您将会看到错误, 比如 :

Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'

或者申请在强制终止之前,只需暂停停产的暂停期。

正确的做法:

public class CorrectService : IHostedService
{
    private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
    private readonly CancellationTokenSource _cts = new();
    private Task _processingTask;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _processingTask = ProcessMessagesAsync(_cts.Token);
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        // CORRECT: Signal cancellation and complete the channel
        await _cts.CancelAsync();
        _channel.Writer.Complete();

        try
        {
            // Wait for processing to finish or for the shutdown timeout
            await Task.WhenAny(_processingTask,
                Task.Delay(Timeout.Infinite, cancellationToken));
        }
        catch (OperationCanceledException)
        {
            // Expected when shutdown timeout is reached
        }
    }

    private async Task ProcessMessagesAsync(CancellationToken token)
    {
        await foreach (var message in _channel.Reader.ReadAllAsync(token))
        {
            try
            {
                await ProcessAsync(message);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested, exit gracefully
                break;
            }
        }
    }
}

正确执行 StopAsync 的要点 :

  1. 信号取消取消 - 使用 a CancellationTokenSource 并取消它
  2. 完成渠道 - 如果你在使用频道,打电话 Writer.Complete()
  3. 等待背景任务 - 使用 Task.WhenAny 关闭取消代号
  4. 处理 操作 - 这是预料到的,应该被抓住
  5. 不要放弃例外 - 例外 StopAsync 可能造成不可预测行为

背景服务:便利基地级

书写 IHostedService 执行可以是重复的。您总是需要背景任务、取消符号源以及相同的清理模式。 BackgroundService 为你处理这个锅炉板:

public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executeTask;
    private CancellationTokenSource _stoppingCts;

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        _stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        _executeTask = ExecuteAsync(_stoppingCts.Token);
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_executeTask == null) return;

        try
        {
            _stoppingCts.Cancel();
        }
        finally
        {
            await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
        }
    }

    public virtual void Dispose()
    {
        _stoppingCts?.Cancel();
    }
}

你只要执行 ExecuteAsync 让基础班处理管道:

public class SimpleBackgroundService : BackgroundService
{
    private readonly ILogger<SimpleBackgroundService> _logger;

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

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

        // Wait for app to finish starting
        await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await DoWorkAsync(stoppingToken);
                await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Shutdown requested
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in background service");
            }
        }

        _logger.LogInformation("Service stopping");
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        _logger.LogInformation("Doing work...");
        // Your actual work here
        await Task.Delay(1000, token);
    }
}

何时使用背景服务相对于 IHosted 服务

使用使用 BackgroundService 时间:

  • 您需要一个长期的背景环环
  • 您想要简单的定期执行
  • 你不需要细细控制 StartAsync/StopAsync 时间时机

使用使用 IHostedService 时间:

  • 你需要控制到底发生了什么 StartAsync 背景工作
  • 你在设置事件处理器或观察器 而不是一个连续循环
  • 启动时您需要与其他服务协调

高级:启动协调

有时您需要服务等待对方。例如,您可能需要您的语义搜索索引, 等待您标记文件处理器完成初始负载 。

以下是一个协调服务启动的模式:

public interface IStartupCoordinator
{
    void RegisterService(string serviceName);
    void SignalReady(string serviceName);
    bool IsServiceReady(string serviceName);
    Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
    Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}

public class StartupCoordinator : IStartupCoordinator
{
    private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
    private readonly ILogger<StartupCoordinator> _logger;

    public void RegisterService(string serviceName)
    {
        _services.TryAdd(serviceName, new TaskCompletionSource());
    }

    public void SignalReady(string serviceName)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            tcs.TrySetResult();
            _logger.LogInformation("{Service} is ready", serviceName);
        }
    }

    public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
    {
        if (_services.TryGetValue(serviceName, out var tcs))
        {
            await tcs.Task.WaitAsync(ct);
        }
    }

    public async Task WaitForAllServicesAsync(CancellationToken ct = default)
    {
        await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
    }
}

服务使用率 :

public class DependentService : IHostedService
{
    private readonly IStartupCoordinator _coordinator;
    private readonly ILogger<DependentService> _logger;

    public DependentService(
        IStartupCoordinator coordinator,
        ILogger<DependentService> logger)
    {
        _coordinator = coordinator;
        _logger = logger;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Wait for another service to be ready
        await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);

        _logger.LogInformation("Dependencies ready, starting work");

        // Do your work...

        // Signal you're ready for services that depend on you
        _coordinator.SignalReady("DependentService");
    }

    public Task StopAsync(CancellationToken cancellationToken)
        => Task.CompletedTask;
}

当您拥有多种具有相互依存关系的背景服务时,这种模式变得特别有用。

与Redis 的分布式协调

启动协调员在一个应用程序实例中工作。 但是当您向多个实例扩展时会怎样? 您不希望三个实例同时运行相同的计划任务 。

重新重订 提供一个简单的解决办法:使用旗帜(钥匙)来协调谁做什么。

简单领袖选举

public class DistributedBackgroundService : BackgroundService
{
    private readonly IConnectionMultiplexer _redis;
    private readonly ILogger<DistributedBackgroundService> _logger;
    private readonly string _instanceId = Guid.NewGuid().ToString();
    private const string LeaderKey = "background:newsletter:leader";

    public DistributedBackgroundService(
        IConnectionMultiplexer redis,
        ILogger<DistributedBackgroundService> logger)
    {
        _redis = redis;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var db = _redis.GetDatabase();

        while (!stoppingToken.IsCancellationRequested)
        {
            // Try to become the leader (SET NX with expiry)
            var acquired = await db.StringSetAsync(
                LeaderKey,
                _instanceId,
                TimeSpan.FromMinutes(5),
                When.NotExists);

            if (acquired)
            {
                _logger.LogInformation("This instance is the leader, running task");

                try
                {
                    await DoScheduledWorkAsync(stoppingToken);
                }
                finally
                {
                    // Release leadership
                    await db.KeyDeleteAsync(LeaderKey);
                }
            }
            else
            {
                var leader = await db.StringGetAsync(LeaderKey);
                _logger.LogDebug("Another instance ({Leader}) is the leader", leader);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

关键区块的分布锁定

对于不能同时执行的任务,不能同时执行:

public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
    var db = _redis.GetDatabase();
    var lockKey = "locks:critical-task";
    var lockValue = _instanceId;

    // Try to acquire lock
    if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
    {
        try
        {
            _logger.LogInformation("Lock acquired, processing...");
            await DoCriticalWorkAsync(cancellationToken);
        }
        finally
        {
            await db.LockReleaseAsync(lockKey, lockValue);
        }
    }
    else
    {
        _logger.LogDebug("Could not acquire lock, another instance is processing");
    }
}

何时使用分配式协调

  • 排定任务 - 仅应发一份每日通讯
  • 有订单的队列处理 - 确保信息处理有序
  • 资源密集型业务 - 防止多重事件压倒外部API
  • 数据库迁移 - 启动时只应出现一个移民案例

对于更复杂的情景(多步骤工作、跨重新启动的可靠日程安排),请考虑使用数据库后端自动处理分布式锁定的Hangfire(Hangfire)。

当不使用背景服务时

在我们潜入更先进的工具之前, 比如杭火, 让我们来谈谈当你 不应该 在您的主网络应用程序中使用背景服务 。

您应该分割到独立工程的符号

您的网络应用程序中运行的背景服务与 HTTP 请求的管道共享资源。 这可能造成问题 :

1. 资源内容

问题: 您的背景服务消耗了大量 CPU、内存或数据库连接 。

// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var video = await _queue.DequeueAsync();
            // This uses 100% of 4 CPU cores for 5 minutes
            await TranscodeVideoAsync(video);
        }
    }
}

当网络请求在转换编码期间到达时, 它们速度缓慢, 因为 CPU 忙碌。

解决方案 : 移动到单独的工人服务处 :

# Your solution structure
/YourApp.Web          # ASP.NET Core web app - no background services
/YourApp.Worker       # .NET Worker Service - handles background work
/YourApp.Shared       # Shared models, interfaces

2. 不同的分摊比例要求

问题: 您的背景工作需要与网络水平不同的缩放 。

  • 网络层次 : 缩放 HTTP 流量( 白天可能需要10例, 晚上2例)
  • 背景分层 : 排队深度缩放( 通常可能需要 1 个实例, 处理批量时20个)

如果他们在同一个过程, 你不能独立地放大它们。

实例设想:

09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers

将背景服务放到网络应用程序中意味着你必须运行20个网络事件, 才能处理通讯,浪费资源。

3. 部署独立

问题: 您想要在不重新启动背景服务( 反之亦然) 的情况下部署 Web 更改 。

// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // This import takes 2 hours
        await ImportMillionsOfRecordsAsync(stoppingToken);
    }
}

每次部署都中断进口。 将其移动到您独立部署的单独的工人服务处 。

4. 不同的失败领域

问题: 您背景服务中的一个错误崩溃了整个网络应用程序 。

// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        string value = null;
        // Unhandled exception - takes down the whole app
        await ProcessAsync(value.Length);
    }
}

如果背景工作是一个单独的进程,它可能崩溃和重新开始,而不影响网络请求。

如何乘法背景服务

当你决定分开时,这里是推荐的建筑:

备选1:.NET工人服务

使用 worker Service 模板创建新工程 :

dotnet new worker -n YourApp.Worker

结构:

/YourApp.Worker
  /Services
    VideoTranscodingService.cs
    EmailSenderService.cs
  /Program.cs
  /appsettings.json

方案:

var builder = Host.CreateApplicationBuilder(args);

// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();

// Share configuration with web app
builder.Services.Configure<VideoConfig>(
    builder.Configuration.GetSection("Video"));

// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

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

单独部署 :

# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3

# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2

选项2:有共享队列的单独项目

使用消息队列来拆分 Web 和工人 :

graph LR
    A[Web App] --> B[Message Queue]
    B --> C[Worker 1]
    B --> D[Worker 2]
    B --> E[Worker N]

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
    style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6

网络应用程序队列工作 :

// In your web controller
public class VideoController : ControllerBase
{
    private readonly IMessageQueue _queue;

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile video)
    {
        await _storage.SaveAsync(video);

        // Queue for processing - don't process in web app
        await _queue.PublishAsync(new VideoTranscodeJob
        {
            VideoId = video.Id,
            Priority = Priority.Normal
        });

        return Accepted(); // Return immediately
    }
}

工人从队列中消耗 :

// In your worker service
public class VideoWorker : BackgroundService
{
    private readonly IMessageQueue _queue;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
        {
            await TranscodeAsync(job);
        }
    }
}

流行信息队列选项 :

备选办法3:多种专业工人

对于复杂的系统,责任划分如下:

/YourApp.Web              # HTTP requests only
/YourApp.EmailWorker      # Sends emails
/YourApp.VideoWorker      # Transcodes videos
/YourApp.ReportWorker     # Generates reports
/YourApp.Scheduler        # Runs scheduled jobs (Hangfire)

每个工人可以:

  • 独立缩放
  • 独立部署
  • 使用不同的资源(电子邮件工人需要SMTP、视频工人需要GPU)
  • 具有不同的监测和警报

何时将背景服务保留在您的 Web App 中

尽管存在上述情况,但一些设想方案对于进程内背景服务来说是完全正常的:

轻量度定期任务

// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _cache.WarmupAsync(); // Quick operation
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

活动听众

// Fine to keep in web app
public class FileWatcherService : IHostedService
{
    // Reacts to events, doesn't consume significant resources
    private FileSystemWatcher _watcher;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _watcher = new FileSystemWatcher("/config");
        _watcher.Changed += OnConfigChanged;
        _watcher.EnableRaisingEvents = true;
        return Task.CompletedTask;
    }
}

基于频道的队列(用于非关键工作)

// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
    // Sends emails in background, but each email takes < 1 second
    // If the app restarts, losing a few queued emails is acceptable
}

启动协调

// Fine to keep in web app
public class WarmupService : IHostedService
{
    // Runs once at startup, then does nothing
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await _database.WarmupConnectionPoolAsync();
        await _cache.LoadCriticalDataAsync();
    }
}

决定矩阵

在 Web App 中保留特性 |---------------|-----------------|------------------------| CPU 每次操作的使用量 < 100ms > 1 秒 * * < 100ms > * * * * * * * * * a second * * * * * * = * = * = * = * = * = * = * = * = * = * = * = * = * = * = * = = * = * = * = * = * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

  • 每次操作的内存 * * < 10 MB * > 100 MB *
  • 频率 * 定期(分钟/小时) * 连续或高频 * 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 非关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 非关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 非关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 关键 持续时间 秒 分钟到小时 工作队列深度

真实世界实例:博客平台

在博客平台的代码 我们检查在第二部分:

在网络应用程序中保存 :

  • MarkdownDirectoryWatcherService - 轻量度文件监视器
  • UmamiBackgroundSender - 快速分析事件
  • EmailSenderHostedService - 体积小,非临界
  • MarkdownReAddPostsService - 仅启动启动,配置配置

如果规模扩大,应移到工人服务部门,如果:

  • BrokenLinkCheckerBackgroundService - 提出许多HTTP要求
  • SemanticIndexingBackgroundService - 呼叫外部嵌入的API

已在单独服务中:

  • Mostlylucid.SchedulerService - 点火仪表仪表板和通讯发送

这是一个务实的方法: 开始简单( 正在处理中) , 当有需要的证据时分割 。

" 超越基础:燃火 "

虽然 IHostedServiceBackgroundService 有时你需要更精密的日程安排。那是图书馆喜欢的地方。 枪火 进来吧

枪火提供:

  • 持久性工作排队 - 工作活过应用程序的重新启动
  • 经常性工作 - 克罗式日程安排
  • Dashboard UI 纸板UI - 看看什么在运行,什么在失败,重试工作
  • 分配执行 - 多个服务器可以处理相同的工作队列
  • 自动自动重试 - 以指数反弹自动重试失败的工作

以下是一个简单的例子:

// In Program.cs
builder.Services.AddHangfire(config => config
    .UsePostgreSqlStorage(connectionString)
    .UseRecommendedSerializerSettings());

builder.Services.AddHangfireServer();

var app = builder.Build();

// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
    .AddOrUpdate<NewsletterService>(
        "send-daily-newsletter",
        x => x.SendDailyNewsletter(),
        Cron.Daily(17)); // 5 PM every day

您的服务只是普通的班级:

public class NewsletterService
{
    private readonly IEmailService _emailService;
    private readonly ISubscriberRepository _subscribers;

    public NewsletterService(
        IEmailService emailService,
        ISubscriberRepository subscribers)
    {
        _emailService = emailService;
        _subscribers = subscribers;
    }

    public async Task SendDailyNewsletter()
    {
        var subscribers = await _subscribers.GetDailySubscribersAsync();

        foreach (var subscriber in subscribers)
        {
            await _emailService.SendNewsletterAsync(subscriber);
        }
    }
}

点火手柄:

  • 确保工作在预定时间进行
  • 失败时重试
  • 正在存储执行历史
  • 提供跟踪仪表板以监测一切
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

    style A stroke:#059669,stroke-width:3px,color:#10b981
    style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
    style E stroke:#059669,stroke-width:3px,color:#10b981
    style H stroke:#dc2626,stroke-width:3px,color:#ef4444

何时使用点火 :

  • 您需要能够生存的持久工作队列 。
  • 您想要一个用于监控和手动触发任务的仪表板
  • 您需要通过多个服务器分配工作处理
  • 您想要内置重试逻辑和失败处理
  • 您需要对重复性任务的日式日程安排

与 IHED Services/ BackServices / BackServices 粘贴时:

  • 您需要精细控制服务寿命周期
  • 您的服务需要对实时事件作出反应
  • 您想要最小化依赖关系
  • 你正在做一个简单的周期性任务 不需要坚持

其他选项

其它图书馆也值得考虑:

Quartz.NET:

  • 比 " 燃火 " 更灵活的日程安排
  • 支持 cron 表达式和日历列表列表
  • 可持续使用多个数据库
  • 更复杂的API,但更强大

大规模过境/NSerpiceBus 服务车:

  • 全功能化信息总客车实施
  • 更好地使用分布式系统和微观服务
  • 支持 sagas( 长期工作流程)
  • 更深的学习曲线

分度函数函数/AWS 兰巴达:

  • 如果你在云中,请考虑无服务器
  • 每执行一次支付薪金,而不是维持服务运行
  • 自动缩放
  • 寒冷起动时一些潜潜伏费用

摘要摘要摘要

在第一部分中,我们在ASP.NET核心中涵盖了背景服务的基本方法:

  1. 长期服务 - 基础、最大灵活性
  2. 背景服务 - 长期循环的方便基级
  3. 启动协调 - 使服务互相等待
  4. 分配分配式协调 - 使用 Redis 用于多个系统设想情景
  5. 枪火 - 需要长期工作和精密日程安排时

最重要的教训是:

  • 总是在 StopAsync 中完成频道并取消标记
  • 决定 StartAsync 是否应该立即阻止还是立即返回
  • 优雅的例外
  • 使用任务 。 当任何带有关机标记来遵守关机超时时 。

第二部分 第二部分我们将从一个制作博客平台上 审视现实世界的执行情况:

  • 将标记下载文件同步到数据库的系统监视器
  • 配有重试策略和断路器的电子邮件发送器
  • 批批量请求的分析事件队列
  • 自动处理内容的语义搜索索引器
  • 定期验证外部 URL 外部 URL 的断断链接检查器

这些例子表明了第一部分行动的模式,包括启动协调模式和适当的关闭处理。

继续阅读

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.