26

扩展shutdown超时设置以保证IHostedService正常关闭

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU4Mjc4NzgyOQ%3D%3D&%3Bmid=2247485215&%3Bidx=1&%3Bsn=c6c655dc0063b56c4ce94dae56462913
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

vAVNbm6.gif

我最近发现一个问题,当应用程序关闭时,我们的应用程序没有正确执行在 IHostedService 中的 StopAsync 方法。经过反复验证发现,这是由于某些服务对关闭信号做出响应所需的时间太长导致的。在这篇文章中,我将展示出现这个问题的一个示例,并且会讨论它为什么会发生以及如何避免这种情况出现。

作者:依乐祝

首发地址:https://www.cnblogs.com/yilezhu/p/12952977.html

英文地址:https://andrewlock.net/extending-the-shutdown-timeout-setting-to-ensure-graceful-ihostedservice-shutdown/

使用IHostedService运行后台服务

ASP.NET Core 2.0引入了 IHostedService 用于运行后台任务的界面。该接口包含两种方法:

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

StartAsync 在应用程序启动时被调用。在ASP.NET核心2.X发生这种情况只是 之后 在应用程序启动处理请求,而在ASP.NET核心3.x中托管服务开始只是 之前 在应用程序启动处理请求。

StopAsync 当应用程序收到shutdown( SIGTERM )信号时(例如,您CTRL+C在控制台窗口中按入,或者应用程序被主机系统停止时),将调用。这样,您就可以关闭所有打开的连接,处置资源,并通常根据需要清理类。

实际上,实现此接口实际上有一些微妙之处,这意味着您通常希望从helper类 BackgroundService 派生。

如果您想了解更多,Steve Gordon会开设有关Pluralsight的课程“ 构建ASP.NET Core托管服务和.NET Core Worker Services ”。

关闭 IHostedService 实施的问题

我最近看到的问题是 OperationCanceledException 在应用程序关闭时引发的问题:

Unhandled exception. System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)

我将这个问题的根源追溯到一个特定的 IHostedService 实现。我们将 IHostedService s作为每个Kafka消费者的主机。具体操作并不重要-关键在于关闭 IHostedService 相对较慢:取消订阅可能需要几秒钟。

问题的一部分是Kafka库(和基础 librdkafka 库)使用同步阻塞 Consume 调用而不是异步可取消调用的方式。解决这个问题的方法不是很好。

理解此问题的简便方法是一个示例。

演示问题

解决此问题的最简单方法是创建一个包含两个 IHostedService 实现的应用程序:

  • NormalHostedService 在启动和关闭时记录日志,然后立即返回。

  • SlowHostedService 记录启动和停止的时间,但要花10秒才能完成关闭

这两个类的实现如下所示。的 NormalHostedService 很简单:

public class NormalHostedService : IHostedService
{
    readonly ILogger<NormalHostedService> _logger;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("NormalHostedService started");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("NormalHostedService stopped");
        return Task.CompletedTask;
    }
}

SlowHostedService 几乎是相同的,但它有一个 Task.Delay 是需要10秒,以模拟一个缓慢的关机

public class SlowHostedService : IHostedService
{
    readonly ILogger<SlowHostedService> _logger;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("SlowHostedService started");
        return Task.CompletedTask;
    }

    public async Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("SlowHostedService stopping...");
        await Task.Delay(10_000);
        _logger.LogInformation("SlowHostedService stopped");
    }
}

IHostedService 就是我曾在实践中只用了1秒关机,但我们有很多人,所以整体效果是一样的上面!

该服务中注册的顺序 ConfigureServices 是非常重要的在这种情况下-来证明这个问题,我们需要 SlowHostedService 被关闭 第一 。服务以相反的顺序关闭,这意味着我们需要 最后 注册它:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHostedService<NormalHostedService>();
    services.AddHostedService<SlowHostedService>();
}

当我们运行该应用程序时,您将像往常一样看到启动日志:

info: ExampleApp.NormalHostedService[0]
      NormalHostedService started
info: ExampleApp.SlowHostedService[0]
      SlowHostedService started
...
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.

但是,如果按CTRL+C关闭该应用程序,则会出现问题。在 SlowHostedService 完成关闭,但随后一个 OperationCanceledException 被抛出:

info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
info: ExampleApp.SlowHostedService[0]
      SlowHostedService stopping...
info: ExampleApp.SlowHostedService[0]
      SlowHostedService stopped

Unhandled exception. System.OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.WaitForShutdownAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
   at ExampleApp.Program.Main(String[] args) in C:\repos\andrewlock\blog-examples\SlowShutdown\Program.cs:line 16

NormalHostedService.StopAsync() 方法从不调用。如果该服务需要进行一些清理,那么您会遇到问题。例如,也许您需要从Consul处优雅地注销该服务,或者取消订阅Kafka主题-现在不会发生。

那么这是怎么回事?超时从哪里来?

原因:HostOptions.ShutDownTimeout

您可以在应用程序关闭时运行的框架 Host 实现中找到有问题的代码。简化的版本如下所示:

internal class Host: IHost, IAsyncDisposable
{
    private readonly HostOptions _options;
    private IEnumerable<IHostedService> _hostedServices;

    public async Task StopAsync(CancellationToken cancellationToken = default)
    {
        // Create a cancellation token source that fires after ShutdownTimeout seconds
        using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
        using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
        {
            // Create a token, which is cancelled if the timer expires
            var token = linkedCts.Token;

            // Run StopAsync on each registered hosted service
            foreach (var hostedService in _hostedServices.Reverse())
            {
                // stop calling StopAsync if timer expires
                token.ThrowIfCancellationRequested();
                try
                {
                    await hostedService.StopAsync(token).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
        }

        // .. other stopping code
    }
}

这里的关键点 CancellationTokenSource 是配置为 HostOptions.ShutdownTimeout 之后触发的。默认情况下,这会在5秒后触发。这意味着5秒后将放弃托管服务关闭-  IHostedService 必须在此超时内关闭所有托管服务。

public class HostOptions
{
    public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
}

foreach 循环的第一次迭代中, SlowHostedService.Stopasync() 执行,需要10秒钟才能运行。在第二次迭代中,超过了5s超时,因此 token.ThrowIfCancellationRequested(); 抛出 OperationConcelledException 。这将退出控制流,并且 NormalHostedService.Stopasync() 永远不会执行。

有一个简单的解决方案-增加 shutdown 超时时间!

解决方法:增加shutdown超时时间

HostOptions 默认情况下未在任何地方显式配置它,因此您需要在 ConfigureSerices 方法中手动对其进行配置。例如,以下配置将超时增加到15s:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHostedService<NormalHostedService>();
    services.AddHostedService<SlowShutdownHostedService>();

    // Configure the shutdown to 15s
    services.Configure<HostOptions>(
        opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(15));
}

或者,您也可以从配置中加载超时时间。例如,如果将以下内容添加到 appsettings.json

{
    "HostOptions": {
        "ShutdownTimeout": "00:00:15"
    }
    // other config
}

然后,您可以将 HostOptions 配置部分绑定到 HostOptions 对象:

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<NormalHostedService>();
        services.AddHostedService<SlowShutdownHostedService>();

        // bind the config to host options
        services.Configure<HostOptions>(Configuration.GetSection("HostOptions"));
    }
}

这会将序列化的 TimeSpan 值绑定 00:00:15 到该 HostOptions 值,并将超时间设置为15s。使用该配置,现在当我们停止应用程序时,所有服务都将正确关闭:

nfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
info: SlowShutdown.SlowShutdownHostedService[0]
      SlowShutdownHostedService stopping...
info: SlowShutdown.SlowShutdownHostedService[0]
      SlowShutdownHostedService stopped
info: SlowShutdown.NormalHostedService[0]
      NormalHostedService stopped

现在,您的应用程序将等待15秒,以使所有托管服务在退出之前完成关闭!

摘要

在这篇文章中,我讨论了一个最近发现的问题,该问题是当应用程序关闭时,我们的应用程序未在 IHostedService 实现中的 StopAsync 中运行该方法。这是由于某些后台服务对关闭信号做出响应所需的时间太长,并且超过了关闭超时时间。文中我演示了单个服务需要10秒才能关闭服务来重现问题,但实际上,只要所有服务的 关闭时间超过默认5秒,就会发生此问题。

该问题的解决方案是 HostOptions.ShutdownTimeout 使用标准ASP.NET Core  IOptions<T> 配置系统将配置值扩展为超过5s 。

往期 精彩 回顾

【推荐】.NET Core开发实战视频课程   ★★★

.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划

【.NET Core微服务实战-统一身份认证】开篇及目录索引

Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

10个小技巧助您写出高性能的ASP.NET Core代码

用abp vNext快速开发Quartz.NET定时任务管理界面

在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度

现身说法:实际业务出发分析百亿数据量下的多表查询优化

关于C#异步编程你应该了解的几点建议

C#异步编程看这篇就够了

给我好看

uaQRNfj.jpg!web

您看此文用

·

秒,转发只需1秒呦~

aArqqiY.png!web

好看你就

点点


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK