Hintergrundtjänster i ASP.NET Core - Del 1: Angreppssätt
Alla moderna webbapplikationer har arbete som inte bör blockera en HTTP-ansökning. IHostedService implementeringar till sofistikerade ramverk som Hangfire.
Inleiding
Bekännelse; Jag gillar bakgrundstjänster en hel del, den här sidan M SK2 en BLOGsida !) har mer än en halva sida - ett dussin av dem som gör allvarliga bakgrundsuppgifter~ , men som allt annat har de några ODDITIES och metoder som kommer göra att din användning av dem blir mycket mer njutningsrik Hintergrundtjänsterna är de okända hjältarna i moderna webbapplikationer.
I dessa två delar-serier, , och ', ska vi utforska de olika metoderna för att implementera bakgrundstjänster i ASP-programet. IHostedService och BackgroundService abstraktioner till mer sofistikerade lösningar som Hangfire Del 2, vi ' dyker in i verkliga implementeringar från en produktionskodbas
viktigt: Vi kommer att lägga speciella uppmärksamhet på livscykelanalys, särskilt de ofta överskattna
StopAsyncmetod, som är där många utvecklare stöter på kryptiska undantag när deras applikationer slutar
Varför Hintergrundtjänster?
Innan du dyker in i " hur ," låter' tar du en titt på | why| ."| bakgrundstjänsterna som låter dig
- Avlappning av långsamma operationer - Låt inte användarna vänta medan du skickar mejl eller genererar PDFs
- Timma upprepade uppgifter - Klär upp gamla rekord varje natt vid 2 klockan
- Process köer - Handla meddelanden från kanaler eller meddelandesmakare
- Att övervaka den externa tillståndet - Poll APIs eller titta på filesystem för ändringar
- Koordinera komplexa arbetsflöden - Mana flera processer som spanner minuter eller timmar
ASP.NET Core erbjuder flera tillvägagångssätt för att implementera dessa tjänster
Den historiska kontexten: Varför är bakgrundstjänsterna nu möjliga
På " gårdagens tid var det generellt sett dåligt att köra bakgrundsarbete i din webbapplikation.
Det var inte bara last, det var kunskap, det baserades på riktiga tekniska begränsningar.
Den ensamma-korens era
Framgångswebserver ( och uppriktigt sagt nu , | | ' | billiga | МSK3 | Azure-tjänster ensamma-core eller duella-core CPUs. Om du körde en CPU-intensiv bakgrundsuppgift
Single Core (2005):
┌─────────────────────┐
│ Background Task │ ← Uses 80% CPU
│ (80% of core) │
├─────────────────────┤
│ Web Requests │ ← Only 20% left!
│ (20% of core) │ ← Slow responses
└─────────────────────┘
Resultat: Ditt hemsida blev långsamt när bakgrundsarbeten började
Thread Pool svält
Klassisk ASP.NET som används thread-per-fråga. Threadpoolen var relativt liten (25-100 trådar vanligtvis ), och bakgrundsuppgifterna skulle stjäla trådar som borde hantera webbbehov
// 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-anwendungspool återvinning
IIS skulle agressivt återvinna applikationspooler. (uppstarta din app, ) baserat på minnesgränser, , inmatning av begäran, M SK3 eller tidsramar,. bakgrundsarbeten skulle dödas mitt i tiden.
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
- Background task killed
- Work lost, must start again
Limited Async/ Vänta till stöd
Innan .NET | | 4.5 | МSK2 | async programmering var | ( | relativt | мsk4 | smärtsamt | . | bakgrundsuppgifter blockerade ofta trådar onödigt
// 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
Vad förändrade modern tid
Idag är landskapet dramatiskt annorlunda.
1. Multi
Ekonomin har förändrats. Cloud VMs med flera kärnor är antagligt dyra , och barmetallserver är överraskande billigaM SK2 Denna blogg kör på en dedikerad 8- kärnserver som kostar mindre än en jämförbar Azure VMMska4 och jag får alla dessa kärner till mig självM Ska5 inga bullrande grannarM ska6 En bakgrundsuppgift på ett kärn påverkar inte signifikant webbansökningar på andra kärner M Ska8
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
Moderna .NET gör att async programmering är triviell . bakgrundsuppgifter kan vänta på I/O utan att blockera trådar
// 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. Bättre Process Hosting
- Docker - Hintergrundtjänster i behållare får inte
- Kuberneter - Rättvis elegant avstängning med
SIGTERM - systemat - Linux-tjänster som återstartar pålitligt
- Windowstjänster - Rätt lång modell
4. Kanaler och moderna primitiver
.NET har nu den första -klassens support för samtida programmering med 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. Ressourcengränser och C-grupper
Moderna containerorchester låter dig begränsa användandet av resurser:
# 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
Det betyder att en springande bakgrundsuppgift kan inte svälta din webbnivå.
Frågan är inte längre "Kan vi köra bakgrundstjänster i vår webbapp, utan ?"Borde vi" Vi ' tar en titt på den här päätöksen i sectionen "" När inte använda bakgrundstjänsterna "" , ", senare ".".
De byggda options
IHostedService: Fonden
På dess kärna, ,, implementerar alla bakgrundstjänster i ASP IHostedService. Den här gränssnittet är vackert enkelt
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Det är "'", det är , . , två metoder, . StartAsync kallas när din application börjar, och StopAsync när den slutar.
Registrera din tjänst i Program.cs:
builder.Services.AddHostedService<MyBackgroundService>();
Här är livscykeln visualiserad
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
StartAsync: Synkrona mot Asynchrona Start
Ett viktigt beslut när man implementerar IHostedService är om du StartAsync metoden borde blockera eller återvända omedelbart.
Synkroniserad (blocking) start
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;
}
Asynkrona (non-blockingM SK2 start
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
}
}
När man ska använda varje metod:
- Blockering: När tjänsten måste avsluta initialisering innan applikationen kan hantera begäran
- icke-blocking: När tjänsten kan initialiseras i bakgrunden medan andra tjänster börjar (e.gM SK2 indexera den existerande innehållet
StopAsync: Det vanliga fallet
Här' är där saker blir intressanta — och där många utvecklare stöter på problem StopAsync på alla hostade tjänster. Du har ett begränsat fönster (default 5 sekunderM SK3 för att städa eleganttMSC4 Du kan utöka det i Program.cs:
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
Den vanligaste felet:
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);
}
}
}
När du kör den här tjänsten och stoppar din application, kommer du att se fel som:
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
Eller så hänger applikationen bara upp för avstängningstid innan den tvingat avslutas.
Den rätta metoden:
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;
}
}
}
}
Nyckelpunkter för korrekt implementation av StopAsync:
- Signalavbrytning - Använd en
CancellationTokenSourceoch avbryta den - kompletta kanaler - Om du
Writer.Complete() - Vänta till bakgrundsuppgifter - Använd
Task.WhenAnymed Shutdown cancellation token . - Handle OperationCanceledException - Det här är förväntat och borde fångas
- Don' släng inte undantag - undantag i
StopAsynckan orsaka oförutsägbart beteende
BackgroundService: Den bekväma basklassen
Att skriva IHostedService implementeringar kan vara repetitiva. Du behöver alltid en bakgrundsuppgift BackgroundService hanterar denna boilerplatta för dig
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();
}
}
Du implementerar bara ExecuteAsync och låter basklassen sköta röran
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);
}
}
När man använder BackgroundService vs IHostedService
Använda BackgroundService när:
- Du behöver en lång-running bakgrundsloop
- Du vill ha en enkel periodisk exécution
- Du behöver ingen fin kontroll över
StartAsync/StopAsyncTiming
Använda IHostedService när:
- Du behöver kontrollera exakt vad som händer i
StartAsyncmot bakgrundsarbete - Du' sätter upp handlingar för händelser eller observatörer snarare än ett kontinuerligt kretslopp
- Du behöver koordinera med andra tjänster under uppstarten
Advanced: Startkoordination
Ibland behöver du tjänster att vänta på varandra. Till exempel
Här är en mönster för att koordinera serviceuppstartning
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);
}
}
Tillämpning i en tjänst:
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;
}
Detta mönster blir särskilt användbart när du har flera bakgrundstjänster med interdependencies.
Distribuerad koordinering med Redis
Startkoordinatorn arbetar inom en enda applikations instans.
Redis erbjuder en enkel lösning
Enkel ledareval
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);
}
}
}
Distribuerad låsning för kritiska sektioner
För uppgifter som inte måste fungera samtidigt över instanser:
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");
}
}
När man ska använda fördelad koordinering
- Avsedda uppgifter - Bara en instance ska skicka den dagliga newslettern
- Processning av köer med ordning - Se till att meddelanden behandlas i ordning
- Ressourcen-intensiva operationer - Att förhindra flera instanser från att överväldiga en extern API
- Migrationer av databaser - Bara en instans borde köra migrationer vid startup
För mer komplexa scenarior (multiM SK1stegarbeten , tillförlitlig avvägning över återstartar ), tänk på Hangfire som hanterar distribuerad lådning automatiskt med sin databas baksida
När inte använda bakgrundstjänster
Innan vi dyker in i mer sofistikerade verktyg som Hangfire borden't använda bakgrundstjänster i din huvudapplikation.
tecknader du borde dela till ett separat projekt
Hintergrundtjänster som körs i din webapplikation delar resurser med ditt HTTP-ansökningskedjan. Det kan orsaka problem
1. Berättelse om resurser
Problemet: Din bakgrundstjänster konsumerar signifikant CPU, minne, eller datauppkopplingar
// 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);
}
}
}
När webbbehov kommer fram under transkodering, är de långsamma eftersom CPUn är upptagna.
Lösningen: Gå till en separat arbetarenhet:
# 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. Skillnade skalningsbehov
Problemet: Din bakgrundsarbete behöver en annan skalning än din webbnivå.
- Web-nivån: Skala för HTTP-trafiken (modig att behöva ♫ 10 ♫ instanser under dagen ♫
- bakgrundsnivå: Skala för ködjupet (modig att behöva 1 instans vanligtvisM SK2 20 när man bearbetar en bunt)
Om de är i samma process, kan du inte skala dem oberoende av varandra.
Ett exempel scenario:
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
Att sätta in bakgrundstjänster i din webbapp betyder att du måste köra 20 webbinstantioner bara för att hantera newslettern
3. Deployment Independence
Problemet: Du vill implementera webbförändringar utan att starta tillbaka bakgrundstjänsterna ( eller vice versa).
// 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);
}
}
Varje utplacering avbryter importen. flytta den till en separat tjänst för arbetare som du utsätter oberoende av dig
4. Olika misslyckande domän
Problemet: En bug i din bakgrundstjänster kraschar hela webapplikationen.
// 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);
}
}
Om bakgrundsarbeten är i en separat process, kan det krascha och återstarta utan att påverka webbbehovet.
Hur man påverkar bakgrundstjänsterna
När du bestämmer dig för att dela upp
Option 1: .Networker Service
Create a new project using the Worker Service template:
dotnet new worker -n YourApp.Worker
Strukturen:
/YourApp.Worker
/Services
VideoTranscodingService.cs
EmailSenderService.cs
/Program.cs
/appsettings.json
Programm .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();
Deploy separat:
# 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
Option 2: Separera projekt med gemensam kö
Använd en meddelandens kö för att avkoppela webb och arbetare:
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
Webapp köer arbetar
// 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
}
}
arbetare konsumerar från köen:
// 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);
}
}
}
Beliebta meddelandens queue-optioner:
- RabbitMQ - Den mest populara , egenskaper
- Azure Service Bus - Om du är på Azure
- AWS SQS - Om du är på AWS
- Redis Streams - Enklare , bra för mindre skala
Option 3: Flera specialiserade arbetare
För komplexa system
/YourApp.Web # HTTP requests only
/YourApp.EmailWorker # Sends emails
/YourApp.VideoWorker # Transcodes videos
/YourApp.ReportWorker # Generates reports
/YourApp.Scheduler # Runs scheduled jobs (Hangfire)
Varje arbetare kan
- Skala oberoende av
- Deploy självständigt
- Använda olika resurser (emailarbetare behöver SMTP , videoarbetar behöver GPU)
- Ha olika övervakning och varning
När man ska behålla bakgrundstjänsterna i sin webbapp
Trots det ovanstående, är vissa scenarior helt bra för i process bakgrundstjänster
Liteviktiga periodiska uppgifter
// 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);
}
}
}
Event lyssnare
// 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;
}
}
Kanaln-Publikerade köer ( för icke-- kritiskt arbete )
// 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
}
Startkoordination
// 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();
}
}
Bestämmande matrix
| Charakteristiska | Håll i en webbapp | Övergå till arbetstjänsterna | |||||||
|---|---|---|---|---|---|---|---|---|---|
| CPU-användbarhet per operation | < | ||||||||
| Memory per operation | < | 10 | MB | ||||||
| Frequens | periodisk (minuterMSG3 timmarMSC4 MSG5 Kontinuös eller hög | ||||||||
| Kritiskhet | icke | - | kritiskt | МSK3 | nödvändigt ♫ | ||||
| Tydlighet | sekunder | minuter till timmar | |||||||
| Skalar med | webbflödet | Gapet i arbets köen | |||||||
| exempel | Cachewarming | МSK2 | config reload | Videoprocessing ♫ , | stora importer ♫ |
Verklig-World Example: Blogplattformen
I bloggplattformen vars kod vi undersöker i del 2:
Bevarat i webapp:
MarkdownDirectoryWatcherService- Litevikt-filer-loggUmamiBackgroundSender- Vinniga analytiska händelserEmailSenderHostedService- Mindre volymen , ickeM SK2 kritisktMarkdownReAddPostsService- Starting - endast , Konfiguration
Ska flytta till arbetarnas tjänst om skalan ökar:
BrokenLinkCheckerBackgroundService- gör många HTTP-behovSemanticIndexingBackgroundService- Aktiverar extern embedding API
Ännu i separat tjänst
Mostlylucid.SchedulerService- Hangfire-dashboard och newslettersändning
Det här är ett pragmatiskt angreppssätt.
Förutom grunderna: Hangfire
Medan IHostedService och BackgroundService är utmärkta för tjänster du äger och kontrollerar Hangfire komma in.
Hangfire erbjuder:
- Hållliga jobb köer - jobb överlever application restarts
- Redaktiva jobb - Cron
- Dashboard-UI - Se vad
- Distribuerad begärelse - Flera server kan bearbeta samma arbetslinje
- Automatiska repetitioner - Misslyckade jobb prövas automatiskt med exponentiell bakslag
Här är ett enkelt exempel
// 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
Din tjänst är bara en vanlig klass:
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);
}
}
}
Hangfire-handlar:
- Att säkerställa att jobbet körs i den avsedda tidpunkten
- Att försöka igen om det misslyckas
- Att spara inrättelsehistorien
- Tillhandahålla ett dashboard för att övervaka allting
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
När man ska använda Hangfire:
- Du behöver konstanta jobb queues som överlever återstarten
- Du vill ha ett dashboard som kan övervaka och manuell utlösa uppgifter
- Du behöver fördelade arbetsprocesser över flera server
- Du vill ha byggt-in återtestningslogik och felhantering
- Du behöver cron-stils avvägning av upprepade uppgifter
När man ska sticka med IHostedService/BackgroundService :
- Du behöver en fin kontroll över service livscykeln
- Din tjänst behöver reagera på händelser i realtid
- Du vill minimera beroenden
- Du bygger en enkel periodisk uppgift som inte kräver uthållighet.
Andra Optioner
Medan Hangfire är populärt finns det andra bibliotek värda att tänka på
- Flexibler planering än Hangfire
- Ondersteuner crontryck och kalender-baserad programmering
- Kan överleva till flera databaser
- Mer komplext API men mer kraftfullt
- Full-featured message bus implementations
- Bättre för fördelade system och mikrotjänster
- Supporting sagas (long-drivande arbetsflöden )
- Steeper learning curve
- Om du är i molnet
- Belöna per utföring istället för att hålla en tjänst igång
- Automatiska skalering
- Lite latens överhead för kalla uppgångar
sammanfattning
I delen 1, har vi ' tagit in de grundläggande tillvägagångssätten för bakgrundstjänsterna i ASP
- IHostedService - grunden , max flexibilitet
- BackgroundService - Praktisk basklass för långa loopar
- Koordinering för att starta - Att låta tjänster vänta på varandra
- Distribuerad koordinering - Att använda Redis för flera scenarior
- Hangfire - När du behöver konstanta jobb och sofistikerad avvägning
De viktigaste lärdomarna:
- Att alltid komplettera kanaler och avfärda tokens i StopAsync
- Bestämma om StartAsync borde blockera eller återvända omedelbart
- Handle OperationCanceledException elegant
- Använd uppdragt.WhenAny med avstängningstoken för att respektera avstämpningstimeouts
I Del 2, vi ' kommer att undersöka verkliga implementeringar i världen från en produktionsbloggplattform
- Att synkronisera markerdown-filer till en databas
- E-post-sändare med återsökande regler och kretsbrötare
- Analytik-händelserna i queues som ett slag kräver
- Semantiska sökindexer som processar innehåll asynkront
- Breckade länkarna som periodiskt validerar externa URLs
De här exempelna visar mönster från del 1 i handlingen , inklusive startkoordinationsmönster och korrekt avbrytningshantering