Background Services in ASP.NET Core - Part 1: The Approaches (ελληνικά (Greek))

Background Services in ASP.NET Core - Part 1: The Approaches

Thursday, 27 November 2025

//

24 minute read

Κάθε σύγχρονη εφαρμογή web έχει εργασία που δεν θα πρέπει να μπλοκάρει ένα αίτημα HTTP αποστολή ηλεκτρονικού ταχυδρομείου, επεξεργασία αρχείων, συγχρονισμός με εξωτερικές υπηρεσίες, εκτέλεση προγραμματισμένη συντήρηση. ASP.NET Core παρέχει πολλαπλές προσεγγίσεις για τη διαχείριση αυτής της εργασίας φόντου, από απλή IHostedService εφαρμογές σε εξελιγμένα πλαίσια όπως Hangfire. Σε αυτό το πρώτο μέρος, θα εξερευνήσουμε τα θεμελιώδη πρότυπα και πότε να χρησιμοποιήσουμε το καθένα.

Εισαγωγή

Ομολογία; Μου αρέσει Background Services a LOT, αυτό το site (ένα BLOG site!) έχει πάνω από μισή ντουζίνα από αυτούς κάνει carious εργασίες φόντου, αλλά όπως όλα όσα έχουν κάποιες ΕΥΘΥΝΕΣ και πρακτικές που θα κάνουν τη χρήση σας από αυτά πολύ πιο ευχάριστη. Ενώ οι ελεγκτές σας χειρίζονται αιτήματα HTTP στο προσκήνιο, οι υπηρεσίες υποβάθρου διαδικασίες αθόρυβα ουρλιαχτά μηνύματα ηλεκτρονικού ταχυδρομείου, το περιεχόμενο ευρετηρίου για αναζήτηση, ελέγξτε τα εξωτερικά APIs, καθαρίστε προσωρινά αρχεία, και χειριστείτε αμέτρητες άλλες εργασίες που διαφορετικά θα μπλοκάρουν τον αγωγό αίτησης σας.

Σε αυτή τη σειρά δύο μερών, θα εξερευνήσουμε τις διαφορετικές προσεγγίσεις για την εφαρμογή υπηρεσιών υποβάθρου στο ASP.NET Core, από την ενσωματωμένη IHostedService και BackgroundService Στο Μέρος 1, θα εξετάσουμε τις θεμελιώδεις προσεγγίσεις και τα χαρακτηριστικά τους. Μέρος 2, θα βουτήξουμε σε πραγματικό κόσμο εφαρμογές από μια βάση κώδικα παραγωγής.

Σημαντικό: Θα δώσουμε ιδιαίτερη προσοχή στη διαχείριση του κύκλου ζωής. StopAsync μέθοδος, η οποία είναι όπου πολλοί προγραμματιστές αντιμετωπίζουν αινιγματικές εξαιρέσεις όταν οι εφαρμογές τους κλείνουν.

Γιατί Υπηρεσίες Υποβάθρου;

Πριν καταδυθείτε στο "πώς," ας εξετάσουμε εν συντομία το "γιατί." Οι υπηρεσίες υποβάθρου σας επιτρέπουν:

  1. Εκφορτώστε τις αργές λειτουργίες - Μην κάνετε τους χρήστες να περιμένουν ενώ στέλνετε μηνύματα ηλεκτρονικού ταχυδρομείου ή να παράγουν PDF
  2. Πρόγραμμα επαναλαμβανόμενων εργασιών - Καθάρισε παλιούς δίσκους κάθε βράδυ στις 2 π.μ.
  3. Ουρά διαδικασίας - Χειριστείτε μηνύματα από κανάλια ή μεσίτες μηνυμάτων
  4. Παρακολουθήστε την εξωτερική κατάσταση - Συστήματα αρχείων Poll APIs ή ρολόι για αλλαγές
  5. Συντεταγμένες σύνθετες ροές εργασίας - Διαχείριση διαδικασιών πολλαπλών βημάτων που καλύπτουν λεπτά ή ώρες

Το ASP.NET Core παρέχει διάφορες προσεγγίσεις για την υλοποίηση αυτών των υπηρεσιών, η καθεμία με διαφορετικές συναλλαγές.

The Historical Context: Why Background Services are Now Feasible

Κατά τις "παλιές ημέρες" (προ-2010), η εκτέλεση εργασιών υποβάθρου στην εφαρμογή σας στο διαδίκτυο θεωρήθηκε γενικά κακή ιδέα. Η συμβατική σοφία ήταν: "Οι διακομιστές Web χειρίζονται αιτήματα ιστού. Η εργασία φόντου ανήκει σε έναν ξεχωριστό διακομιστή."

Αυτό δεν ήταν μόνο η σοφία του φορτίου που βασίζεται σε πραγματικούς τεχνικούς περιορισμούς:

Η Εποχή της Ενιαίας Τιμής

Early web servers (και ειλικρινά τώρα, 'cheap' Azure services) συνήθως τρέχει σε ΚΜΕ ενός πυρήνα ή διπλού πυρήνα. Εάν εκτελούσατε μια εργασία φόντου έντασης CPU, αγωνίστηκε άμεσα με web αιτήματα για τον ίδιο πυρήνα:

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

Αποτέλεσμα: Η ιστοσελίδα σας έγινε νωθρή τη στιγμή που ξεκίνησε η δουλειά του υποβάθρου.

Θραύσμα λιμνοθάλασσας

Χρησιμοποιείται κλασικό ASP.NET threat-per-request. Η δεξαμενή κλωστών ήταν σχετικά μικρή (25-100 κλωστές συνήθως), και οι εργασίες φόντου θα κλέψουν νήματα που θα πρέπει να χειρίζονται web αιτήματα:

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

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

Ανακύκλωση πισίνας εφαρμογής IIS

Το IIS θα ανακυκλώσει επιθετικά τις πισίνες εφαρμογής (επανεκκινήστε την εφαρμογή σας) με βάση τα όρια μνήμης, τις μετρήσεις αιτήσεων, ή τα χρονοδιαγράμματα.

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

Περιορισμένη υποστήριξη Async/Await

Πριν από .NET 4.5 (2012), ο προγραμματισμός async ήταν (σχετικά) επώδυνος.

// 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. Multi-Core είναι προσιτή

Τα οικονομικά έχουν αναποδογυρίσει. Cloud VMs με πολλαπλούς πυρήνες είναι λογικές τιμές, και γυμνά metal servers είναι εκπληκτικά φθηνά. Αυτό το blog τρέχει σε έναν ειδικό 8-κορεάτες διακομιστή που κοστίζει λιγότερο από ένα συγκρίσιμο Azure VM _BAR_και παίρνω όλους αυτούς τους πυρήνες για τον εαυτό μου, δεν θορυβώδη γείτονες.

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/Await Παντού

Το σύγχρονο.NET κάνει τον προγραμματισμό async ασήμαντο. Οι εργασίες φόντου μπορούν να περιμένουν σε I/O χωρίς να μπλοκάρουν τα νήματα:

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

3. Καλύτερη φιλοξενία διαδικασιών

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

Αυτό σημαίνει ότι ένα έργο που έφυγε από το παρελθόν δεν μπορεί να λιμοκτονήσει το επίπεδο ιστού σας.

Το ερώτημα δεν είναι πλέον "Μπορούμε να εκτελούμε υπηρεσίες υποβάθρου στην εφαρμογή μας στο διαδίκτυο;" αλλά "Θα έπρεπε;"Θα εξερευνήσουμε αυτή την απόφαση στην ενότητα "Όταν ΔΕΝ χρησιμοποιήσετε τις Υπηρεσίες Υποβάθρου" αργότερα.

Οι Ενσωματωμένες Επιλογές

IHostedService: Το Ίδρυμα

Στον πυρήνα της, κάθε υπηρεσία υποβάθρου στο ASP.NET Core εφαρμόζει 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

StartAsync: Συγχρονισμός εναντίον Ασύγχρονης Έναρξης

Κρίσιμη απόφαση κατά την εφαρμογή IHostedService είναι το αν σας StartAsync η μέθοδος πρέπει να μπλοκάρει ή να επιστρέφει αμέσως.

Έναρξη συγχρονισμού:

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

        // Only now will the application continue starting
    }

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

Ασύγχρονη (μη-μπλοκάρισμα) έναρξη:

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

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

        return Task.CompletedTask;
    }

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

Πότε να χρησιμοποιήσετε κάθε προσέγγιση:

  • Αποκλεισμός: Όταν η υπηρεσία πρέπει να ολοκληρώσει την αρχικοποίηση πριν από την εφαρμογή μπορεί να χειριστεί τα αιτήματα (π.χ., τη φόρτωση κρίσιμης διαμόρφωσης, τις κρυψώνες θέρμανσης)
  • Μη μπλοκάρισμα: Όταν η υπηρεσία μπορεί να αρχικοποιηθεί στο παρασκήνιο ενώ αρχίζουν άλλες υπηρεσίες (π.χ. ευρετηρίαση υπάρχοντος περιεχομένου, συγχρονισμός με εξωτερικές υπηρεσίες)

StopAsync: The Common Pitfall

Εδώ είναι όπου τα πράγματα γίνονται ενδιαφέροντα και όπου πολλοί προγραμματιστές αντιμετωπίζουν προβλήματα. Όταν η εφαρμογή σας κλείνει, ASP.NET Core καλεί StopAsync σε όλες τις υπηρεσίες που φιλοξενούνται. Έχετε ένα περιορισμένο παράθυρο (προεπιλογή 5 δευτερόλεπτα) για να καθαρίσει με χάρη. Program.cs:

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

Το πιο κοινό λάθος:

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

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

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

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

Όταν εκτελέσετε αυτή την υπηρεσία και σταματήσετε την εφαρμογή σας, θα δείτε λάθη όπως:

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

Ή η εφαρμογή απλά θα κρεμαστεί για την περίοδο λήξης του χρόνου πριν από τη βία.

Η σωστή προσέγγιση:

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

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

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

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

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

Βασικά σημεία για τη σωστή εφαρμογή StopAsync:

  1. Ακύρωση σήματος - Χρησιμοποίησε ένα CancellationTokenSource και ακυρώστε το.
  2. Πλήρη κανάλια - Αν χρησιμοποιείτε κανάλια, τηλεφωνήστε. Writer.Complete()
  3. Αναμονή για εργασίες υποβάθρου - Χρήση Task.WhenAny με την ένδειξη ακύρωσης κλεισίματος
  4. Χειρισμός Λειτουργία Ακυρώθηκε Εκτός από - Αυτό αναμένεται και πρέπει να συλληφθεί.
  5. Μην ρίχνεις εξαιρέσεις. - Απαλλαγές από την StopAsync μπορεί να προκαλέσει απρόβλεπτη συμπεριφορά

BackgroundService: Η βολική βασική κατηγορία

Γράφοντας IHostedService εφαρμογές μπορεί να επαναληφθεί. Χρειάζεστε πάντα μια εργασία φόντου, μια πηγή σημάδι ακύρωσης, και το ίδιο μοτίβο καθαρισμού. BackgroundService χειρίζεται αυτό το λεβητοστάσιο για σας:

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

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

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

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

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

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

Απλά εφαρμόζεις. ExecuteAsync και αφήστε την βασική τάξη να χειριστεί τα υδραυλικά:

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

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

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

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

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

        _logger.LogInformation("Service stopping");
    }

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

Πότε να χρησιμοποιήσετε το BackgroundService vs IHostedService

Χρήση BackgroundService όταν:

  • Χρειάζεσαι έναν μακρύ κύκλο υποβάθρου.
  • Θέλετε απλή περιοδική εκτέλεση
  • Δεν χρειάζεσαι καλό έλεγχο. StartAsync/StopAsync συγχρονισμός

Χρήση IHostedService όταν:

  • Πρέπει να ελέγξεις ακριβώς τι συμβαίνει στο StartAsync vs background work
  • Ετοιμάζετε χειριστές γεγονότων ή παρατηρητές αντί για συνεχή βρόχο
  • Θα πρέπει να συντονίσετε με άλλες υπηρεσίες κατά τη διάρκεια της εκκίνησης

Προχωρημένος: Συντονισμός εκκίνησης

Μερικές φορές χρειάζεστε υπηρεσίες για να περιμένετε ο ένας τον άλλον. Για παράδειγμα, ίσως θέλετε τον σημασιολογικό δείκτη αναζήτησης σας να περιμένει μέχρι ο επεξεργαστής αρχείων σας να τελειώσει το αρχικό του φορτίο.

Εδώ είναι ένα μοτίβο για το συντονισμό εκκίνησης υπηρεσιών:

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

Αλλά τι συμβαίνει όταν κλιμακώνετε σε πολλαπλές περιπτώσεις; Δεν θέλετε τρεις περιπτώσεις όλες τρέχουν το ίδιο προγραμματισμένο έργο ταυτόχρονα.

ΡεντίςCity name (optional, probably does not need a translation) παρέχει μια απλή λύση: χρησιμοποιήστε σημαίες (κλειδιά) για να συντονίσετε ποιος κάνει τι.

Απλή Εκλογή Ηγέτη

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

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

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

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

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

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

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

Διανεμημένη κλειδαριά για Κρίσιμα Τμήματα

Για καθήκοντα που δεν πρέπει να εκτελούνται ταυτόχρονα σε περιπτώσεις:

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

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

Πότε να χρησιμοποιήσετε τον κατανεμημένο συντονισμό

  • Προγραμματισμένα καθήκοντα - Μόνο μία περίπτωση θα πρέπει να στείλει το ημερήσιο ενημερωτικό δελτίο
  • Επεξεργασία αναμονής με παραγγελία - Βεβαιωθείτε ότι τα μηνύματα υποβάλλονται σε επεξεργασία με τη σειρά τους
  • Πτητικές εργασίες έντασης πόρων - Αποτρέψτε πολλαπλές περιπτώσεις από συντριπτική ένα εξωτερικό API
  • Μετανάστες βάσης δεδομένων - Μόνο μία περίπτωση θα πρέπει να τρέξει μεταναστεύσεις κατά την εκκίνηση

Για πιο περίπλοκα σενάρια (πολλαπλά βήματα εργασίας, αξιόπιστος προγραμματισμός σε όλες τις επανεκκινήσεις), σκεφτείτε Hangfire που χειρίζεται κατανεμημένο κλείδωμα αυτόματα με το backend της βάσης δεδομένων.

Όταν ΔΕΝ χρησιμοποιείτε υπηρεσίες υποβάθρου

Πριν βουτήξουμε σε πιο εξελιγμένα εργαλεία όπως το Hangfire, ας μιλήσουμε για το πότε εσύ Δεν θα έπρεπε. χρήση υπηρεσιών υποβάθρου στην κύρια εφαρμογή σας web.

Σημάδια Πρέπει να Χωριστείτε σε ένα Ξεχωριστό Έργο

Οι υπηρεσίες φόντου που λειτουργούν στο web εφαρμογή σας μοιράζονται τους πόρους με τον αγωγό αίτησης HTTP σας. Αυτό μπορεί να προκαλέσει προβλήματα:

1. Περιεχόμενο πόρων

Πρόβλημα: Η υπηρεσία υποβάθρου σας καταναλώνει σημαντικές συνδέσεις CPU, μνήμης ή βάσης δεδομένων.

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

Όταν τα αιτήματα web φτάνουν κατά τη διάρκεια της Transcoding, είναι αργή επειδή η CPU είναι απασχολημένος.

Διάλυμα: Μετακίνηση σε χωριστή υπηρεσία εργαζομένων:

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

2. Διαφορετικές απαιτήσεις βαθμολόγησης

Πρόβλημα: Η δουλειά σας χρειάζεται διαφορετικό σκαλί από το επίπεδο ιστού σας.

  • Διαδικτυακή βαθμίδα: Κλίμακα κυκλοφορίας HTTP (μπορεί να χρειαστεί 10 περιπτώσεις κατά τη διάρκεια της ημέρας, 2 τη νύχτα)
  • Βαθμίδα υποβάθρου: Κλίμακα για βάθος αναμονής (μπορεί να χρειάζεται 1 περίπτωση κανονικά, 20 κατά την επεξεργασία μιας παρτίδας)

Αν είναι στην ίδια διαδικασία, δεν μπορείς να τα κλιμακώσεις ανεξάρτητα.

Παράδειγμα σεναρίου:

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

Βάζοντας υπηρεσίες υποβάθρου στην εφαρμογή σας web σημαίνει ότι θα πρέπει να εκτελέσετε 20 web περιπτώσεις μόνο για να χειριστεί το ενημερωτικό δελτίο, σπαταλάτε πόρους.

3. Ανεξαρτησία στην Ανάπτυξη

Πρόβλημα: Θέλετε να αναπτύξετε αλλαγές ιστού χωρίς να επανεκκινήσετε τις υπηρεσίες υποβάθρου (ή αντίστροφα).

// 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. Διαφορετικοί τομείς αποτυχίας

Πρόβλημα: Ένα ζωύφιο στην υπηρεσία παρασκήνιο σας συντρίβει ολόκληρη την εφαρμογή 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);
    }
}

Εάν η εργασία υποβάθρου βρίσκεται σε ξεχωριστή διαδικασία, μπορεί να συντριβεί και να επανεκκινήσει χωρίς να επηρεάζει τα αιτήματα ιστού.

Πώς να Factor Υπηρεσίες φόντου

Όταν αποφασίσετε να χωρίσετε, εδώ είναι η συνιστώμενη αρχιτεκτονική:

Επιλογή 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

Επιλογή 2: Ξεχωριστό Έργο με Shared Queeue

Χρησιμοποιήστε μια ουρά μηνυμάτων για να αποσυνδέσετε το διαδίκτυο και τους εργαζομένους:

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 ουρές εργασίας:

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

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

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

        return Accepted(); // Return immediately
    }
}

Ο εργαζόμενος καταναλώνει από την ουρά:

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

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

Δημοφιλείς επιλογές αναμονής μηνυμάτων:

Επιλογή 3: Πολλαπλοί ειδικευμένοι εργαζόμενοι

Για πολύπλοκα συστήματα, χωρισμένα ανά ευθύνη:

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

Κάθε εργαζόμενος μπορεί:

  • Κλίμακα ανεξάρτητα
  • Αναπτύξτε ανεξάρτητα
  • Χρήση διαφορετικών πόρων (email worker χρειάζεται SMTP, video worker χρειάζεται GPU)
  • Να έχετε διαφορετική παρακολούθηση και προειδοποίηση

Πότε να κρατήσει τις υπηρεσίες φόντου στο Web App σας

Παρά τα παραπάνω, ορισμένα σενάρια είναι απολύτως καλά για τις υπηρεσίες in-process background:

Ελαφρά Περιοδικά Καθήκοντα

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

Ακροατές γεγονότων

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

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

Κρουαζιέρα καναλιού (για μη-κρίσιμη εργασία)

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

Συντονισμός εκκίνησης

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

Απόφαση Matrix

Διατηρήστε σε Web App Μετακινηθείτε στην υπηρεσία εργατών |---------------|-----------------|------------------------| Χρήση CPU ανά λειτουργία < 100ms > 1 δευτερόλεπτο Μνήμη ανά λειτουργία < 10 MB > 100 MB ~ Συχνότητα ~ Περιοδική (λεπτά/ώρες) ~ Συνεχής ή υψηλή συχνότητα ~ Κρίσιμη Κρίσιμη Κρίση Μη Κρίσιμη Κρίσιμη Κρίση Κρίσιμη Κρίση Κρίσιμη Κρίση Κρίσιμη Κρίση Κρίσιμη Κρίση Κρίσιμη Κρίσιμη Κρίση Διάρκεια Δευτερόλεπτα λεπτά με ώρες Σκάλες με Web κυκλοφορία ~ Βάθος ουράς εργασίας ~ Παράδειγμά του: Ζεστασιά Cache, config reload Βίντεο επεξεργασίας, μεγάλες εισαγωγές

Real-World Παράδειγμα: Η πλατφόρμα blog

Στην πλατφόρμα blog της οποίας τον κώδικα εξετάζουμε στο Μέρος 2:

Κρατήστηκε σε web app:

  • MarkdownDirectoryWatcherService - Ελαφρύς παρατηρητής αρχείων
  • UmamiBackgroundSender - Γρήγορες αναλυτικές εκδηλώσεις
  • EmailSenderHostedService - Μικρός όγκος, μη κρίσιμος
  • MarkdownReAddPostsService - Εκκίνηση μόνο, διαμόρφωση-gated

Θα πρέπει να μετακινηθείτε στην υπηρεσία των εργαζομένων εάν αυξηθεί η κλίμακα:

  • BrokenLinkCheckerBackgroundService - Κάνει πολλά αιτήματα HTTP
  • SemanticIndexingBackgroundService - Καλεί την εξωτερική ενσωμάτωση API

Ήδη σε ξεχωριστή υπηρεσία:

  • Mostlylucid.SchedulerService - Hangfire ταμπλό και newsletter αποστολή

Αυτή είναι μια ρεαλιστική προσέγγιση: ξεκινήστε απλά (κατά τη διαδικασία), χωριστείτε όταν έχετε αποδείξεις που πρέπει να.

Πέρα από τα Βασικά: Hangfire

Ενώ IHostedService και BackgroundService είναι εξαιρετική για τις υπηρεσίες που έχετε στην κατοχή σας και τον έλεγχο, μερικές φορές χρειάζεστε πιο εξεζητημένο προγραμματισμό. Φωτιά στην κρεμάλα Περάστε.

Hangfire παρέχει:

  • Διαρκείς ουρές θέσεων εργασίας - Δουλειές επιβιώνουν εφαρμογή επανεκκίνηση
  • Επαναλαμβανόμενες θέσεις εργασίας - Cron-style προγραμματισμός
  • Dashboard UI - Δες τι τρέχει, τι απέτυχε, ξαναπροσπάθησε.
  • Κατανεμημένη εκτέλεση - Πολλαπλοί διακομιστές μπορούν να επεξεργαστούν την ίδια ουρά εργασίας
  • Αυτόματες επαναλήψεις - Οι αποτυχημένες δουλειές επανέρχονται αυτόματα με εκθετική υποχώρηση

Να ένα απλό παράδειγμα:

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

builder.Services.AddHangfireServer();

var app = builder.Build();

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

Η υπηρεσία σας είναι απλά μια κανονική τάξη:

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

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

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

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

Λαβές πυρκαϊάς:

  • Εξασφάλιση της λειτουργίας της εργασίας κατά την προγραμματισμένη ώρα
  • Ξαναπροσπαθώ αν αποτύχει.
  • Ιστορικό εκτέλεσης αποθήκευσης
  • Παρέχοντας ένα ταμπλό για να παρακολουθεί τα πάντα
graph TD
    A[Hangfire Server] --> B{Check Schedule}
    B -->|Job Due| C[Dequeue Job]
    C --> D[Execute Job Method]
    D -->|Success| E[Mark Complete]
    D -->|Failure| F[Retry with Backoff]
    F --> G{Max Retries?}
    G -->|No| C
    G -->|Yes| H[Mark Failed]
    E --> I[Update Dashboard]
    H --> I
    I --> B

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

Πότε να χρησιμοποιήσετε το Hangfire:

  • Χρειάζεστε επίμονες ουρές εργασίας που επιβιώνουν επανεκκινήσεις
  • Θέλεις ένα ταμπλό για να παρακολουθείς και να ενεργοποιείς χειροκίνητα τις δουλειές
  • Χρειάζεστε κατανεμημένη επεξεργασία εργασίας σε πολλούς διακομιστές
  • Θέλετε ενσωματωμένη λογική και χειρισμό αποτυχίας
  • Χρειάζεστε cron-style προγραμματισμό επαναλαμβανόμενων εργασιών

Πότε να κολλήσει με IHostedService / BackgroundService:

  • Χρειάζεστε καλό έλεγχο του κύκλου ζωής των υπηρεσιών
  • Η υπηρεσία σας πρέπει να αντιδράσει σε γεγονότα σε πραγματικό χρόνο
  • Θέλετε να ελαχιστοποιήσετε τις εξαρτήσεις
  • Φτιάχνεις μια απλή περιοδική εργασία που δεν χρειάζεται επιμονή.

Άλλες επιλογές

Ενώ Hangfire είναι δημοφιλής, υπάρχουν και άλλες βιβλιοθήκες που αξίζει να εξετάσει:

Quartz.NET:

  • Πιο ευέλικτος προγραμματισμός από το Hangfire
  • Υποστηρίζει εκφράσεις cron και χρονοδιάγραμμα με βάση το πρόγραμμα
  • Μπορεί να επιμείνει σε πολλαπλές βάσεις δεδομένων
  • Πιο πολύπλοκο API αλλά πιο ισχυρό

MassTransit/NServiceBusCity name (optional, probably does not need a translation):

  • Πλήρεις εφαρμογές λεωφορείων μηνυμάτων
  • Καλύτερα για κατανεμημένα συστήματα και μικροϋπηρεσίες
  • Σάγκα υποστήριξης (μακροκίνητες ροές εργασίας)
  • Καμπύλη εκμάθησης Steeper

Λειτουργίες Azure/AWS LambdaCity name (optional, probably does not need a translation):

  • Αν είσαι στο σύννεφο, σκέψου το χωρίς σέρβερ.
  • Πληρωμή ανά εκτέλεση αντί να λειτουργεί υπηρεσία
  • Αυτόματη κλιμάκωση
  • Κάποια αργοπορία για το κρύο ξεκινάει

Περίληψη

Στο Μέρος 1, έχουμε καλύψει τις θεμελιώδεις προσεγγίσεις για τις υπηρεσίες υποβάθρου στο ASP.NET Core:

  1. IHostedService - Το θεμέλιο, μέγιστη ευελιξία
  2. BackgroundService - Βολικό βασικό μάθημα για μακρινούς βρόχους
  3. Συντονισμός εκκίνησης - Κάνοντας τις υπηρεσίες να περιμένουν ο ένας τον άλλο
  4. Διανεμημένος συντονισμός - Χρήση Redis για σενάρια πολλαπλών στοιχείων
  5. Φωτιά στην κρεμάλα - Όταν χρειάζεστε επίμονες δουλειές και εξεζητημένο προγραμματισμό

Τα πιο σημαντικά μαθήματα:

  • Πάντα να ολοκληρώνετε τα κανάλια και να ακυρώνετε τις μάρκες στο StopAsync
  • Αποφασίστε αν το StartAsync πρέπει να μπλοκάρει ή να επιστρέψει αμέσως
  • Χειριστείτε τη λειτουργία Ακυρώθηκε Εκτός χάρης
  • Χρησιμοποιήστε το έργο.Όταν με το σημείο κλεισίματος για να σεβαστείτε τα χρονικά κενά

Το Μέρος 2, θα εξετάσουμε πραγματικές εφαρμογές από μια πλατφόρμα blog παραγωγής:

  • Παρατηρητές συστημάτων αρχείων που συγχρονίζουν αρχεία markdown σε μια βάση δεδομένων
  • Αποστολές email με retry πολιτικές και διακόπτες κυκλώματος
  • Analytics ουρές γεγονότων που ζητούν παρτίδα
  • Σημασιολογικοί δείκτες αναζήτησης που επεξεργάζονται το περιεχόμενο ασύγχρονα
  • Σπασμένα χειριστήρια σύνδεσης που επικυρώνουν περιοδικά τα εξωτερικά URL

Αυτά τα παραδείγματα δείχνουν τα πρότυπα από το Μέρος 1 σε δράση, συμπεριλαμβανομένου του τρόπου συντονισμού εκκίνησης και του κατάλληλου χειρισμού κλεισίματος.

Περαιτέρω Ανάγνωση

Finding related posts...
logo

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