原文:Running async tasks on app startup in ASP.NET Core (Part 3)
作者:Andrew Lock
译者:Lamond Lu
之前我写了两篇有关在ASP.NET Core中运行异步任务的博文,本篇博文是对之前两篇博文中演示示例和实现方法的简短跟进。
你可以通过以下链接查看之前的博文。
启动任务的例子
在之前博客中,我收到的最常见的反馈是关于我在描述问题时使用的例子。在我最初的博客中,我列举了3种可能场景,在这3种场景中,你希望在ASP.NET Core应用启动时运行一些异步任务。
- 检查强类型配置是否合法
- 使用数据库或者API填充缓存
- 运行数据库迁移
对于前两种场景,没有任何问题,但是对于数据库迁移,一些博友提出了一些疑问。其实在两篇博文中我一直都反复说明,数据库迁移作为启动任务不是一个很好的方案,这里我只是想用它作为一个说明如何在ASP.NET Core程序启动时运行异步任务的例子。现在来看,当时使用这个例子是非常失败的。
数据库迁移是一个糟糕的选择
那么为什么在ASP.NET Core应用启动时,运行数据库迁移任务会是一个问题呢?毕竟,在应用程序开始处理请求之前,你肯定要完成数据库迁移!
其实这里其实有3个问题:
- 数据库迁移是应该是单线程的
- 迁移数据库需要更多的权限
- 开发人员不太喜欢直接运行数据库迁移
下面我们依次说明一下。
数据库迁移应该是单线程的
扩展一个Web应用最常用的方式之一是进行横向扩展,启动多个运行实例并使用负载均衡分发请求
这种Web集群扩展的方案是非常有效的,特别是当当前应用是无状态的(请求被分发到各个应用程序中,如果一个应用程序崩溃,其他的Web应用实例依然可以处理请求)。
但是不幸的是,如果尝试将数据库迁移作为应用启动任务,你很可能就会遇到问题。如果有超过1个以上的实例同时启动,多个数据库迁移任务将同时运行。
虽然并不能保证你一定会遇到麻烦,但除非你非常小心地确保幂等更新和错误处理,否则你很可能会陷入困境。
你肯定不希望使用这种方法,因为它可能产生的数据库完整性问题。 这里一个更好的选择是先启动单个实例完成迁移操作。 这样数据库迁移任务变成一个单线程任务,自然也就避开最严重的危险。
这种方法比将数据库迁移作为启动任务运行更有意义,但它更安全,更容易实现。
当然,这不是唯一的选择。 如果你对启动任务迁移的想法有所了解,那么你可以使用分布式锁来确保只有一个应用程序实例来运行迁移脚本。 然而,这并没有解决第二个问题…
迁移通常需要更多的权限
通常来说,最佳实践是你必须限制你的应用程序,以便他们只有权访问和修改所需的资源。 如果报表应用只需要读取销售数据,那么它应该无法修改它们,或者更改数据库表结构! 为指定的连接字符串配置可操作的权限,可以防止在的的应用出现安全问题时产生大量影响。
如果你正在使用Web应用程序本身来运行数据库迁移,那么该Web应用程序自然需要数据库权限才能执行高风险活动,例如修改数据库表结构,更改权限或更新/删除数据。 你真的希望您的Web应用程序能够删除你的学生表吗?
同样,你可以使用一些特定的实现,例如,与正常的数据库访问相比,使用不同的连接字符串进行迁移。 但是,如果使用外部迁移过程,你根本无法锁定Web应用程序进程。
开发人员不习惯直接运行EF Core迁移
这是一个不那么明显的观点,但是很多人表示在生产环境中使用EF Core迁移工具可能不是一个好主意。
就个人而言,我已经有1年多没有在生产环境中使用EF Core迁移了,到目前为止迁移工具肯定已经有所改善。 话虽如此,我仍然看到一些问题:
- 使用EF Core全局工具进行迁移需要安装.NET Core SDK,这在生产服务器上是不需要的。
- 如果你想安全地更新数据库,你可能还是必须对生成的脚本进行一些编辑。 迁移后的数据库结构应与现有(运行)应用程序兼容,以避免停机。
- 微软官方文档中暗示在应用启动时运行EF Core迁移不是一个好主意!
就我自己而言,我经常使用DbUp和FluentMigrator,而不会使用EF Core迁移。我发现它们都运行良好。
因此,如果数据库迁移任务不适合应用启动任务示例,那么哪些任务才是比较适合的呢?
比较适合作为启动任务的一些例子
虽然在之前的博文中我已经反复提到了一些例子,但我还是将在下面再次描述它们。这里其他博友还给我一些有趣的补充。
- 检查强类型配置是否有效。ASP.NET Core 2.2引入了配置验证,但它只在首次访问
IOptions
类时执行此操作。 正如我在之前文章中所描述的那样,你可能希望在应用启动时进行验证,以确保你的环境和配置有效。 - 填充缓存。 你的应用程序可能需要来自文件系统或远程服务的数据,它只需要加载一次,但加载需要耗费相当多的资源,所以在应用程序启动之前加载此数据可减少请求延迟。
- 预连接到数据库和/或外部服务。 以类似的方式,你可以通过连接到数据库或其他外部服务来填充数据库连接池。 这些通常是相对昂贵的操作,因此是很好的用例。
- 预编译加载应用中所有的单例服务。我认为这个一个非常有趣的想法,通过预加载依赖注入容器中注册的单例服务,你可以减少请求的响应时间。
使用健康检查来完成启动任务
我完全同意有关数据库迁移的反馈。 当这不是一个好主意时,将它们用作启动任务的示例有点误导,特别是因为我个人不使用我所描述的方法!
然而,很多人都同意我所描述的另外一种方法 - 在启动Kestrel服务器和处理请求之前运行启动任务。
这里Damian Hickey针对这个方案提出了一个问题,他建议尽快启动Kestrel服务器。 他建议在所有启动任务完成后,使用运行健康检查向负载均衡器发出信号,表明应用程序已准备好开始接收请求。 与此同时,所有非健康检查流量(如果负载均衡器正在执行此任务,则不应该有任何流量)将收到503服务不可用。
这种方法的主要好处是它可以避免网络超时。 一般来说,应用程序最好能尽快的针对请求返回错误代码,而不是根本不响应请求,并导致客户端超时。 这减少了客户端所需的资源数量。 通过较早启动Kestrel服务器,应用程序可以更早地开始响应请求,即使响应是“未就绪”响应。
这实际上与我在第一篇文章中描述的方法非常相似,但是我没有选用它了,因为它太复杂了,而且没有达到我设定的目标。 从技术上来说,在那篇文章中,我只是关注在Kestrel服务器启动之前运行任务的方法,健康检查方法不能完成这个功能。
然而,Damian提出的问题让我再次思考。 在我下一篇博客中,我将描述如何使用ASP.NET Core 2.2的健康检查功能向负载均衡器发送信号,表明应用程序已经完成了所有启动任务。
总结
在这篇文章中,我分享了我之前关于在ASP.NET Core应用程序启动时运行异步任务的一些反馈。 这里最大的问题是我选择使用EF Core数据库迁移作为启动任务的示例。 数据库迁移不适合在应用程序启动时运行,因为它们通常需要由单个进程运行,并且需要比更多的数据库权限。
我提供了一些比较适合作为的启动任务的场景,并且描述了Damian给出的建议 - 尽快启动Kestrel服务器,并使用运行状况检查来指示任务何时完成。 我将在下一篇文章中描述如何实现这一功能。