译者: Akini Xu
原文: Introducing IHostLifetime and untangling the Generic Host startup interactions
作者: Andrew Lock
此文是 探索 ASP.NET Core 3.0 第5篇:
ASP.Net Core 3.0
.csproj文件,Program.cs及通用主机ASP.Net Core 3.0
Startup.cs在不同类型项目中的差异ASP.Net Core 3.0
新特性-Service provider validationASP.Net Core 3.0
应用程序启动时运行异步任务- 介绍IHostLifetime及与通用主机间的作用关系
ASP.Net Core 3.0
新特性-启动时的结构化日志.Net Core 3.0
新特性-本地工具
在本文中,将介绍如何在通用主机上重新构建ASP.NET Core 3.0,以及由此带来的一些好处。 另外还展示了3.0中引入的新的抽象类IHostLifetime
,并介绍它在管理应用程序(尤其是worker services
)生命周期中的作用。
在文章的后半部分,我会详细介绍各个类之间的如何交互,及它们在应用程序启动和关闭期间的作用。 同时也会详细介绍通常不需要我们处理的事情,即使不需要关心,但是理解其原理对于我们也很有必要!
背景 在通用主机上重新构建ASP.NET Core 3.0
ASP.NET Core 3.0的主要功能之一就是整体框架都已基于.NET 通用主机进行了重写。 .NET 通用主机是在ASP.NET Core 2.1中引入的,它是ASP.NET CoreWebHost
的“非Web”版本。通用主机允许您在非Web情况下使用Microsoft.Extensions中的功能,比如依赖注入,配置和日志记录等。
这绝对是一个令人羡慕的功能,但是实际使用中也存在一些问题。通用主机本质上直接复制了ASP.NET Core所需的许多抽象类,但是使用不同的名称空间。例如IHostingEnvironment
,在1.0版本中位于Microsoft.AspNetCore.Hosting,但2.1版本中,又在Microsoft.Extensions.Hosting名称空间下添加了新的IHostingEnvironment
。即使接口内容是相同的,也会导致针对此接口的扩展方法冲突。
在3.0中,ASP.NET Core团队进行较大的重构,直接解决此问题。他们在通用主机的架构上重写了ASP.NET Core主机,来替代里两个独立的主机。 这意味着它可以真正重用相同的抽象,从而解决了上述问题。 此举动机是希望在通用主机之上构建其他非HTTP协议的主机(例如ASP.NET Core 3.0中引入的gRPC功能)。
在ASP.NET Core 3中,对通用主机之上进行“重构”或“重新平台化”的真正意义是什么? 从根本上讲,这意味着Kestrel Web服务器(处理HTTP请求和对中间件管道的调用)只是作为IHostedService
运行。 我已经在博客上写了很多有关创建Hosted Service
的文章,现在应用程序启动时,Kestrel
只是在后台运行的另一个服务而已。
注意:值得强调的一点是,在ASP.NET Core 2.x应用程序中使用的已有
WebHost
和WebHostBuilder
的相关实现在3.0中并没有消失。 它们不再被推荐,也没有被删除,甚至没有被标记为过时。 我希望它们会在下一个主要版本中被标记为过时,因此值得考虑进行切换。
上面简单介绍了背景。 在通用主机中,Kestrel是作为IHostedService
运行的。 另外,ASP.NET Core 3.0中引入的另一个功能是IHostLifetime
接口,该接口允许使用其它托管模型。
Worker services和新的IHostLifetime接口
在ASP.NET Core 3.0引入了“worker services”的概念以及相关的新应用程序模板。Worker services旨在提供可长时间运行的应用程序,可以将它们安装为Windows Service或Systemd service。 这些服务有两个主要功能:
- 通过实现
IHostedService
接口,来实现后台服务应用程序。 - 通过实现
IHostLifetime
接口,来管理后台服务应用程序的生命周期。
第一点中的IHostedService
已经存在了很长时间,并且允许您运行后台服务。 第二点则是有趣的地方,IHostLifetime
接口是.NET Core 3.0的新增功能,它具有两种方法:
public interface IHostLifetime |
我们先总体说明下这2方法,在稍后的部分中,再将详细介绍:
WaitForStartAsync
:在通用主机启动时(starting)被调用,可用于启动侦听关闭事件或延迟应用程序的启动,直到发生某些事件为止。StopAsync
:在通用主机停止时(stopping)被调用。
目前在.NET Core 3.0三个不同IHostLifetime
实现:
ConsoleLifetime
: 侦听SIGTERM
或Ctrl + C
并停止主机应用程序SystemdLifetime
: 监听SIGTERM
并停止主机应用程序,并通知systemd
状态变化(Ready
和Stopping
)WindowsServiceLifetime
: 挂接到Windows服务事件生命周期管理
在 ASP.NET Core 2.x应用程序中,通用主机默认使用ConsoleLifetime
,当应用程序从控制台接收到SIGTERM
信号或Ctrl + C
时,应用程序将停止。如果是创建Worker services(Windows服务或systemd服务)时,则需要配置IHostLifetime
。
理解应用程序启动
当我在研究这个新的抽象时,开始感到非常困惑。它什么时候会被调用? 它与ApplicationLifetime
有什么关系? 谁先调用了IHostLifetime
? 为了使事情更清晰,我花了一些时间,在ASP.NET Core 3.0的默认应用程序中找出他们之间的调用关系。
我们从ASP.NET Core 3.0的Program.cs
文件开始分析,本系列第一篇文章中有写道:
public class Program |
我感兴趣的是当创建了通用主机对象后,run()
的方法到底做了些什么?
请注意,我不会分析所有的代码-我会跳过无关紧要的内容。 我的目标是对调用的过程有一个整体了解。如果您想更深入一点,可以 查看源代码!
Run()
是HostingAbstractionsHostExtensions的扩展方法,它调用了异步RunAsync()
方法,并阻塞直到该方法退出。 当该方法退出时,应用程序也会退出。 下图是RunAsync()
执行的时序图,后面将讨论详细过程:
Program.cs调用Run()
扩展方法,该方法调用RunAsync()
扩展方法。 然后再调用IHost
实例上的StartAsync()
。 StartAsync
方法会完成一些其他工作,例如,启动IHostingServices
(稍后将介绍),该方法在被调用后会快速返回。
接下来,RunAsync()
方法会调用另一个扩展方法WaitForShutdownAsync()
。 此扩展方法的作用与它的方法名是一致的(等待关闭)。 此方法先对其自身进行配置,以使其暂停,直到IHostApplicationLifetime
的ApplicationStopping Token
触发为止(等会我们再讲解如何触发该Token
)。
扩展方法WaitForShutdownAsync()
,使用TaskCompletionSource
,并等待关联的Task
来实现此目的。它看起来很有趣,这并不是我以前使用过的模式,源码如下:(HostingAbstractionsHostExtensions)
public static async Task WaitForShutdownAsync(this IHost host) |
此扩展方法解释了如何“暂停”应用程序的运行状态,而让所有任务都在后台任务中运行。 让我们更深入地了解上图顶部的IHost.StartAsync()
方法调用。 让我们更深入的了解上图中IHost.StartAsync()
方法的调用。
Host.StartAsync()
在上图中,我们研究了在接口IHost
上运行的HostingAbstractionsHostExtensions
扩展方法。 如果我们想知道在调用IHost.StartAsync()
时通常会发生什么,那么我们需要看看具体的实现。 下图显示了通用主机是如何实现的StartAsync()方法:
从上图可以看到,其中还是有很多步骤的! 在Host.StartAsync()
方法中,首先调用了IHostLifetime
实例的上WaitForStartAsync()
方法。 Host.StartAsync()
具体实现取决于您使用的是哪个IHostLifetime
,假定我们正在使用ConsoleLifetime
(ASP.NET Core应用程序的默认设置)。
注意:
SystemdLifetime
的行为与ConsoleLifetime
非常相似,并具有一些额外的功能。WindowsServiceLifetime
则完全不同的,它派生于System.ServiceProcess.ServiceBase
。
ConsoleLifetime.WaitForStartAsync()
方法(如下所示)做了一件重要的事情:它为控制台中的SIGTERM
请求和Ctrl + C
添加了事件侦听器。 当准备关闭应用程序时将触发这些事件。 因此,通常由IHostLifetime
负责控制应用程序何时关闭。
public Task WaitForStartAsync(CancellationToken cancellationToken) |
如上面的代码所示,此方法立即完成,并将控制权返回给Host.StartAsync()
。 此时,主机加载所有IHostedService
实例,并调用每个实例的StartAsync()
方法。还包括用于启动Kestrel Web
服务器的GenericWebHostService
(该Hosted Service
最后启动,上一篇曾提到过的应用程序启动时运行异步任务)。
一旦所有IHostedServices
全部启动,Host.StartAsync()
就会调用IHostApplicationLifetime.NotifyStarted()
方法,来触发所有已绑定的回调方法(通常只是记录)并退出。
请注意,
IHostLifetime
与IHostApplicationLifetime
是不同的。 前者用于控制应用程序何时启动。 后者(由ApplicationLifetime实现)包含CancellationTokens
,用于应用程序各个生命周期的事件回调绑定。
此时,应用程序处于“运行”状态,所有后台服务都在运行中、Kestrel处理请求、扩展方法WaitForShutdownAsync()
等待ApplicationStopping
事件的触发。 最后,我们在控制台中键入Ctrl + C
,看看会发生什么?
shotdown process
当ConsoleLifetime
从控制台接收到SIGTERM
信号或Ctrl + C
时,将引发关闭过程。 下图显示了关闭过程中所有关键参与者之间的相互作用:
-
当触发
Ctrl + C
终止事件时,ConsoleLifetime
会调用IHostApplicationLifetime.StopApplication()
方法。 它将触发所有绑定了ApplicationStopping
取消令牌的回调。 本文开头写的理解应用程序启动,RunAsync()
正在等待着ApplicationStopping
的触发,当await
的任务完成时,调用了Host.StopAsync()
。 -
Host.StopAsync()
方法中第二次调用IHostApplicationLifetime.StopApplication()
方法。 这次调用是个空方法,从技术上讲,这是必需的,因为还有其他方式,导致Host.StopAsync()
被触发(本文是从Ctrl + C
触发)。 -
接下来,主机以相反的顺序关闭所有
IHostedServices
。 最先启动的服务将被最后关闭,因此GenericWebHostedService
第一个被关闭。 -
服务关闭后,将调用
IHostLifetime.StopAsync
,对于ConsoleLifetime
和SystemdLifetime
来说,都是空操作,但对于WindowsServiceLifetime
来说,有其他逻辑要执行的。 最后,Host.StopAsync()
在退出之前,调用IHostApplicationLifetime.NotifyStopped()
以通知其它关联的处理程序。 -
此时,所有的都关闭,
Program.Main
函数退出,应用程序退出。
总结
在这篇文章中,我们了解一些有关如何在通用主机之上重新构建ASP.NET Core 3.0的背景知识,并介绍了新的IHostLifetime
接口。 然后,我详细讲述了,通用主机上的ASP.NET Core 3.0应用程序在启动和关闭时,各个类之间的如何交互及它们的作用。
显然这需要一个漫长的过程, 我个人认为通过查看代码,可以让你理解的更加深刻,希望本文也可以对其他人有所帮助!