Back to "ASP.NET核心背景服务 *- Part @1: 方法"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Core BackgroundService Hangfire IHostedService

ASP.NET核心背景服务 *- Part @1: 方法

Thursday, 27 November 2025

每个现代的网络应用程序都有工作, 都应该“ ' @ t stlock a HTTP request” MS K1}%sendemails,}处理文件@,{与外部服务同步},*MSK4}{正在运行预定的维护}.}ASPMSKO6NET Core 从简单角度提供多种方法来处理此背景工作}♪, IHostedService 执行像Hangfire@.这样的复杂框架。 在此第一部分中, @, we}'将探索基本模式以及何时使用这些基本模式和时间。#.

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

这个网站「(a BLOG站点 @!)}有超过一半人做讽刺的背景工作@,, 背景服务是现代网络应用程序的未上传英雄@. @% 虽然您的控制器在前景@ ,} 背景服务中处理 HTTP 请求, 静悄悄地处理已排队的电子邮件 MS K2}# 索引内容, 用于搜索@ I,}检查外部 APIs,}清理临时文件 @ ,并处理无数其他任务, 否则会阻塞您的请求管道.}

在这两集“-Part系列”中, ,}(我们)' 会探索在ASP#.NET Core{,}内实施背景服务的不同方法。 IHostedService 和 BackgroundService 在“Hangfire”和“M1,”部分中,我们“'”将研究基本方法及其特点 `.'在 部分#2“, ”我们“'”将跳入一个生产代码库“.”的世界执行中。

重要的: 我们特别注意生命周期管理 特别是常被忽略的 StopAsync 方法@, 许多开发商在关闭应用程序时遇到加密例外 @.

为何提供背景服务@? @%

在跳进“"”之前 让我们简单考虑一下 “"”,为什么? ."?

  1. 卸载慢速操作 - 唐't 讓使用者等待您發送郵件或產生 PDF
  2. 附表所列经常性任务 {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}每天晚上在"2 am.
  3. 进程队列 - 处理来自频道或信息经纪人的信息
  4. 监视外部状态 -polll APIs 或监视文件系统以备更改
  5. 协调复杂的工作流程 - 管理跨分钟或小时的多@ MS K1 step 进程

ASP.NET核心为提供这些服务提供了几种实施方法 =,}每种服务都有不同的交易方式

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

在您的网络应用程序中运行背景工作时, @"@ " @ @ @ MS K2pre @ MPK3}通常被认为是一个坏主意 @ I. @ 常规智慧是 @: @ @ *"# web 服务器处理网络请求@._ 背景工作属于另外的服务器\ MSKO8}

这是基于真正的技术限制 :

单星际纪事

早期网络服务器 @( @ and honest now @MS K1} @ 'cheap'# Azure service@)通常运行 单 -Core 或 双 {-} 核心 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 线索@ - @ per @ MS K1 @ 请求@._ 线串池相对较小, 通常是@(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 应用池回收利用

IMS将积极回收应用池@( @restart your app)}根据内存限制,}请求计数{,}或时间计划 <.> 背景工作将会在中=-}操作}:}失败

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

Async=/}等待支援

在“.NET”之前, “4.5Q @(2012),QAsync 编程是‘(’或‘相对的’)Q}痛苦的{.ZA 背景任务经常不必要地堵住线条_:#

// 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. 多-Core 是可负担的

这个博客在专用的“8-”核心服务器上运行, 成本低于可比Azure VMQ—,

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. @Async@/# 等待无处不在

现代的 @.NET 使得 Async 编程变得微不足道@.}背景任务可以等待在 IQ/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 重新可靠恢复的服务
  • 视窗服务 @ - 适当的长效@- 运行过程模型

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

这意味着一个离家出走的背景任务 can't 饿死你的网络阶梯@.

问题不再在于 @"#我们能否在我们的网络 App?"}中提供背景服务,我们是不是该做点什么@"_We'}在不使用背景服务时,

内建@- @in 选项

基金会

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

启动 Async@ : 同步对非同步开始

执行时的一项关键决定 IHostedService 是否是您的 StartAsync 方法是否应该设置块块或立即返回@. @%

同步@( @ 阻塞 @ )_ 启动 @ I: @

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;
}

asynchronous @ ( @ non @ - @ 阻塞@ MS K2 @ 开始 @ MPK3 @

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
    }
}

何时使用每种方法@: @%

  • 屏蔽中 @ : @% 当服务在应用程序能够处理请求前必须完成初始化时@ ( @ e @ MS K1} g.,} 装入关键配置@ MPK3} 升温缓存=)}
  • 不是- 阻塞@: @ 当服务可在背景中初始化时, 当其他服务开始为现有内容编制索引时 =% ,}{与外部服务同步})}

停止 Async: 常见陷阱

这里#' @%s 在这里事情变得有趣 —}和很多开发者遇到问题 K2}当你的应用程序关闭时 #,}ASP=.NET核心电话 StopAsync 在所有主机服务@. 您有一个有限的窗口 <% ( default {5} 秒@ MS K3_ 您可以在 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);
        }
    }
}

当您运行此服务并停止您的应用程序时 , 您' _ 这将看到错误如 @ MS K2 *

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. 完成渠道 - 如果您使用频道 <'fs18\bord1\shad0\cH00FFFF\b0} Writer.Complete()
  3. 等待背景任务 - 使用 Task.WhenAny 关闭取消代号
  4. 处理操作 - 这是预期的,应该被抓住
  5. Donó't 丢弃例外 - 例外 StopAsync 可能造成不可预测行为

背景服务=: 方便基地级

写书 IHostedService 执行可以是重复的@. 您总是需要一个背景任务 @ , 一个取消信号源,和相同的清理模式@ MS K3 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 Services

使用 BackgroundService 当:

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

使用 IHostedService 当:

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

高级:启动协调

有时您需要服务等待对方@. @ 例如 @ ,} 您可能希望您的语义搜索索引器等待您的标记文件处理器完成初始装入@ MS K2}

用于协调服务启动的模式@: @%

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的分布式协调

启动协调员在一个单一的应用程序实例中工作@ . @ but what happens when you size to discription 多个实例 MS K1}您不想要三例同时执行相同的计划任务=.}

重新重订 提供简单解决方案@: @ 使用标记 @ (} keys)}来协调谁做了什么?

简单领袖选举

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
  • 数据库迁移 @-_ 启动时只应该有一个例子运行迁移

对于更复杂的假想 {(}%multi-step job},可靠地排程 跨重开_),考虑Hangfire(使用数据库后端)自动处理分布式锁定}MPK4

当不使用背景服务时

在我们潜入更先进的工具之前,比如Hangfire=,* Let'}让我们来谈论你什么时候 {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}应该的 在您的主网络应用程序中使用背景服务@. @%

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

您的网络应用程序中运行的背景服务与您的 HTTP 请求共享资源@. 这将引发问题 @:

1.资源内容

问题M: 您的背景服务消耗了相当大的 CPU@ , @ memory @,}或数据库连接@.

// 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);
        }
    }
}

当网络请求在转码期间到达时@, @ 它们 @ ' re slow, 因为 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

不同的扩大要求

问题M: 您的背景工作需要与您的网络阶梯不同的缩放@ . @

  • Web 梯度: HTTP 交通量缩放@ @ might need {10} 在晚上2}(可能需要在白天的例數 _, _)
  • 背景阶梯: 排队深度缩放 @ ( @ might need {1} 可能需要@ MS K1} 在处理批量@ MPK4} 时通常情况下,}\ 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”网站实例, 才能处理通讯“MS K2”浪费资源@. @

部署独立

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

// 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. ” 不同的失败域

问题M: 您背景服务中的一个错误崩溃了整个 Web 应用程序@.

// 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);
    }
}

如果背景工作在另外的进程中 @ ,, 它可以在不影响网络请求的情况下崩溃和重新启动@ MS K1

如何乘法背景服务

当您决定在此分割“ , ” 时, “ MS K1 ” 是推荐的架构“ I: ”

选项@1: @ @ @ .#NET 工人服务

使用工人服务模板创建新工程@:

dotnet new worker -n YourApp.Worker

结构#:

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

程序#._cs}:

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

具有共享队列的单独工程

使用消息队列来拆分网络和工人@: @%

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);
        }
    }
}

流行信息队列选项@: @%

多种专业工人

对于复杂系统来说 , 由责任分担

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

每个工人都能够

  • 独立缩放
  • 独立部署
  • 使用不同资源@(email工人需要 SMTP},}视频工作者需要GPU}MS K2}
  • 具有不同的监测和警报

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

尽管存在上述情况, ,}(有些假想对于 in @-}进程背景服务来说是完全正常的

轻量度定期任务

// 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;
    }
}

频道@- @ 基于队列 @ (_ for non\ MS K2 @ 关键工作 @ MPK3}

// 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();
    }
}

决定矩阵

| 特点 @| 保留在 WebApp @MS K2 移动到工人服务@| |---------------|-----------------|------------------------| @| CPU 每次操作使用 | < @#100} @MS K4 @_> @ @#1第二集 @| @|每个操作的記憶體 @ | @ @ < @ *10□ MB @ *| @\ MS K5# @ I100MT@|# |}频率 @|#周期性@(#minutes @MS K3_hours @)}|{连续或高 MSKO6}频度 @|} -=YTET -伊甸园字幕组=- 翻译: @| @ 持续时间 @ | @ 秒@ MS K2 @ 分钟到小时 @ MPK3 @ 工作队列深度 @|# “|”示例“| @Cache warming,}配置重新加载@|}视频处理 ‘,’大宗进口 □|

博客平台

在博客平台的代码 我们检查部分2:。

保存在网络应用程序 App:

  • MarkdownDirectoryWatcherService -轻量级文件监视器
  • UmamiBackgroundSender 快速分析事件
  • EmailSenderHostedService @ - @ 小容量 @ I, @ non@ MS K2 @ 关键
  • MarkdownReAddPostsService @ - @ 启动@ MS K1 @ 仅 ,} 配置 @ MPK3 @ gateted

如果规模扩大,应该迁移到工人服务@: @%

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

已经在单独服务中@: @%

  • Mostlylucid.SchedulerService 校对:Portnoy

这是一个务实的方法 @ : @ 开始简单的@ MS K1_ in @ MPK2} process\ ),} 当您有证据需要时, 请将此方法分割为% .

校对:Portnoy

虽然 IHostedService 和 BackgroundService 您拥有和控制的服务是优秀的, 有时您需要更精密的日程安排@ . @ that'}在图书馆喜欢的地方 枪火 进来吧!

校对:Portnoy

  • 持久性工作排队 - 工作在应用程序恢复运行后生存
  • 经常性工作 -Cron-风格排程
  • Dashboard 界面 UI - 查看正在运行的 @,} 什么是' 失败@, retry job
  • 分配执行 - 多功能服务器可以处理相同的工作队列
  • 自动自动重试 & -\ 失败的工作会自动以指数反转方式重试 。

在这里,' @%s a simple example

// 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

何时使用 Hangfire @: @

  • 您需要能够生存的持久工作队列 。
  • 您想要一个用于监控和手动触发任务的仪表板
  • 您需要通过多个服务器分配工作处理
  • 您想要在重试逻辑和处理失败时创建@ - @ in retroll
  • 您需要重现任务的风格排程

何时继续使用 IHosted Services @/ @BackFlorService@:

  • 您需要精细控制服务寿命周期
  • 您的服务需要对真实事件的反应@-_time
  • 您想要最小化依赖关系
  • 您正在构建一个简单的周期性任务, 不需要持续时间@ ' @ title status

其他选项

校对:Soup

Quartz.NET:

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

大规模过境/NSerpiceBus 服务车:

  • complete -_
  • 更好地使用分布式系统和微观服务
  • 支持 sagas @ ( @ long_ - @ 运行工作流程@ MS K2
  • 更深的学习曲线

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

  • 若您在云中 @ 're in the cloud *,考虑无服务器
  • 每执行一次支付薪金,而不是维持服务运行
  • 自动缩放
  • 寒冷起动时一些潜伏电压

摘要摘要

在“1,”部分中,我们'* 已经涵盖了ASP_.NET Core中关于背景服务的基本方法。

  1. 长期服务 最大灵活度
  2. 背景服务 @ -_ 方便的基础级, 用于 longQ-_ 运行环
  3. 启动协调 让服务互相等待
  4. 分配式协调 @ -} 使用 Rediis 用于多@ MS K1 @ instance 假想
  5. 枪火 当您需要持续的工作和复杂的日程安排时, -

最重要的课目: :

  • 总是在 StopAsync 中完成频道并取消标记
  • 决定 StartAsync 是否应该立即封锁或返回
  • 优雅的例外
  • 使用任务@ . @ 当任何带有停机符以遵守关闭超时时时 。 @ info/ plain

内 部分#2@,_We'}翻譯制作部落格平台上的世界實現: possible@-BARBAR

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

这些例子显示了行动中的“1”部分的模式,包括启动协调模式和适当的关闭处理@.}

继续阅读

logo

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