الخدمات الأساسية - الجزء 1: النُهُج (العربية (Arabic))

الخدمات الأساسية - الجزء 1: النُهُج

Thursday, 27 November 2025

//

24 minute read

كل تطبيقات الويب الحديثة لديها عمل لا يجب أن يمنع طلب HTTP - إرسال البريد الإلكتروني، معالجة الملفات، مزامنة مع الخدمات الخارجية، تشغيل الصيانة المقررة. ASP.NET Cors توفر نُهُج متعددة للتعامل مع هذا العمل الأساسي، من بسيط. IHostedService في هذا الجزء الأول، سنستكشف الأنماط الأساسية ومتى نستخدم كل واحدة منها.

أولاً

إن هذا الموقع (موقع BLOG!) يحتوي على أكثر من نصف دزينة منهم يقومون بمهام خلفيات غريبة، ولكن مثل كل شيء لديهم بعض ODDITIES والممارسات التي من شأنها أن تجعل استخدامكم لها أكثر متعة. خدمات المعلومات الأساسية هم الأبطال المجهولون في التطبيقات الحديثة على الإنترنت. في حين يتعامل مراقبوكم مع طلبات HTTP في المقدمة، تقوم خدمات الخلفية بهدوء بتجهيز البريد الإلكتروني الطابور، فهرس المحتوى للبحث، تحقق من الأرقام القياسية المرجعية الخارجية، تنظيف الملفات المؤقتة، والتعامل مع عدد لا يحصى من المهام الأخرى التي من شأنها أن تعرقل خط طلبك لولا ذلك.

في هذه السلسلة المكونة من جزين، سنستكشف النُهُج المختلفة لتنفيذ خدمات المعلومات الأساسية في ASP.net IHostedService وقد عقد مؤتمراً بشأن BackgroundService في الجزء الأول، سوف نفحص النُهج الأساسية وخصائصها. الجزء الثانيسوف نغوص في تنفيذات العالم الحقيقي من قاعدة شفرات الإنتاج

المهم المهم: سوف نولي اهتماماً خاصاً لإدارة دورة الحياة خصوصاً StopAsync حيث يواجه العديد من المطورين استثناءات مشفرة عند إغلاق تطبيقاتهم.

لماذا الخدمات الأساسية؟

قبل الغوص في "كيف"، دعونا نفكر بإيجاز في "لماذا". الخدمات الأساسية تسمح لك:

  1. عمليات البطء في التحميل - لا تجعل المستخدمين ينتظرون بينما ترسل رسائل البريد الإلكتروني أو تولد PDFs
  2. مهام - تنظيف السجلات القديمة كل ليلة في الساعة الثانية صباحاً
  3. **** - مناولة الرسائل الواردة من القنوات أو من سماسرة الرسائل
  4. مراقب خارجي الدولة - شاشات التصفح أو نظم ملفات المراقبة للتغييرات
  5. ** تسيير أعمال معقدة** - إدارة العمليات المتعددة الخطوات التي تستغرق دقائق أو ساعات

وتوفر الشبكة الأساسية عدة نُهُج لتنفيذ هذه الخدمات، لكل منها مقايضات مختلفة.

السياق التاريخي: لماذا أصبحت خدمات المعلومات الأساسية ممكنة الآن

في "أيام قديمة" (قبل عام 2010)، كان العمل الأساسي في تطبيقك على الشبكة يعتبر عموما فكرة سيئة. الحكمة التقليدية كانت: "الحواسيب على الشبكة تتعامل مع طلبات الويب. العمل الأساسي ينتمي إلى خادم منفصل".

لم يكن هذا مجرد حكمة في مجال نقل البضائع، بل كان مبنياً على حدود تقنية حقيقية:

أُعْرَكَرُ القُوّي الوحيد

خوادم الويب المبكرة (وصدقاً الآن، خدمات "cheap" Azure") يجري تشغيلها عادة وحدات مزج واحدة أو مزجات أساسية أو مُزْدِدة مُخَززات أساسية أو مُزْرِدةإذا كنت تدير مهمة خلفية كثيفة للمعالج المعالج، فإنه يتنافس مباشرة مع طلبات الويب لنفس النواة:

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

الموقع الإلكتروني الخاص بك أصبح بطيئاً في اللحظة التي بدأ فيها العمل الأساسي.

المجلدات

كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل كل ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط ط● كان مجمع الخيوط صغيرا نسبيا (يتراوح نمطيا بين 25 و100 خيط)، ومن شأن المهام الأساسية أن تسرق الخيوط التي ينبغي أن تعالج طلبات الشبكة:

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

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

مجموعة تطبيقات IIS

سوف يقوم المعهد بإعادة تدوير مجمعات التطبيقات (اعادة تشغيل تطبيقك) استناداً إلى حدود الذاكرة، أو عدد الطلبات، أو الجداول الزمنية. وسوف يقتل العمل الأساسي في منتصف العملية:

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

Limed Assync/A انتظر دعم

قبل 4.5 (2012)، كانت البرمجة التركيبية مؤلمة (نسبياً)، وكثيراً ما كانت المهام الأساسية تسد الخيوط دون داع:

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

التغير : العصر الحديث

مشهد اليوم مختلف بشكل كبير:

1 - القدرة على تحمل التكلفة

لقد انقلبت الإقتصاديات. حيث يتم تقدير السعر بشكل معقول ل VMs التي تحتوي على نواة متعددة، و الخوادم المعدنية العارية رخيصة بشكل مفاجئ. تعمل هذه المدونة على خادم مخصص من 8 أساسيات والذي يكلف أقل من Azore VM-و أنا أحصل على كل تلك النواة لنفسي، لا جيران مزعجين. مهمة خلفية على أحد النواة لا تؤثر بشكل كبير على طلبات الويب على النواة الأخرى:

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

2 - async/aنتظر في كل مكان

يمكن للمهام الأساسية أن تنتظر التنفيذ/الإعمال دون أن تعوق الخيوط التالية:

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

تقديم عملية أفضل

  • مُصد مُصد مُصد مُكر - الخدمات الأساسية في الحاويات لا يتم إعادة تدويرها بصورة تعسفية
  • الكوبرنيات - التساهل المناسب في التعامل مع SIGTERM
  • نُقل مُنْد - خدمات لينكس التي تعيد تشغيلها بصورة موثوقة
  • & ويندS خدمات - نموذج سليم لعملية طويلة الأمد

4 - القنوات والمبتكرات الحديثة

وقد أصبح لدى الشبكة الآن دعم من الدرجة الأولى للبرمجة المتزامنة مع 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هذه الواجهة هي في غاية البساطة:

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

بدء AsSync: تزامن

مقرر حاسم الأهمية عند التنفيذ 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 لديك نافذة محدودة (الافتراضية 5 ثوان) لتنظيف كل الخدمات المستضافة. لديك نافذة محدودة (الافتراضية 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;
            }
        }
    }
}

النقاط الرئيسية في التنفيذ الصحيح للتقويم:

  1. مُنْسِ الإشارة - استخدام CancellationTokenSource ويُلغيها
  2. & -إذا كنت تستخدم القنوات، فاتصل Writer.Complete()
  3. انتظار لـ معلومات خلفية مهام - الاستخدام - الاستخدام Task.WhenAny مَع إلغاء الإغلاق
  4. عملية مَنْدِ المُنْفِج - وهذا أمر متوقع وينبغي القبض عليه
  5. لا يَلْمُ استثناءاتَ - الاستثناءات في StopAsync يمكن أن يسبب سلوكاً لا يمكن التنبؤ به

الخدمات الأساسية: درجة القاعدة التيسيرية

الكتابة IHostedService تحتاج دائماً إلى مهمة خلفية، ومصدر إلغاء رمزي، ونفس نمط التنظيف. BackgroundService يُعالجُ هذا المرجلِ لَك:

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

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

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

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

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

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

أنت فقط تنفذ ExecuteAsync ودعي فئة القاعدة تتعامل مع السباكة:

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

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

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

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

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

        _logger.LogInformation("Service stopping");
    }

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

إلى استخدام إلى استخدام معلومات الخلفية SSerS الخدمات vs I HoustServ

** BackgroundService متى:**

  • تحتاج إلى حلقة خلفية طويلة الأمد
  • تريد تنفيذاً دورياً بسيطاً
  • لا تحتاج إلى التحكم في StartAsync/StopAsync 1 ف-4، 1 ف-3، 1 خ م، 1 م و، 1 م و، 1 م و، 1 م أ م

** IHostedService متى:**

  • تحتاج إلى التحكم بالضبط ما يحدث في StartAsync العمل الأساسي
  • أنت تَضِعُ الأحداثَ المعالجين أَو المشاهدين بدلاً مِنْ a حلقة متصلة
  • تحتاج إلى التنسيق مع الخدمات الأخرى أثناء بدء التشغيل.

المتقدم: تنسيق بدء التشغيل

في بعض الأحيان تحتاج إلى خدمات للانتظار لبعضها البعض. على سبيل المثال، قد ترغب في أن ينتظر فهرس البحث الدلالي الخاص بك حتى ينتهي معالج ملف العلامة التنازلية من تحميله الأولي.

هذا هو نمط تنسيق بدء الخدمة:

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

يصبح هذا النمط مفيداً بشكل خاص عندما يكون لديك خدمات خلفية متعددة مع ترابطات.

التنسيق الموزع مع Resedis

يعمل منسق البدأ ضمن a واحد تطبيق مثال لكن ماذا يحدث عند التوسّع إلى متعدد؟ أنت لا تريد ثلاثة حالات الكل تشغيل نفس المهمة المقررة في وقت واحد.

توفر حلاً بسيطاً: استخدام الأعلام (المفاتيح) لتنسيق من يفعل ما.

انتخاب المرشد المبسولة

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

توقيت استخدام التنسيق الموزع

  • المهام المدرجة - حالة واحدة فقط ينبغي أن ترسل الرسالة الإخبارية اليومية
  • طلب طلب تجهيز طلب طلب طلب طلب طلب تقديم طلب طلب - التأكد من تجهيز الرسائل حسب الطلب
  • العمليات الكثيفة الاستخدام - منع وقوع حالات متعددة من واقع مؤشر أسعار فائدة منخفضة خارجية
  • ** الهجرات** - حالة واحدة فقط يجب أن تقوم بالهجرات عند بدء التشغيل

وبالنسبة للسيناريوهات الأكثر تعقيداً (الوظائف المتعددة الخطوات، والمواعيد التي يمكن الاعتماد عليها في جميع عمليات إعادة النشاط)، النظر في قضية Handfire التي تتعامل مع عمليات الإقفال الموزعة تلقائياً مع قاعدة بياناتها الخلفية.

عندما لا تستخدم خدمات المعلومات

قبل أن نغوص في أدوات أكثر تطوراً مثل (هانفاير) لنتحدث عندما لا ينبغي أن لا معلومات خلفية في طلبك الرئيسي على شبكة الإنترنت.

علامات يجب أن تقسم إلى مشروع منفصل

تشارك الخدمات الأساسية العاملة في تطبيقاتك على شبكة الإنترنت الموارد مع خط طلب HTTP الخاص بك. وهذا يمكن أن يسبب مشاكل:

1 - المحتوى من الموارد

المشكلة: الخدمة الخلفية الخاصة بك تستهلك المعالجات المهمة، الذاكرة، أو وصلات قاعدة البيانات.

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

عندما تصل طلبات الويب أثناء عملية الترميز، فهي بطيئة لأن المعالج مشغول.

الحل: نقل إلى خدمة عامل منفصلة:

# 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 - الاحتياجات المختلفة من الرفع

المشكلة: عملك الأساسي يحتاج إلى مستوى مختلف عن مستوى الإنترنت الخاص بك.

  • مستوى الشبكة: جدول حركة HTTTP (تحتاج إلى 10 حالات أثناء النهار، و2 في الليل)
  • رقم معلومات أساسية: جدول لعمود الصف (قد تحتاج إلى 1 مرة عادة، 20 عندما تقوم بتجهيز دفعة)

إذا كانوا في نفس العملية، لا يمكنك قياسها بشكل مستقل.

السيناريو:

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

تقديم خدمات المعلومات الأساسية في تطبيقك على الإنترنت يعني أنه يجب عليك إدارة 20 حالة على الإنترنت فقط للتعامل مع الرسالة الإخبارية، إهدار الموارد.

3 - الاستقلال في النشر

المشكلة: تريد نشر التغييرات على الشبكة بدون إعادة تشغيل خدمات الخلفية (أو العكس).

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

كل توزيع يقاطع الاستيراد. نقله إلى خدمة عمالية منفصلة تقوم بنشرها بشكل مستقل.

4 - مجالات التقصير المختلفة

المشكلة: حشرة في الخدمة الخلفية الخاصة بك تحطّم تطبيق الويب بأكمله.

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

وإذا كان العمل الأساسي يجري في عملية منفصلة، فإنه يمكن أن يتعطل ويستأنف دون التأثير على طلبات الإنترنت.

أولاً كيف يمكن

عندما تقرر الإنقسام، ها هو الهيكل الموصى به:

الخيار 1: خدمة العاملين الصافية

إنشاء a جديد مشروع يعمل عامل خدمة قالب::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

dotnet new worker -n YourApp.Worker

الهيكل:

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

البرامج:

var builder = Host.CreateApplicationBuilder(args);

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

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

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

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

& مستخدم منفصل:

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

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

الخيار 2: مشروع منفصل مع طلب مشترك

إستعمل a رسالة سطر إلى فصل و عامل:

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

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

كِل:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

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

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

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

        return Accepted(); // Return immediately
    }
}

عامل المزامن من اللّفِل من اللّفِل:

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

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

%s: %s

الخيار 3: العمال المتخصصون المتعددون

بالنسبة للنظم المعقدة، مقسمة إلى مسؤولية:

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

ويمكن لكل عامل أن يقوم بما يلي:

  • المجلد
  • يجري النشر بشكل مستقل
  • استخدام موارد مختلفة (احتياجات عامل البريد الإلكتروني (SMTP، احتياجات عامل الفيديو)
  • (أ) أن يكون له رصد وتنبيه مختلفان؛

متى تُبقي خدمات المعلومات الأساسية في برنامجك

وعلى الرغم مما تقدم، فإن بعض السيناريوهات تُعتبر مُحكمة تماماً فيما يتعلق بخدمات المعلومات الأساسية أثناء العمليات:

الملحقات الدوريات

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

مُرْمَج

□ الحفاظ في موقع انتپ انتقال الى خدمة العمال □ |---------------|-----------------|------------------------| (ي) مستوى استخدام البولول في كل عملية □ < < 100ms o > 1 ثانية / 1 (ط) ذاكرة لكل عملية عملية < < 10 بروميد بروميد بروميد < 100 بروميد بروميد ٪ (بالدقائق/الساعات) □ الأهمية الحيوية □ غير الحرجة □

□ جداول قياسات مع حركة الشبكة العالمية □ بُعْمَة عمَل بُعْمَمَة الطوابق مثال على ذلك: Cache Haven, Fucig refload o difater production، واردات كبيرة o

مثال عالمي حقيقي: منصة المدونات

منصة المدونات التي ندرس رمزها في الجزء الثاني:

حفظت في تطبيق الشبكة::

  • MarkdownDirectoryWatcherService - مراقب مُراقب الملفات
  • UmamiBackgroundSender - سرعة سرعة الأحداث التحليلية
  • EmailSenderHostedService - الحجم الصغير وغير الحرج
  • MarkdownReAddPostsService - البداية - فقط، والتشكيلات - المسطرة

ينبغي الانتقال إلى خدمة العمال إذا زاد الجدول:

  • BrokenLinkCheckerBackgroundService - يجعل العديد من طلبات HTTP
  • SemanticIndexingBackgroundService - الاتصالات الخارجية التي ترسِّخ NAP

في الخدمة المنفصلة:

  • Mostlylucid.SchedulerService - دعامة ومرسل الرسالة الإخبارية

هذا نهج عملي: ابدأ بسيط (في العملية)، انقسم عندما يكون لديك دليل تحتاج إليه.

ما وراء الأساسيات: تبادل

كان يُبد IHostedService وقد عقد مؤتمراً بشأن BackgroundService في بعض الأحيان تحتاج إلى جدولة أكثر تعقيداً حيث تحب المكتبات يأتي في الداخل في.

2 - ينص الغلور على ما يلي:

  • **** - الوظائف الباقية على قيد الحياة
  • الوظائف التي تحتاج إلى وظائف - جدولة جدولة جدولة جدولة جدولة جدولة جدولة جدولة جدولة جدولة جدولة
  • مُضْرَك - نرى ما يجري، ما هو فشل، إعادة وظائف إعادة المحاولة
  • **** - يمكن لحواسيب خدمة متعددة متعددة تشغيل نفس قائمة الوظائف
  • متكر - الوظائف الفاشلة تُعاد تجربتها تلقائياً مع التراجع الأساسي

هنا مثال بسيط:

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

مواصفات الإجمال:

  • ضمان سير العمل في الوقت المقرر
  • إعادة IF فشل IF IF فشل
  • جاري التنفيذ
  • توفير وسيلة رصد لرصد كل شيء
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

متى إلى استخدام:

  • تحتاج إلى طاولات الوظائف المستمرة التي تنجو من عمليات إعادة التشغيل
  • هل تريد لوحة متابعة و تحريك الوظائف
  • تحتاج إلى توزيع وظيفة معالجة عبر خوادم متعددة
  • تُريدُ مُدْمَمَجَمَلَةً في مُعَادَلَةً ومُخَشَلَةً
  • تحتاج إلى جدولة المهام المتكررة

متى إلى اللصق مع IHolstedService/ Backgngard Service:

  • تحتاج إلى سيطرة دقيقة على دورة حياة الخدمة
  • خدماتك يجب أن تتفاعل مع الأحداث في الوقت الحقيقي
  • تريد التقليل من الاعتمادات
  • أنت تبني مهمة دورية بسيطة لا تحتاج إلى استمرار

خيارات أخرى

وفي حين أن الإطفاء شائع، هناك مكتبات أخرى ينبغي النظر فيها:

Qotz.net:

  • جدولة أكثر مرونة من
  • يُدعِم
  • يمكن المواصلة إلى قواعد بيانات متعددة
  • أكثر تعقيداً من المستوى المستهدف ولكن أكثر قوة

ما/SSSS:

  • تنفيذ تشغيل حافلات الرسائل الكاملة
  • تحسين النظم الموزعة والخدمات الصغيرة
  • مقالات الدعم (تدفقات العمل الطويلة الأمد)
  • منح منحني التعلم

حالات الميزات/حامل حملا:

  • إذا كنت في السحابة، و النظر في خادم
  • كل عملية تنفيذ بدلا من الاستمرار في خدمة جارية
  • المُقَرَّر
  • بعض التأخيرات في الإنفاق على بدءات بردية

أولاً - موجز

في الجزء الأول، قمنا بتغطية النُهُج الأساسية لخدمات المعلومات الأساسية في قاعدة ASP.net:

  1. الخدمة المعتمدة - الأساس، أقصى قدر من المرونة
  2. الخلفية SSSSSS - الدرجة الأساسية المناسبة لحلقات طويلة الأمد
  3. تَوْءْ بدء - جعل الخدمات تنتظر بعضها البعض
  4. التنسيق - استخدام Resedis لسين سيناريوهات متعددة الأقسام
  5. **** - عندما تحتاج إلى أعمال مستمرة ومواعيد معقدة

أهم الدروس المستفادة:

  • يجري تكرار تحميل و إلغاء المزامض في الحالة
  • تقرر ما إذا كان يجب على بدء التشغيل Assyync أن توقف أو ترجع فوراً
  • عملية المُسْتَرَحَة
  • استخدام مهمّة. متى أي منها مع خانة الإقفال إلى احترام وقت الإقفال

داخل الجزء الثانيسوف نفحص تنفيذات العالم الحقيقي من منصة مدوّنة إنتاج:

  • مُراقبات نظام الملفات التي تُزِخِل ملفات إلى قاعدة بيانات
  • مرسِلات بريد إلكتروني مع سياسات إعادة المُحاولات وأجهزة مُقْسِر الدوائر
  • تحليل المحل المحل المحللات
  • مُفَرِّزات البحث Syminitate Sears التي تُجهز المحتوى الذي يُشكِل المحتوى على نحو متزامن
  • مدققات الوصلات التي تقوم دورياً بالتصديق الخارجي

وتبين هذه الأمثلة الأنماط المستمدة من تنفيذ الجزء الأول، بما في ذلك نمط تنسيق بدء التشغيل والمناولة السليمة للإغلاق.

& ما

Finding related posts...
logo

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