该NormalHostedService.StopAsync()方法从不调用。如果该服务需要进行一些清理,那么您会遇到问题。例如,也许您需要从Consul处优雅地注销该服务,或者取消订阅Kafka主题-现在不会发生。
那么这是怎么回事?超时从哪里来?
原因:HostOptions.ShutDownTimeout您可以在应用程序关闭时运行中找到有问题的。简化的版本如下所示:
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 。