我最近发现一个问题,当应用程序关闭时,我们的应用程序没有正确执行在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用于运行后台任务的界面。该接口包含两种方法

  1. public interface IHostedService
  2. {
  3. Task StartAsync(CancellationToken cancellationToken);
  4. Task StopAsync(CancellationToken cancellationToken);
  5. }

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在应用程序关闭时引发的问题:

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

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

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

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

演示问题

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

  • NormalHostedService 在启动和关闭时记录日志,然后立即返回。
  • SlowHostedService 记录启动和停止的时间,但要花10秒才能完成关闭

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

  1. public class NormalHostedService : IHostedService
  2. {
  3. readonly ILogger<NormalHostedService> _logger;
  4. public NormalHostedService(ILogger<NormalHostedService> logger)
  5. {
  6. _logger = logger;
  7. }
  8. public Task StartAsync(CancellationToken cancellationToken)
  9. {
  10. _logger.LogInformation("NormalHostedService started");
  11. return Task.CompletedTask;
  12. }
  13. public Task StopAsync(CancellationToken cancellationToken)
  14. {
  15. _logger.LogInformation("NormalHostedService stopped");
  16. return Task.CompletedTask;
  17. }
  18. }

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

  1. public class SlowHostedService : IHostedService
  2. {
  3. readonly ILogger<SlowHostedService> _logger;
  4. public SlowHostedService(ILogger<SlowHostedService> logger)
  5. {
  6. _logger = logger;
  7. }
  8. public Task StartAsync(CancellationToken cancellationToken)
  9. {
  10. _logger.LogInformation("SlowHostedService started");
  11. return Task.CompletedTask;
  12. }
  13. public async Task StopAsync(CancellationToken cancellationToken)
  14. {
  15. _logger.LogInformation("SlowHostedService stopping...");
  16. await Task.Delay(10_000);
  17. _logger.LogInformation("SlowHostedService stopped");
  18. }
  19. }

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

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddHostedService<NormalHostedService>();
  4. services.AddHostedService<SlowHostedService>();
  5. }

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

  1. info: ExampleApp.NormalHostedService[0]
  2. NormalHostedService started
  3. info: ExampleApp.SlowHostedService[0]
  4. SlowHostedService started
  5. ...
  6. info: Microsoft.Hosting.Lifetime[0]
  7. Application started. Press Ctrl+C to shut down.

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

  1. info: Microsoft.Hosting.Lifetime[0]
  2. Application is shutting down...
  3. info: ExampleApp.SlowHostedService[0]
  4. SlowHostedService stopping...
  5. info: ExampleApp.SlowHostedService[0]
  6. SlowHostedService stopped
  7. Unhandled exception. System.OperationCanceledException: The operation was canceled.
  8. at System.Threading.CancellationToken.ThrowOperationCanceledException()
  9. at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
  10. at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.WaitForShutdownAsync(IHost host, CancellationToken token)
  11. at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
  12. at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
  13. at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
  14. at ExampleApp.Program.Main(String[] args) in C:\repos\andrewlock\blog-examples\SlowShutdown\Program.cs:line 16

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

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

原因:HostOptions.ShutDownTimeout

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

  1. internal class Host: IHost, IAsyncDisposable
  2. {
  3. private readonly HostOptions _options;
  4. private IEnumerable<IHostedService> _hostedServices;
  5. public async Task StopAsync(CancellationToken cancellationToken = default)
  6. {
  7. // Create a cancellation token source that fires after ShutdownTimeout seconds
  8. using (var cts = new CancellationTokenSource(_options.ShutdownTimeout))
  9. using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken))
  10. {
  11. // Create a token, which is cancelled if the timer expires
  12. var token = linkedCts.Token;
  13. // Run StopAsync on each registered hosted service
  14. foreach (var hostedService in _hostedServices.Reverse())
  15. {
  16. // stop calling StopAsync if timer expires
  17. token.ThrowIfCancellationRequested();
  18. try
  19. {
  20. await hostedService.StopAsync(token).ConfigureAwait(false);
  21. }
  22. catch (Exception ex)
  23. {
  24. exceptions.Add(ex);
  25. }
  26. }
  27. }
  28. // .. other stopping code
  29. }
  30. }

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

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

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

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

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

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddHostedService<NormalHostedService>();
  4. services.AddHostedService<SlowShutdownHostedService>();
  5. // Configure the shutdown to 15s
  6. services.Configure<HostOptions>(
  7. opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(15));
  8. }

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

  1. {
  2. "HostOptions": {
  3. "ShutdownTimeout": "00:00:15"
  4. }
  5. // other config
  6. }

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

  1. public class Startup
  2. {
  3. public IConfiguration Configuration { get; }
  4. public Startup(IConfiguration configuration)
  5. {
  6. Configuration = configuration;
  7. }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. services.AddHostedService<NormalHostedService>();
  11. services.AddHostedService<SlowShutdownHostedService>();
  12. // bind the config to host options
  13. services.Configure<HostOptions>(Configuration.GetSection("HostOptions"));
  14. }
  15. }

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

  1. nfo: Microsoft.Hosting.Lifetime[0]
  2. Application is shutting down...
  3. info: SlowShutdown.SlowShutdownHostedService[0]
  4. SlowShutdownHostedService stopping...
  5. info: SlowShutdown.SlowShutdownHostedService[0]
  6. SlowShutdownHostedService stopped
  7. info: SlowShutdown.NormalHostedService[0]
  8. NormalHostedService stopped

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

摘要

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

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

【半译】扩展shutdown超时设置以保证IHostedService正常关闭的更多相关文章

  1. 进程线程协程补充、docker-compose一键部署项目、搭建代理池、requests超时设置、认证设置、异常处理、上传文件

    今日内容概要 补充:进程,线程,协程 docker-compose一键部署演示 搭建代理池 requests超时设置 requests认证设置 requests异常处理 requests上传文件 内容 ...

  2. delphi tidhttp 超时设置无效的解决方法

    现在delphi都发布到xe8了,tidhttp还有缺陷,那就是超时设置在没有网络或者连不上服务器的时候是无效的,不管你设置为多少都要10-20秒.connectTimeout和readTimeout ...

  3. Linux串口中的超时设置

    在Linux下使用串口通信时,默认的阻塞模式是不实用的.而采用select或epoll机制的非阻塞模式,写代码有比较麻烦.幸好Linux的串口自己就带有超时机制. Linux下使用termios.h中 ...

  4. org.apache.http.client.HttpClient; HttpClient 4.3超时设置

    可用的code import org.apache.commons.lang.StringUtils;import org.apache.http.HttpEntity;import org.apac ...

  5. HttpClient 3.X 4.3 4.x超时设置

    HttpClient 4.3.HttpClient这货和Lucene一样,每个版本的API都变化很大,这有点让人头疼.就好比创建一个HttpClient对象吧,每一个版本的都不一样, 3.X是这样的 ...

  6. Apache性能优化、超时设置,linux 重启apache

    在httpd.conf中去掉Include conf/extra/httpd-default.conf前的#以使httpd-default.php生效.其中调节以下参数Timeout 15 (连接超时 ...

  7. libcurl多线程超时设置不安全(转)

    from http://www.cnblogs.com/kex1n/p/4135263.html (1), 超时(timeout) libcurl 是 一个很不错的库,支持http,ftp等很多的协议 ...

  8. CXF超时设置

    转自: http://peak.iteye.com/blog/1285211 http://win.sy.blog.163.com/blog/static/9419718620131014385644 ...

  9. 使用select io复用实现超时设置

    在linux的socket编程中,经常会遇到超时设置的问题,例如请求方如果在Ks内不发送数据则服务器要断开连接停止服务.这里我使用select的io复用实现超时5s设置,具体代码片段如下: fd_se ...

随机推荐

  1. 8种MySQL分页方法总结

    这篇文章主要介绍了8种MySQL分页方法总结,小编现在才知道,MySQL分页竟然有8种实现方法,本文就一一讲解了这些方法,需要的朋友可以参考下 MySQL的分页似乎一直是个问题,有什么优化方法吗?网上 ...

  2. pycharm(py 文件中添加作者、时间)

    1.打开 Pycharm,点击 File,再找到 Settings... 2.选择 Editor ----> File and Code Templates ----> Python Sc ...

  3. <学习笔记之 JQuery>

    1. mouseenter   当鼠标指针进入(穿过)元素时,触发事件 var is_enter_help = false; $("#help-div").mouseenter(f ...

  4. 《Docker从入门到跑路》之简介

    什么是Docker Docker,中文翻译是"码头工人".根据官方的定义,Docker是以Docker容器为资源分割和调度的基本单元,封装了整个软件运行的环境,为开发者和系统管理员 ...

  5. 【Redis】跳跃表原理分析与基本代码实现(java)

    最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist).于是花时间学习了跳表的原理,并用java对其实现. 主要参考以下两本书: <Redis设计与实现>跳表 ...

  6. JS实现显示来访者的停留时间

    显示来访者的停留时间 <script language="javascript"> var ap_name = navigator.appName; var ap_vi ...

  7. Azkaban无法连接网页

    出的问题如下图 首先我查看日志看到有一个 [ERROR] 2020/03/13 11:12:34.417 +0800 ERROR [PluginCheckerAndActionsLoader] [Az ...

  8. Windows命令行:xcopy、move、rename

    Windows命令行,xcopy复制粘贴,move剪切粘贴,rename/ren重命名.当简单事情重复做时,Windows命令行有用武之地了.批命令中,暂时用不到的行,用两个冒号注释掉. 不同路径下, ...

  9. 【漫画】互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock

    ReentrantLock完美实现了互斥,完美解决了并发问题.但是却意外发现它对于读多写少的场景效率实在不行.此时ReentrantReadWriteLock来救场了!一种适用于读多写少场景的锁,可以 ...

  10. TP5 order排序

    order方法属于模型的连贯操作方法之一,用于对操作的结果排序. ->order('sort desc,id desc') 用法如下: Db::table('think_user')->w ...