zoukankan      html  css  js  c++  java
  • 【半译】扩展shutdown超时设置以保证IHostedService正常关闭

    我最近发现一个问题,当应用程序关闭时,我们的应用程序没有正确执行在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实现。我们将IHostedServices作为每个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:
    eposandrewlocklog-examplesSlowShutdownProgram.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 。

  • 相关阅读:
    A component required a bean named xxx that could not be found. Action: Consider defining
    jmeter 配置csv 登陆网站 报错
    动手动脑(文件与流)
    java异常处理总结
    动手动脑(异常处理)
    动手动脑(继承与多态)
    查询对象个数
    动手动脑
    动手动脑
    统计英语文章的单词
  • 原文地址:https://www.cnblogs.com/yilezhu/p/12952977.html
Copyright © 2011-2022 走看看