如何确保优雅地关闭 IHostedService
目录
我最近遇到一个问题,当我们的应用程序关闭时,它没有在我们的 IHostedService
实现中运行 StopAsync
方法。结果发现,这是由于一些服务对关机信号响应时间过长造成的。在这篇文章中,我展示了一个问题的示例,讨论了它发生的原因以及如何避免它。
使用 IHostedService 运行后台服务
ASP.NET Core 2.0 引入了 IHostedService
接口用于运行后台任务。该接口包括两个方法:
public
StartAsync
在应用程序启动时被调用。在 ASP.NET Core 2.x 中,这在应用程序开始处理请求之后发生的,而在 ASP.NET Core 3.x 中,托管服务是在应用程序开始处理请求之前启动的。
StopAsync
在应用程序接收到关闭(SIGTERM
)信号时被调用,例如当您在控制台窗口中按下 CTRL+C
,或者应用程序被宿主系统停止时。这允许您关闭任何打开的连接,释放资源,并根据需要清理您的类。
在实际操作中,实现这个接口实际上有一些细微之处,这意味着你通常希望从辅助类 BackgroundService
派生。
如果你想了解更多,Steve Gordon 在 Pluralsight 上有一门课程“Building ASP.NET Core Hosted Services and .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
作为我们每个 Kafka 消费者的主机。具体的细节并不重要——关键在于关闭 IHostedService
相对较慢:可能需要几秒钟来取消订阅。
Part of the problem is the way the Kafka library (and underlying
librdkafka
library) uses synchronous, blockingConsume
calls, instead of async, cancellable calls. There’s not a great way around that.
通过一个例子最容易理解这个问题。
展示问题
最简单理解这个问题的方式是创建一个包含两个 IHostedService
实现的应用程序:
NormalHostedService
在启动和关闭时记录日志,然后立即返回。SlowHostedService
在启动和停止时记录日志,但完成关机需要 10 秒。
以下是为这两个类提供的实现。 NormalHostedService
非常简单:
public
SlowHostedService
几乎完全相同,但它有一个 Task.Delay,需要 10 秒来模拟缓慢关机过程:
public
我在实践中遇到的
IHostedService
只需要 1 秒就能关闭,但我们有很多这样的设备,所以总体效果和上面提到的是一样的!
在这种情况下,服务在 ConfigureServices
中注册的顺序很重要——为了演示这个问题,我们需要先关闭 SlowHostedService
。服务是按相反的顺序关闭的,这意味着我们需要最后注册它:
public void
当我们运行应用程序时,您会像往常一样看到启动日志:
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:\xxx\blog-examples\SlowShutdown\Program.cs:line 16
NormalHostedService.StopAsync()
方法从未被调用。如果服务需要执行一些清理工作,那么您会遇到问题。例如,您可能需要从 Consul 优雅地注销服务,或者从 Kafka 主题取消订阅——现在这些都不会发生。
所以这里发生了什么?那个超时是从哪里来的?
原因:HostOptions.ShutDownTimeout
您可以在运行于应用程序关闭时的框架 Host 实现代码中找到问题。下面是一个简化版本:
internal
这里的要点是设置为在 HostOptions.ShutdownTimeout
之后触发的 CancellationTokenSource
。默认情况下,这会在 5 秒后触发。这意味着托管服务在 5 秒后会被放弃关闭——所有 IHostedService
的关闭必须在超时时间内完成。
public
在 foreach
循环的第一轮迭代中,执行了 SlowHostedService.Stopasync()
,这需要 10 秒才能运行完成。在第二轮迭代中,超出了 5 秒的限时,因此 token.ThrowIfCancellationRequested()
; 抛出了一个 OperationConcelledException
。这退出了控制流程, NormalHostedService.Stopasync()
从未被执行。
这个问题有一个简单的解决办法——增加关闭超时时间!
解决方案:增加关闭超时时间
HostOptions
默认情况下没有在任何地方明确配置,因此您需要在您的 ConfigureSerices
方法中手动配置它。例如,以下配置将超时时间增加到 15 秒:
public void
另外,您也可以从配置中加载超时设置。例如,如果您在 appsettings.json
中添加以下内容:
然后,您可以将 HostOptions
配置节绑定到 HostOptions
对象:
public
这会将序列化的 TimeSpan
值 00:00:15
绑定到 HostOptions
值,并将超时设置为 15 秒。在此配置下,当我们停止应用程序时,所有服务都会正确关闭:
info: 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 秒,以便所有托管服务完成关闭后再退出!
总结
在这篇文章中,我讨论了一个最近的问题,当我们的应用程序关闭时, StopAsync
方法没有在我们的 IHostedService
实现中运行。这是由于一些后台服务对关机信号的响应时间过长,超过了关机超时时间。我用一个服务需要 10 秒关闭的例子演示了这个问题,但实际上,只要所有服务的总关闭时间超过默认的 5 秒,就会发生这种情况。
问题的解决方案是将 HostOptions.ShutdownTimeout
配置值延长至超过 5 秒,使用标准的 ASP.NET Core IOptions<T>
配置系统。
本文转译自:Extending the shutdown timeout setting to ensure graceful IHostedService shutdown。