扩展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
实现。我们将 IHostedService
s作为每个Kafka消费者的主机。具体操作并不重要-关键在于关闭 IHostedService
相对较慢:取消订阅可能需要几秒钟。
问题的一部分是Kafka库(和基础 librdkafka
库)使用同步阻塞 Consume
调用而不是异步可取消调用的方式。解决这个问题的方法不是很好。
理解此问题的简便方法是一个示例。
演示问题
解决此问题的最简单方法是创建一个包含两个 IHostedService
实现的应用程序:
-
NormalHostedService
在启动和关闭时记录日志,然后立即返回。 -
SlowHostedService
记录启动和停止的时间,但要花10秒才能完成关闭
这两个类的实现如下所示。的 NormalHostedService
很简单:
public class NormalHostedService : IHostedService { readonly ILogger_logger; public NormalHostedService(ILogger 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_logger; public SlowHostedService(ILogger 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(); services.AddHostedService (); }
当我们运行该应用程序时,您将像往常一样看到启动日志:
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_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(); services.AddHostedService (); // Configure the shutdown to 15s services.Configure ( 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(); services.AddHostedService (); // bind the config to host options services.Configure (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
配置系统将配置值扩展为超过5s 。
往期 精彩 回顾
.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
【.NET Core微服务实战-统一身份认证】开篇及目录索引
Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)
.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了
用abp vNext快速开发Quartz.NET定时任务管理界面