Реализация фоновых задач в микрослужбах с помощью IHostedService и класса BackgroundService

Совет

Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Фоновые задачи и запланированные задания обычно требуется использовать в любом приложении независимо от того, строится ли оно на основе архитектуры микрослужб. Разница при использовании архитектуры микрослужб в том, что вы можете реализовать фоновую задачу в отдельном процессе или контейнере, чтобы затем масштабировать его по мере необходимости.

Если посмотреть шире, в .NET такие задачи называются размещенными службами, так как они представляют собой службы или логику, размещаемые в узле, приложении или микрослужбе. Обратите внимание на то, что в этом случае под размещенной службой понимается просто класс с логикой фоновой задачи.

Начиная с версии .NET Core 2.0, платформа предоставляет новый интерфейс IHostedService, который позволяет легко реализовывать размещенные службы. Главная идея заключается в том, что вы можете регистрировать несколько фоновых задач (размещенных служб), которые выполняются в фоновом режиме в процессе работы веб-узла или обычного узла, как показано на рис. 6-26.

Diagram comparing ASP.NET Core IWebHost and .NET Core IHost.

Рис. 6-26. Использование интерфейса IHostedService в классах WebHost и Host

ASP.NET Core версий 1.x и 2.x поддерживает IWebHost для фоновых процессов в веб-приложениях. .NET Core 2.1 и более поздних версий поддерживает IHost для фоновых процессов с простыми консольными приложениями. Обратите внимание на различие между WebHost и Host.

WebHost (базовый класс, реализующий интерфейс IWebHost) в ASP.NET Core 2.0 — это артефакт инфраструктуры, с помощью которого процесс получает доступ к возможностям сервера HTTP, например при реализации веб-приложения MVC или службы веб-интерфейса API. Он предоставляет все новые преимущества инфраструктуры в ASP.NET Core, позволяя использовать внедрение зависимостей, вставлять промежуточные слои в конвейер запросов и т. д. WebHost использует те же IHostedServices для фоновых задач.

Объект Host (базовый класс, реализующий IHost) впервые появился в .NET Core 2.1. По существу, класс Host обеспечивает практически такую же инфраструктуру, что и класс WebHost (внедрение зависимостей, размещенные службы и т. д.), но в этом случае узел должен представлять собой более простой процесс без связанных с MVC веб-интерфейсами API или сервером HTTP возможностей.

Таким образом, вы можете либо создать специальный хост-процесс с помощью IHost для реализации только размещенных служб, например микрослужбу, предназначенную для размещения IHostedServices, либо расширить существующий в ASP.NET Core класс WebHost, например существующий веб-интерфейс API ASP.NET Core или приложение MVC.

Каждый подход имеет свои преимущества и недостатки в зависимости от потребностей бизнеса и требований к масштабируемости. Основной принцип заключается в том, что если фоновые задачи не связаны с HTTP (IWebHost), следует использовать интерфейс IHost.

Регистрация размещенных служб в WebHost или Host

Давайте более подробно разберем интерфейс IHostedService, так как его использование в классах WebHost и Host во многом схоже.

Библиотека SignalR — один из примеров артефактов, использующих размещенные службы, однако ее можно применять и для более простых задач, таких как следующие:

  • фоновая задача опроса базы данных для поиска изменений;
  • запланированная задача для периодического обновления кэша;
  • реализация QueueBackgroundWorkItem, которая позволяет задаче выполняться в фоновом потоке;
  • обработка сообщений из очереди сообщений веб-приложения в фоновом режиме при использовании общих служб, таких как ILogger;
  • фоновая задача, запускаемая с помощью Task.Run().

Выполнение любого из этих действий можно перенести в фоновую задачу, которая реализует IHostedService.

Для добавления одного или нескольких экземпляров IHostedServices в WebHost или Host их следует зарегистрировать посредством метода расширения AddHostedService в классе ASP.NET Core WebHost (или в классе Host в .NET Core 2.1 и более поздних версий). В основном необходимо зарегистрировать размещенные службы в Program.cs запуска приложения.

//Other DI registrations;

// Register Hosted Services
builder.Services.AddHostedService<GracePeriodManagerService>();
builder.Services.AddHostedService<MyHostedServiceB>();
builder.Services.AddHostedService<MyHostedServiceC>();
//...

В этом коде размещенная служба GracePeriodManagerService представляет собой реальный код микрослужбы размещения заказов в приложении eShopOnContainers, а другие две службы — это просто дополнительные примеры.

Выполнение фоновой задачи IHostedService согласуется с временем существования приложения (узла или микрослужбы). Задачи регистрируются при запуске приложения, а при завершении его работы есть возможность произвести надлежащую обработку или очистку.

Фоновый поток можно запускать для выполнения любой задачи и без использования IHostedService. Различие именно в том, что будет происходить при завершении работы приложения: поток будет просто завершаться без возможности надлежащей очистки.

Интерфейс IHostedService

При регистрации IHostedService.NET вызывает StartAsync() и StopAsync() методы типа IHostedService во время запуска и остановки приложения соответственно. Дополнительные сведения см. в интерфейсе IHostedService.

Как вы можете себе представить, можно создать несколько реализаций IHostedService и зарегистрировать каждую из них в Program.cs, как показано ранее. Все эти размещенные службы будут запускаться и останавливаться вместе с приложением или микрослужбой.

Как разработчик, вы несете ответственность за обработку действия завершения своих служб при активации метода StopAsync() узлом.

Реализация IHostedService с помощью пользовательского класса размещенной службы, производного от базового класса BackgroundService

Можно пойти дальше и создать пользовательский класс размещенной службы с нуля, реализовав интерфейс IHostedService, как это требуется делать в .NET Core 2.0 и более поздних версий.

Но большинство фоновых задач имеют схожие потребности в отношении управления токенами отмены и других типичных операций, поэтому существует удобный абстрактный базовый класс BackgroundService, от которого можно создавать производные классы (доступно с .NET Core 2.1).

Он будет выполнять основную часть работы, связанной с настройкой фоновой задачи.

Следующий код — это абстрактный базовый класс BackgroundService, реализованный в .NET.

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

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

Благодаря такой унаследованной реализации при создании собственного класса размещенной службы, производного от приведенного выше абстрактного базового класса, необходимо просто реализовать в нем метод ExecuteAsync(). Пример представлен в следующем упрощенном коде из приложения eShopOnContainers, который опрашивает базу данных и при необходимости публикует события интеграции в шине событий.

public class GracePeriodManagerService : BackgroundService
{
    private readonly ILogger<GracePeriodManagerService> _logger;
    private readonly OrderingBackgroundSettings _settings;

    private readonly IEventBus _eventBus;

    public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
                                     IEventBus eventBus,
                                     ILogger<GracePeriodManagerService> logger)
    {
        // Constructor's parameters validations...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"GracePeriodManagerService is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($" GracePeriod background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"GracePeriod task doing background work.");

            // This eShopOnContainers method is querying a database table
            // and publishing events into the Event Bus (RabbitMQ / ServiceBus)
            CheckConfirmedGracePeriodOrders();

            try {
                    await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
                }
            catch (TaskCanceledException exception) {
                    _logger.LogCritical(exception, "TaskCanceledException Error", exception.Message);
                }
        }

        _logger.LogDebug($"GracePeriod background task is stopping.");
    }

    .../...
}

В этом конкретном примере eShopOnContainers выполняет метод приложения, который отправляет запрос к таблице базы данных для поиска заказов с определенным состоянием, а при применении изменений публикует события интеграции через шину событий (она может быть основана на RabbitMQ или служебной шине Azure).

Естественно, вместо этого можно выполнять любую другую фоновую бизнес-задачу.

По умолчанию для токена отмены задается время ожидания, равное 5 секундам, хотя это значение можно изменить при создании WebHost с помощью расширения UseShutdownTimeout интерфейса IWebHostBuilder. Это означает, что отмена службы ожидается в течение 5 секунд. В противном случае ее работа будет завершена внезапно.

В следующем коде это время изменяется на 10 секунд:

WebHost.CreateDefaultBuilder(args)
    .UseShutdownTimeout(TimeSpan.FromSeconds(10))
    ...

Сводная диаграмма классов

На приведенном ниже рисунке представлена наглядная сводка классов и интерфейсов, которые задействованы в реализации IHostedService.

Diagram showing that IWebHost and IHost can host many services.

Рис. 6-27. Диаграмма классов и интерфейсов, связанных с IHostedService

Схема классов: IWebHost и IHost могут разместить много служб, наследующих от BackgroundService, который реализует IHostedService.

Основные положения и моменты, связанные с развертыванием

Важно отметить, что способ развертывания узла WebHost в ASP.NET Core или Host в .NET может влиять на конечное решение. Например, если вы развертываете WebHost в службах IIS или обычной службе приложений Azure, работа узла может быть завершена из-за перезапуска пула приложений. Однако если вы развертываете узел как контейнер в оркестраторе, таком как Kubernetes, вы можете контролировать гарантированное число активных экземпляров узла. Кроме того, в облачной среде можно рассмотреть другие подходы, особенно предназначенные для таких сценариев, как Функции Azure. Если же требуется, чтобы служба работала постоянно и была развернута в Windows Server, используйте службу Windows.

Даже при развертывании WebHost в пуле приложений применим ряд сценариев, таких как повторное заполнение или сброс кэша в памяти для приложения.

Интерфейс IHostedService предоставляет удобный способ запуска фоновых задач в веб-приложении ASP.NET Core (в .NET Core 2.0 или более поздних версий) или в любом процессе или узле (при использовании IHost начиная с .NET Core 2.1). Его главное преимущество — это возможность надлежащей отмены с целью очистки кода фоновых задач при завершении работы самого узла.

Дополнительные ресурсы