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
Hintergrunddienste im ASP.NET Core - Teil 1: Die Ansätze
Thursday, 27 November 2025
Jede moderne Web-Anwendung hat Arbeit, die eine HTTP-Anfrage nicht blockieren sollte – E-Mails aussenden, Dateien verarbeiten, mit externen Diensten synchronisieren, geplante Wartung durchführen. ASP.NET Core bietet mehrere Ansätze für die Bearbeitung dieser Hintergrundarbeit, von einfachen IHostedService Implementierungen zu ausgeklügelten Frameworks wie Hangfire. In diesem ersten Teil werden wir die grundlegenden Muster erforschen und wann wir jedes einzelne verwenden.
Einleitung
Confession; I like Background Services a LOT, diese Website (eine BLOG-Website!) hat mehr als eine halbe Dutzend von ihnen tun kariöse Hintergrundaufgaben, aber wie alles, was sie haben einige ODDITIES und Praktiken, die Ihren Gebrauch von ihnen viel angenehmer machen. Hintergrunddienste sind die ungesungenen Helden moderner Web-Anwendungen. Während Ihre Controller HTTP-Anfragen im Vordergrund bearbeiten, verarbeiten Hintergrunddienste leise Warteschlangen-E-Mails, Indexinhalte für die Suche, überprüfen externe APIs, bereinigen temporäre Dateien und behandeln unzählige andere Aufgaben, die sonst Ihre Anfrage-Pipeline blockieren würden.
In dieser zweiteiligen Serie werden wir die verschiedenen Ansätze zur Implementierung von Hintergrunddiensten in ASP.NET Core aus dem integrierten IHostedService und BackgroundService Abstraktionen zu anspruchsvolleren Lösungen wie Hangfire. In Teil 1 werden wir die grundlegenden Ansätze und deren Eigenschaften untersuchen. Teil 2Wir tauchen in Real-World-Implementierungen von einer Produktionscodebase ein.
Wichtig: Wir werden dem Lebenszyklusmanagement besondere Aufmerksamkeit widmen – vor allem den oft überblickten
StopAsyncMethode, wo viele Entwickler auf kryptische Ausnahmen stoßen, wenn ihre Anwendungen heruntergefahren werden.
Warum Hintergrunddienste?
Bevor wir in das "Wie" tauchen, lassen Sie uns kurz das "Warum" betrachten. Hintergrunddienste lassen Sie:
- Langsamer Betrieb ausladen - Lassen Sie Benutzer nicht warten, während Sie E-Mails senden oder PDFs generieren
- wiederkehrende Aufgaben planen - Bereinigen Sie die alten Platten jeden Abend um 2 Uhr
- Warteschlangen verarbeiten - Verwalten von Nachrichten von Kanälen oder Nachrichten-Broker
- Externen Zustand überwachen - Poll APIs oder Dateisysteme für Änderungen ansehen
- Komplexe Workflows koordinieren - Mehrschritt-Prozesse verwalten, die Minuten oder Stunden umfassen
ASP.NET Core bietet verschiedene Ansätze für die Implementierung dieser Dienste, jeweils mit unterschiedlichen Kompromissen.
Der historische Kontext: Warum Hintergrunddienste jetzt machbar sind
In den „alten Tagen" (vor 2010) galt die Durchführung von Hintergrundarbeiten in Ihrer Web-Anwendung im Allgemeinen als schlechte Idee. Die herkömmliche Weisheit war: „Web-Server bearbeiten Web-Anfragen. Hintergrundarbeiten gehören auf einem separaten Server."
Das war nicht nur Fracht-Kult-Weisheit – es basierte auf realen technischen Einschränkungen:
Die Ein-Kern-Ära
Frühe Web-Server (und ehrlich gesagt jetzt, 'billige' Azure-Dienste) lief in der Regel auf Single-Core- oder Dual-Core-CPUs. Wenn Sie eine CPU-intensive Hintergrundaufgabe ausgeführt, es direkt mit Web-Anfragen für den gleichen Kern konkurriert:
Single Core (2005):
┌─────────────────────┐
│ Background Task │ ← Uses 80% CPU
│ (80% of core) │
├─────────────────────┤
│ Web Requests │ ← Only 20% left!
│ (20% of core) │ ← Slow responses
└─────────────────────┘
Ergebnis: Ihre Website wurde träge, als die Hintergrundarbeit einsetzte.
Fadenpool-Verhungerung
Klassisches ASP.NET verwendet Thread-per-Request. Der Thread-Pool war relativ klein (25-100 Threads typischerweise), und Hintergrundaufgaben würden Threads stehlen, die Web-Anfragen bearbeiten sollten:
// 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-Recycling
IIS würde Anwendung Pools aggressiv recyceln (neu starten Sie Ihre App), basierend auf Speicherlimits, Request-Counts oder Zeitplänen. Hintergrundarbeit würde Mitte der Operation getötet werden:
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
- Background task killed
- Work lost, must start again
Begrenzter Async/Await-Support
Vor .NET 4.5 (2012) war die async-Programmierung (relativ) schmerzhaft. Hintergrundaufgaben blockierten Threads oft unnötig:
// 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
Was sich verändert hat: Die Neuzeit
Die heutige Landschaft ist dramatisch anders:
1. Multi-Core ist erschwinglich
Die Wirtschaft ist umgedreht. Cloud-VMs mit mehreren Kernen sind preiswert, und blanke Metall-Server sind überraschend billig. Dieser Blog läuft auf einem dedizierten 8-Core-Server, der weniger kostet als ein vergleichbarer Azure VM – und ich bekomme all diese Kerne für mich, keine lauten Nachbarn. Eine Hintergrundaufgabe auf einem Kern beeinflusst Web-Anfragen auf anderen Kernen nicht signifikant:
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/Awai Everywhere
Moderne .NET macht die async-Programmierung trivial. Hintergrundaufgaben können auf I/O warten, ohne Threads zu blockieren:
// 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. Bessere Prozess-Hosting
- Schwanzlutscher - Hintergrunddienste in Containern werden nicht willkürlich recycelt
- Kubernete - Richtige anmutige Herunterfahren Handhabung mit
SIGTERM - Systemd - Linux-Dienste, die zuverlässig neu starten
- Windows-Dienste - Richtiges langlaufendes Prozessmodell
4. Kanäle und moderne Primitive
.NET hat jetzt erstklassige Unterstützung für die gleichzeitige Programmierung mit 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. Ressourcengrenzen und C-Gruppen
Moderne Container-Orchester lassen Sie Ressourcennutzung begrenzen:
# 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
Dies bedeutet, dass eine auslaufende Hintergrundaufgabe nicht Ihre Web-Ebene verhungern kann.
Die Frage ist nicht mehr "Können wir Hintergrunddienste in unserer Web-App ausführen?", sondern "Sollen wir?" Wir werden diese Entscheidung im Abschnitt "Wann nicht zu verwenden Hintergrunddienste" später untersuchen.
Die eingebauten Optionen
IHostedService: Die Stiftung
Jeder Hintergrunddienst in ASP.NET Core implementiert im Kern IHostedService. Diese Schnittstelle ist sehr einfach:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Das war's. Zwei Methoden. StartAsync wird aufgerufen, wenn Ihre Bewerbung beginnt, und StopAsync wenn es abschaltet.
Registrieren Sie Ihren Service in Program.cs:
builder.Services.AddHostedService<MyBackgroundService>();
Hier ist der Lebenszyklus visualisiert:
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: Synchroner vs. Asynchroner Start
Eine kritische Entscheidung bei der Umsetzung IHostedService ist, ob Sie StartAsync Die Methode sollte sofort blockieren oder zurückkehren.
Synchroner (Blockieren) 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;
}
Asynchroner (nicht blockierender) Beginn:
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
}
}
Wann wird jeder Ansatz angewendet:
- Blockieren: Wenn der Dienst die Initialisierung abschließen muss, bevor die Anwendung Anfragen bearbeiten kann (z.B. Laden kritischer Konfiguration, Erwärmung von Caches)
- Nicht blockieren: Wenn der Dienst im Hintergrund initialisiert werden kann, während andere Dienste beginnen (z.B. Indexierung vorhandener Inhalte, Synchronisierung mit externen Diensten)
StopAsync: Der häufige Pitfall
Hier wird es interessant – und wo viele Entwickler auf Probleme stoßen. Wenn Ihre Anwendung heruntergefahren wird, ruft ASP.NET Core an StopAsync auf allen gehosteten Diensten. Sie haben ein begrenztes Fenster (Standard 5 Sekunden), um anmutig aufzuräumen. Sie können dies in verlängern Program.cs:
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
Der häufigste Fehler:
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);
}
}
}
Wenn Sie diesen Service ausführen und Ihre Anwendung beenden, sehen Sie Fehler wie:
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
Oder die Anwendung hängt einfach für die Shutdown-Timeout-Periode, bevor sie gewaltsam beendet wird.
Der richtige Ansatz:
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;
}
}
}
}
Die wichtigsten Punkte für die korrekte StopAsync-Implementierung:
- Signalannullierung - Verwenden Sie eine
CancellationTokenSourceund stornieren - Vollständige Kanäle - Wenn Sie Kanäle benutzen, rufen Sie an.
Writer.Complete() - Warten Sie auf Hintergrundaufgaben - Verwendung
Task.WhenAnymit dem Abschaltungs-Token - Manage OperationCanceledException - Dies wird erwartet und sollte gefangen werden
- Wirf keine Ausnahmen. - Ausnahmen in
StopAsynckann unvorhersehbares Verhalten verursachen
HintergrundService: Die komfortable Basisklasse
Schreiben IHostedService Implementationen können sich wiederholen. Sie benötigen immer eine Hintergrundaufgabe, eine Annullierungs-Token-Quelle und das gleiche Aufräummuster. BackgroundService behandelt diese Heizplatte für Sie:
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();
}
}
Sie einfach nur implementieren ExecuteAsync und lassen Sie die Basisklasse mit den Sanitären umgehen:
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);
}
}
Wann BackgroundService vs IHostedService verwendet werden soll
Verwendung BackgroundService wenn:
- Sie brauchen eine langlaufende Hintergrundschleife
- Sie wollen eine einfache periodische Ausführung
- Du brauchst keine Kontrolle.
StartAsync/StopAsyncZeitpunkt
Verwendung IHostedService wenn:
- Sie müssen genau kontrollieren, was passiert in
StartAsyncvs Hintergrundarbeit - Du stellst eher Event-Handler oder -Beobachter als eine kontinuierliche Schleife ein
- Sie müssen sich während des Starts mit anderen Diensten abstimmen
Fortgeschritten: Startup-Koordinierung
Manchmal benötigen Sie Dienste, um aufeinander zu warten. Zum Beispiel möchten Sie, dass Ihr semantischer Suchindexer wartet, bis Ihr Markdown-Dateiprozessor seine anfängliche Last beendet hat.
Hier ist ein Muster für die Koordination von Service-Startups:
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);
}
}
Nutzung in einem Service:
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;
}
Dieses Muster wird besonders nützlich, wenn Sie mehrere Hintergrunddienste mit Interdependenzen haben.
Verteilte Koordination mit Redis
Der Startkoordinator arbeitet innerhalb einer einzelnen Anwendungsinstanz. Aber was passiert, wenn Sie auf mehrere Instanzen skalieren? Sie wollen nicht, dass alle drei Instanzen gleichzeitig die gleiche geplante Aufgabe ausführen.
Wiedereinsetzen bietet eine einfache Lösung: Verwenden Sie Flags (Schlüssel), um zu koordinieren, wer was tut.
Einfache Leader-Wahl
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);
}
}
}
Verteilte Verriegelung für kritische Abschnitte
Für Aufgaben, die nicht gleichzeitig über Instanzen laufen dürfen:
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");
}
}
Wann die verteilte Koordinierung genutzt werden soll
- Geplante Aufgaben - Nur eine Instanz sollte den täglichen Newsletter senden
- Bearbeitung der Warteschlange bei Bestellung - Stellen Sie sicher, dass Nachrichten in der Reihenfolge verarbeitet werden
- Ressourcenintensive Operationen - Verhindern, dass mehrere Instanzen eine externe API überwältigen
- Datenbankmigrationen - Nur eine Instanz sollte Migrationen beim Starten ausführen
Für komplexere Szenarien (mehrstufige Jobs, zuverlässige Terminplanung über Neustarts) sollten Sie Hangfire beachten, der mit seinem Datenbank-Backend automatisch verteilte Sperrungen verarbeitet.
Wenn Sie Hintergrunddienste NICHT verwenden
Bevor wir in anspruchsvollere Werkzeuge wie Hangfire tauchen, lassen Sie uns darüber reden, wenn Sie sollte nicht verwenden Sie Hintergrunddienste in Ihrer Haupt-Web-Anwendung.
Signiert, dass Sie ein separates Projekt aufteilen sollten
Hintergrunddienste, die in Ihrer Webanwendung laufen, teilen Ressourcen mit Ihrer HTTP-Request-Pipeline. Dies kann zu Problemen führen:
1. Ressourcen-Streitbeilegung
Problemstellung: Ihr Hintergrundservice verbraucht signifikante CPU-, Speicher- oder Datenbankverbindungen.
// 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);
}
}
}
Wenn Web-Anfragen während der Transcodierung ankommen, sind sie langsam, weil die CPU beschäftigt ist.
Lösung: Wechseln Sie zu einem separaten Arbeiterdienst:
# 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. Verschiedene Anforderungen an die Skalierung
Problemstellung: Ihre Hintergrundarbeit braucht eine andere Skalierung als Ihre Web-Ebene.
- Web-Ebene: Skalierung für HTTP-Verkehr (vielleicht 10 Instanzen während des Tages, 2 in der Nacht)
- Hintergrundebene: Skala für Warteschlangentiefe (vielleicht benötigen 1 Instanz normalerweise, 20 bei der Verarbeitung einer Charge)
Wenn sie im selben Prozess sind, können Sie sie nicht unabhängig skalieren.
Beispielszenario:
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
Hintergrunddienste in Ihrer Web-App setzen bedeutet, dass Sie 20 Web-Instanzen ausführen müssen, nur um den Newsletter abzuwickeln und Ressourcen zu verschwenden.
3. Einsatz Unabhängigkeit
Problemstellung: Sie möchten Web-Änderungen bereitstellen, ohne Hintergrunddienste neu zu starten (oder umgekehrt).
// 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);
}
}
Jede Bereitstellung unterbricht den Import. Verschieben Sie ihn zu einem separaten Worker-Dienst, den Sie unabhängig bereitstellen.
4. Verschiedene Fehler-Domains
Problemstellung: Ein Fehler in Ihrem Hintergrunddienst stürzt die gesamte Webanwendung ab.
// 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);
}
}
Wenn Hintergrundarbeit in einem separaten Prozess ist, kann es abstürzen und neu starten, ohne Web-Anfragen zu beeinflussen.
Wie man Hintergrunddienste beeinflusst
Wenn Sie sich entscheiden zu teilen, hier ist die empfohlene Architektur:
Option 1: .NET Worker Service
Erstellen Sie ein neues Projekt mit der Vorlage Worker Service:
dotnet new worker -n YourApp.Worker
Struktur:
/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();
Getrennt einsetzen:
# 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: Getrenntes Projekt mit geteilter Warteschlange
Verwenden Sie eine Nachricht Warteschlange, um Web und Arbeiter zu entkoppeln:
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
Web-App Warteschlangen funktionieren:
// 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
}
}
Arbeiter verbraucht aus der Warteschlange:
// 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);
}
}
}
Beliebte Nachrichten Warteschlange-Optionen:
- KaninchenMQ - Die beliebtesten, funktionsreichen
- Azure Service Bus - Wenn du auf Azure bist
- AWS-SQS - Wenn Sie auf AWS sind
- Streams wiederholen - Einfacher, gut für kleinere Größen
Option 3: Mehrfachfacharbeiter
Für komplexe Systeme, aufgeteilt nach Verantwortung:
/YourApp.Web # HTTP requests only
/YourApp.EmailWorker # Sends emails
/YourApp.VideoWorker # Transcodes videos
/YourApp.ReportWorker # Generates reports
/YourApp.Scheduler # Runs scheduled jobs (Hangfire)
Jeder Arbeitnehmer kann:
- unabhängig skalieren
- selbstständig einsetzen
- Verwenden Sie verschiedene Ressourcen (E-Mail-Arbeiter braucht SMTP, Video-Arbeiter braucht GPU)
- Haben Sie unterschiedliche Überwachung und Alarmierung
Wann Sie Hintergrunddienste in Ihrer Web-App behalten
Trotz der oben genannten, einige Szenarien sind perfekt für In-Prozess-Hintergrund-Services:
Leichte periodische Aufgaben
// 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);
}
}
}
Ereignishörer
// 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;
}
}
Kanalgestützte Warteschlangen (für nicht-kritische Arbeiten)
// 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
}
Koordinierung der Unternehmensgründungen
// 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();
}
}
Entscheidungsmatrix
Charakteristisch: Halten Sie sich in der Web-App. Bewegen Sie sich zum Worker Service. |---------------|-----------------|------------------------| CPU-Nutzung pro Betrieb < 100ms > 1 Sekunde Speicher pro Betrieb < 10 MB > 100 MB Periodische Häufigkeit (Minuten/Stunden) Kritischität, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch, nicht-kritisch Dauer Sekunden Minuten bis Stunden Skalen mit dem Web-Verkehr Arbeit Warteschlange Tiefe Beispiel: Cache-Erwärmung, Config-Reload, Video-Verarbeitung, große Importe
Real-World Beispiel: Die Blog-Plattform
In der Blog-Plattform, deren Code wir in Teil 2:
In der Web-App beibehalten:
MarkdownDirectoryWatcherService- Leichter DateibeobachterUmamiBackgroundSender- Schnelle Analyse-EreignisseEmailSenderHostedService- Kleines Volumen, nicht kritischMarkdownReAddPostsService- Start-only, Konfigurations-gated
Sollte in den Arbeitsdienst umziehen, wenn die Skala zunimmt:
BrokenLinkCheckerBackgroundService- Macht viele HTTP-AnfragenSemanticIndexingBackgroundService- Ruft externe Einbettungs-API auf
Bereits im eigenen Betrieb:
Mostlylucid.SchedulerService- Hangfire Dashboard und Newsletter senden
Dies ist ein pragmatischer Ansatz: Starten Sie einfach (in-Prozess), Split, wenn Sie Beweise haben, die Sie benötigen.
Jenseits der Grundlagen: Hangfire
Während IHostedService und BackgroundService sind ausgezeichnet für Dienstleistungen, die Sie besitzen und steuern, manchmal brauchen Sie anspruchsvollere Planung. Das ist, wo Bibliotheken wie Hangfeuer Kommen Sie rein.
Hangfire bietet:
- Dauerhafte Job-Warteschlangen - Jobs überleben Anwendung neu gestartet
- Wiederkehrende Arbeitsplätze - Chronische Terminplanung
- Dashboard UI - Sehen Sie, was läuft, was fehlgeschlagen ist, versuchen Sie es noch mal.
- Verteilte Ausführung - Mehrere Server können die gleiche Auftragswarteschlange bearbeiten
- Automatische Wiederholungen - Fehlerhafte Jobs werden automatisch mit exponentiellem Backoff nachgetestet
Hier ist ein einfaches Beispiel:
// 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
Ihr Service ist nur eine normale Klasse:
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);
}
}
}
Feuergriffe:
- Sicherstellen, dass der Job zum geplanten Zeitpunkt läuft
- Versuchen, wenn es fehlschlägt
- Verlauf der Ausführung speichern
- Bereitstellung eines Dashboards, um alles zu überwachen
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
Wann wird Hangfire verwendet:
- Sie brauchen dauerhafte Job Warteschlangen, die Neustarts überleben
- Sie möchten, dass ein Dashboard die Aufträge überwacht und manuell auslöst
- Sie benötigen verteilte Auftragsbearbeitung über mehrere Server
- Sie wollen eingebaute Retry-Logik und Fehlerbehandlung
- Sie benötigen cron-style Terminierung von wiederkehrenden Aufgaben
Wann Sie mit dem IHostedService/BackgroundService bleiben:
- Sie brauchen eine gute Kontrolle über den Lebenszyklus
- Ihr Service muss auf Ereignisse in Echtzeit reagieren
- Sie wollen Abhängigkeiten minimieren
- Sie bauen eine einfache periodische Aufgabe, die keine Ausdauer braucht
Weitere Optionen
Während Hangfire beliebt ist, gibt es noch andere Bibliotheken, die es wert sind:
- Flexiblere Planung als Hangfire
- Unterstützt Cron-Ausdrücke und kalenderbasierte Terminplanung
- Kann auf mehreren Datenbanken bestehen bleiben
- Komplexere API, aber leistungsstärker
- Umsetzbare Implementierungen von Message-Bussen
- Besser für verteilte Systeme und Microservices
- Unterstützung von Sagas (Langlauf-Workflows)
- Steeper-Lernkurve
- Wenn Sie in der Cloud sind, betrachten Sie serverlos
- Bezahlen Sie pro Ausführung, anstatt einen Dienst laufen zu lassen
- Automatische Skalierung
- Einige Latenz Overhead für kalte Starts
Zusammenfassung
In Teil 1 haben wir die grundlegenden Ansätze für Hintergrunddienste in ASP.NET Core behandelt:
- IHostedService - Das Fundament, maximale Flexibilität
- Hintergrunddienst - Bequeme Basisklasse für Langlaufschleifen
- Koordinierung der Unternehmensgründungen - Die Dienste aufeinander warten zu lassen
- Verteilte Koordinierung - Verwendung von Redis für Multi-Instanz-Szenarien
- Hangfeuer - Wenn Sie dauerhafte Jobs und anspruchsvolle Planung brauchen
Die wichtigsten Lektionen:
- Vervollständigen Sie immer Kanäle und löschen Sie Token in StopAsync
- Entscheiden Sie, ob StartAsync blockiert oder sofort zurückgegeben werden soll
- Handle OperationCanceledException anmutig
- Verwenden Sie Task.WhenAny mit dem Shutdown-Token, um Shutdown-Timeouts zu respektieren
Im Teil 2, wir untersuchen Real-Welt-Implementierungen von einer Produktions-Blog-Plattform:
- Dateisystembeobachter, die Markdown-Dateien mit einer Datenbank synchronisieren
- E-Mail-Sender mit Retry-Richtlinien und Leistungsschaltern
- Analysieren Ereignis Warteschlangen, die Batch-Anfragen
- Semantische Suchindexer, die Inhalte asynchron verarbeiten
- Gebrochene Link-Checker, die regelmäßig externe URLs validieren
Diese Beispiele zeigen die Muster aus Teil 1 in Aktion, einschließlich des Startkoordinationsmusters und der ordnungsgemäßen Abschaltung.