标签搜索

在ASP.NET Core中的应用程序启动时运行异步任务(四)

冰封一夏
2021-09-28 23:56:25 / 43 阅读 / 正在检测是否收录...

第4部分-使用运行状况检查在ASP.NET Core中运行异步任务

这是该系列的第一篇文章:在ASP.NET Core中的应用程序启动时运行异步任务

  1. 第1部分-用于运行异步任务的内置选项(本文)
  2. 第2部分-两种运行异步任务的方法
  3. 第3部分-对异步任务示例和其他可能解决方案的反馈
  4. 第4部分-使用运行状况检查在ASP.NET Core中运行异步任务

在这篇文章中,我展示了一种在应用启动时运行异步任务的方法,在本系列的第一篇文章中我对此打了折扣,但是Damian Hickey最近表达了对它的偏爱。此方法使用IHostedService抽象来运行启动任务,并带有运行状况检查以指示所有启动任务何时完成。此外,一小段中间件可确保503启动任务尚未完成时,非运行状况检查流量返回响应。

ASP.NET Core运行状况检查:简要入门

这篇文章中的方法使用ASP.NET Core 2.2中引入的运行状况检查功能。运行状况检查是Web应用程序的常见功能,用于指示应用程序是否正常运行。协调员和流程经理经常使用它们来检查应用程序是否正常运行,并能够为流量提供服务。

ASP.NET Core中的运行状况检查功能是高度可扩展的。您可以注册任何数量的运行状况检查,以测试应用程序“运行状况”的某些方面。您还将添加HealthCheckMiddleware到应用程序的中间件管道中。这会在您选择的路径(例如/healthz)上响应请求,并带有200指示应用程序运行状况良好或503指示应用程序运行状况不佳的信息。您可以自定义所有细节,但这是广泛的概念。

Damian主张使用运行状况检查来指示应用程序的启动任务何时完成以及该应用程序准备开始处理请求的时间。他表示希望Kestrel快速运行并响应运行状况检查请求,而不是允许运行状况请求超时(这是您使用本系列第2部分中讨论的两种方法获得的行为)。我认为这是有道理的,并作为本文中显示的方法的基础。

您没有理由必须使用内置的ASP.NET Core 2.2运行状况检查功能。实际的运行状况检查非常简单,并且可以根据需要将其轻松地实现为一小部分中间件。

更改异步启动任务的要求

在本系列的第一篇文章中,我描述了在应用启动时运行各种任务的需求。我的要求如下:

  1. 任务应异步运行(即使用asyncawait),以避免异步同步。
  2. 在运行任务之前,应先构建DI容器(最好是中间件管道)。
  3. 在Kestrel开始处理请求之前,应完成所有任务。

另外,对于我提供的示例,所有任务都是串行运行的(而不是并行运行),等待一个任务完成之后再开始下一个任务。这不是一个明确的要求,只是一个在某种程度上简化了事情的要求。

第1点和第2点仍然适用于本文中显示的方法,但是第3点被明确删除并交换为以下两点:

  1. 红隼开始,可以开始处理面前的任务请求开始,但它应该给所有非健康检查响应流量503响应。
  2. 所有启动任务完成后,运行状况检查仅应返回“运行状况”。

在下一节中,我将概述满足这些要求的各种运动部件。

使用运行状况检查运行异步启动任务的概述

此处显示的解决方案有四个主要组成部分:

  1. 共享的(单个)上下文。这样可以跟踪需要执行多少任务,以及仍在运行多少任务。
  2. 一个或多个启动任务。这些是我们在应用开始提供流量之前需要运行的任务。
    • 源自IHostedService使用标准ASP.NET Core后台服务功能
    • 应用启动时在共享上下文中注册。
    • 在Kestrel启动之后立即开始运行(以及其他IHostedService实现)
    • 完成后,在共享上下文中将任务标记为完成
  3. “启动任务”运行状况检查。一个ASP.NET Core运行状况检查实现,用于检查共享上下文以查看任务是否已完成。返回Unhealthy直到所有任务完成。
  4. 一种“屏障”中间件。紧随标准之后的一小部分中间件HealthCheckMiddleware。通过返回a来阻止所有请求,503直到共享上下文指示所有启动任务已完成。

整体系统图

我将在以下各节中逐步介绍这些组件中的每一个,以构建完整的解决方案。

跟踪已完成的任务

此设计中的关键组件是StartupTaskContext。这是一个共享/单个对象,用于跟踪所有启动任务是否已完成。

与典型的ASP.NET Core设计概念保持一致,我没有使用static类或方法,而是依靠DI容器创建单例实例。这对于功能来说不是必需的。您可以根据需要使用共享对象。无论哪种方式,都需要确保这些方法是线程安全的,因为如果同时完成多个启动任务,则可以同时访问这些方法。

public class StartupTaskContext
{
    private int _outstandingTaskCount = 0;

    public void RegisterTask()
    {
        Interlocked.Increment(ref _outstandingTaskCount);
    }

    public void MarkTaskAsComplete()
    {
        Interlocked.Decrement(ref _outstandingTaskCount);
    }

    public bool IsComplete => _outstandingTaskCount == 0;
}

这几乎是最基本的实现,它可以统计尚未完成的任务数。一旦_outstandingTaskCount达到0,所有启动任务就完成了。显然,在这里可以做更多的事情来使实现更健壮,但是在大多数情况下它都可以做到。

除了共享上下文之外,我们还需要一些启动任务来运行。我们使用IStartupTask继承自的标记器接口将服务标记为启动任务IHostedService

public interface IStartupTask : IHostedService 

我使用内置IHostedService接口作为WebHost在Kestrel之后自动启动它们的句柄,它使您可以使用帮助程序类,如BackgroundService帮助编写长时间运行的任务的类。

除了实现标记器接口之外,启动任务还应在完成其工作之后调用MarkTaskAsComplete()共享库StartupTaskContext

为了简单起见,下面显示的示例服务仅等待10秒钟,然后才调用MarkTaskAsComplete()被注入的对象StartupTaskContext

public class DelayStartupTask : BackgroundService, IStartupTask
{
    private readonly StartupTaskContext _startupTaskContext;
    public DelayStartupTask(StartupTaskContext startupTaskContext)
    {
        _startupTaskContext = startupTaskContext;
    }

    // run the task
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Delay(10_000, stoppingToken);
        _startupTaskContext.MarkTaskAsComplete();
    }
}

与ASP.NET Core中常见的一样,我创建了一些帮助程序扩展方法,用于使用DI容器注册共享上下文和启动任务:

  • AddStartupTasks()StartupTaskContext向DI容器注册共享,确保添加的共享不止一次。
  • AddStartupTask<T>() 用于添加特定的启动任务,它
    • 通过调用在共享上下文中注册任务 RegisterTask()
    • 将共享上下文添加到DI容器(如果尚未添加)
    • 将任务添加<T>为托管服务(如IStartupTask实施IHostedService

public static class StartupTaskExtensions
{
    private static readonly StartupTaskContext _sharedContext = new StartupTaskContext();
    public static IServiceCollection AddStartupTasks(this IServiceCollection services)
    {
        // Don't add StartupTaskContext if we've already added it
        if (services.Any(x => x.ServiceType == typeof(StartupTaskContext)))
        {
            return services;
        }

        return services.AddSingleton(_sharedContext);
    }

    public static IServiceCollection AddStartupTask<T>(this IServiceCollection services) 
        where T : class, IStartupTask
    {
        _sharedContext.RegisterTask();
        return services
            .AddStartupTasks() // in case AddStartupTasks() hasn't been called
            .AddHostedService<T>();
    }
}

注册的例子任务DelayStartupTaskStartup.ConfigureServices()是一个单一的方法调用:

public void ConfigureServices(IServiceCollection services)
{
    // ... Existing configuration
    services.AddStartupTask<DelayStartupTask>();
}

这就是启动任务的机制,现在我们可以添加运行状况检查。

实施StartupTasksHealthCheck

在许多情况下,如果您需要自定义运行状况检查,则应检出BeatPulse库。它直接与ASP.NET Core 2.2集成,当前列出约20种不同的检查,包括系统(磁盘,内存)和网络检查,以及RabbitMQ,SqlServer或Redis等集成检查。

幸运的是,如果您需要编写自己的自定义运行状况检查,则实现起来IHealthCheck很简单。它有一个异步方法CheckHealthAsync,从中返回HealthCheckResult三个值中的一个实例:HealthyDegraded,或Unhealthy

我们的自定义运行状况检查会检查的值,StartupTaskContext.IsComplete并在适当时返回HealthyUnhealthy

public class StartupTasksHealthCheck : IHealthCheck
{
    private readonly StartupTaskContext _context;
    public StartupTasksHealthCheck(StartupTaskContext context)
    {
        _context = context;
    }

    public Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context, 
        CancellationToken cancellationToken = new CancellationToken())
    {
        if (_context.IsComplete)
        {
            return Task.FromResult(HealthCheckResult.Healthy("All startup tasks complete"));
        }

        return Task.FromResult(HealthCheckResult.Unhealthy("Startup tasks not complete"));
    }
}

要注册运行状况检查Startup.ConfigureServices(),请调用AddHealthChecks()扩展方法,然后调用AddCheck<>()。您可以为运行状况检查提供一个有意义的名称,"Startup tasks"在这种情况下:

public void ConfigureServices(IServiceCollection services)
{
    // ... Existing configuration
    services.AddStartupTask<DelayStartupTask>();

    services
        .AddHealthChecks()
        .AddCheck<StartupTasksHealthCheck>("Startup tasks");
}

最后,将运行状况检查中间件添加到中的中间件管道的开头Startup.Configure(),定义用于运行状况检查的路径("/healthz"

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/healthz");

    // ... Other middleware config
}

这是实现此帖子的主要目标所需的所有步骤。如果启动应用程序并点击/healthz终点,您将得到503响应并看到文本Unhealthy。10秒后,DelayStartupTask完成操作后,您将得到一个200响应和以下Healthy文本:

运行状况检查失败然后成功的示例

请注意,添加此运行状况检查不会影响您可能拥有的任何其他运行状况检查。运行状况检查终结点将返回不正常状态,直到所有运行状况检查通过为止,包括StartupTasksHealthCheck。还要注意,如果您不想使用内置的ASP.NET Core健康状况检查功能,则可以使用一小部分中间件来实现上述功能。

我们尚未满足的一项要求是,在所有启动任务完成之后,非运行状况检查流量才应该能够调用我们的常规中间件管道/ MVC操作。理想情况下,无论如何都是不可能的,因为负载平衡器应先等待运行状况良好的检查返回,200然后再将流量路由到您的应用程序。但是比后悔更安全!

中间件

我们需要的中间件非常简单:如果启动任务尚未完成,则需要返回错误响应代码。如果任务完成,则不执行任何操作,并让其余中间件管道完成。

以下自定义中间件完成了该工作,并"Retry-After"StartupTaskContext指示任务未完成的情况下将标头添加到响应中。您可以提取诸如Retry-After值,纯文本响应("Service Unavailable")或什至对配置对象的响应代码之类的东西,但对于本文,我将其简化了:

public class StartupTasksMiddleware
{
    private readonly StartupTaskContext _context;
    private readonly RequestDelegate _next;

    public StartupTasksMiddleware(StartupTaskContext context, RequestDelegate next)
    {
        _context = context;
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        if (_context.IsComplete)
        {
            await _next(httpContext);
        }
        else
        {
            var response = httpContext.Response;
            response.StatusCode = 503;
            response.Headers["Retry-After"] = "30";
            await response.WriteAsync("Service Unavailable");
        }
    }
}

在运行状况检查中间件之后注册中间件。如果启动任务尚未完成,则通过健康检查的所有流量都将被停止。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/healthz");
    app.UseMiddleware<StartupTasksMiddleware>();

    // ... Other middleware config
}

现在,如果您运行应用程序并点击非健康检查端点,则最初会得到503。启动任务完成后,将恢复正常功能。

在启动任务完成之前,应用程序主页返回503的示例

概括

达米安·希基(Damian Hickey)向我建议了这篇文章中的方法。它使用ASP.NET Core的运行状况检查功能向负载均衡器和协调器指示应用程序是否已准备好运行。这样可以确保Kestrel尽快启动,因此,与我在本系列第2部分中介绍的方法相比,Kestrel可以减少负载均衡器中的网络超时次数。您可以在GitHub上的这篇文章中找到完整的代码示例。

10

评论

博主关闭了所有页面的评论