Azure 应用服务是一种平台即服务 (PaaS),用于托管 Web 应用程序、REST API 和移动后端。 该产品/服务的优势之一是计划内维护在后台执行。 我们的客户可以专注于部署、运行和维护他们的应用程序代码,而不必担心底层基础设施的维护活动。 Azure 应用服务维护是一个可靠的过程,旨在避免或尽量减少托管应用程序的停机时间。 对于托管应用程序的用户来说,此过程基本不可见。 然而,我们的客户经常好奇他们遇到的停机是否是我们的计划内维护导致的,特别是当它们看起来在时间上重合时。
背景
我们的计划内维护机制围绕着缩放单元的体系结构进行,这些缩放单元托管着运行已部署应用程序的服务器。 任何给定的缩放单元都包含几种不同类型的角色,这些角色协同工作。 与我们的计划内维护更新机制最相关的两个角色是“工作器”和“文件服务器”角色。 有关应用服务体系结构的所有不同角色和其他详细信息的更详细说明,请查看在 Azure 应用服务体系结构中
可以采用不同的方法来设计更新策略,这些不同的设计各有优缺点。 我们用于重大更新的策略之一是,这些更新不会在客户当前使用的服务器/角色上运行。 我们的更新流程会以波次形式更新实例,正在进行更新的实例不会被应用程序使用。 应用程序正在使用的实例逐渐被交换出来,并被已更新的实例替换。 对应用程序产生的影响是应用程序要经历启动或重启。 从统计角度和经验观察来看,应用程序重启的破坏性比在应用程序正在使用的服务器上执行维护小得多。
实例更新详细信息
在每个计划内维护周期内,有两种略有不同的场景。 这两种场景与在“工作器”和“文件服务器”角色上执行的更新有关。 从高层次上讲,从最终用户的角度来看,这两个场景看起来很相似,但存在一些重要的差异,有时会导致一些意想不到的行为。
当需要更新“文件服务器”角色时,应用程序使用的存储卷需要从一个文件服务器实例迁移到另一个。 在此更改期间,更新后的“文件服务器”角色将添加到应用程序。 这会导致在该应用服务计划中的所有工作实例上同时重启工作进程。 工作进程重启是重叠的 - 更新机制将首先启动新的工作进程,让它完成启动,然后将新请求发送到新的工作进程。 在新的工作进程正常响应后,现有请求默认情况下有 30 秒时间在旧工作进程中完成,然后旧工作进程将停止。
当更新“工作器”角色时,更新机制同样会换入一个新的更新后的“工作器”角色。 工作器如下所述进行交换:将更新后的工作器添加到 ASP 中,在新工作器上启动应用程序,我们的基础结构等待应用程序启动,新请求被发送到新的工作器实例,允许旧实例上的请求完成,然后从 ASP 中移除旧的工作器实例。 此序列通常针对 ASP 中的每个工作器实例发生一次,并根据计划和缩放单元的大小在几分钟或几小时内展开。
这两种场景之间的主要区别是:
- “文件服务器”角色更改会导致在所有实例上同时重启重叠的工作进程,而“工作器”更改会导致在单个实例上启动应用程序。
- “文件服务器”角色更改意味着应用程序在它之前运行的同一实例上重启,而“工作器”更改则导致应用程序在启动后在不同的实例上运行。
重叠的重启机制使大多数应用程序的停机时间为零,用户甚至不会注意到计划内维护。 如果应用程序需要一些时间进行启动,那么在进程启动期间或之后不久,应用程序可能会经历一段与应用程序缓慢或故障相关的最小停机时间。 我们的平台会不断尝试启动应用程序,直到成功,但如果应用程序完全无法启动,可能会出现更长的停机时间。 停机会一直持续到采取某个纠正措施,例如在该实例上手动重启应用程序。
意外故障处理
虽然本文侧重于计划内维护活动,但值得一提的是,当平台从意外故障中恢复时,也会发生类似的行为。 如果发生影响“工作器”角色的意外硬件故障,平台同样会用新的工作器替换该硬件。 应用程序将在此新的“工作器”角色上启动。 当故障或延迟影响到与应用程序关联的“文件服务器”角色时,一个新的“文件服务器”角色会替换它。 所有“工作器”角色都会重启工作进程。 在评估提高应用程序正常运行时间的策略时,考虑这一事实非常重要。
提高正常运行时间的策略
我们的大多数托管应用程序在计划内维护期间只会经历有限的停机时间或不会停机。 然而,如果你的特定应用程序具有更复杂的启动行为,因而在重启时容易停机,则此事实没有帮助。 如果应用程序每次重启时都会经历停机,那么处理停机问题更加紧迫。 我们的“应用服务”产品中提供了设计用于减少这些场景中的停机时间的多项功能。 从广义上讲,可以采用两类策略:
- 提高应用程序启动一致性
- 尽量减少应用程序重启
从统计数据来看,提高应用程序启动速度并确保其持续成功具有更高的成功率。 我们建议首先查看此领域中可用的选项。 其中一些相当容易实施,可以带来很大的改进。 启动一致性策略利用“应用服务”功能和与应用程序代码或配置相关的技术。 尽量减少重启是一组选项,如果我们不能足够程序地提高应用程序启动一致性,可以使用这些选项。 这些选项通常更昂贵,可靠性更低,因为它们通常可以防止一部分重启。 无法避免所有重启。 同时使用这两种策略非常有效。
用于启动一致性的策略
应用程序初始化 (AppInit)
当应用程序在 Windows“工作器”角色上启动时,Azure 应用服务基础结构会在外部请求路由到此工作器之前尝试确定应用程序何时准备好为请求提供服务。 默认情况下,对应用程序的根 (/) 的成功请求表示应用程序已准备好为请求提供服务。 对于某些应用程序,此默认行为不足以确保应用程序已完全预热。 通常,如果应用程序的根具有有限的依赖项,但其他路径依赖于更多的库或外部依赖项才能工作,则会发生这种情况。 IIS 应用程序初始化模块适用于微调预热行为。 概括而言,它允许应用程序所有者定义哪个或哪些路径可以作为指示器来指示应用程序是否已确实准备好为请求提供服务。 有关如何实现此机制的详细讨论,请查看以下文章:应用服务预热揭秘。 如果正确实施,即使应用程序启动更复杂,此功能也可以实现零停机。
Linux 应用程序可以通过使用 WEBSITE_WARMUP_PATH 应用程序设置来利用类似的机制。
运行状况检查
运行状况检查功能设计用于处理正常应用程序执行过程中出现的意外代码和平台故障,但也有助于增强启动弹性。 运行状况检查执行两个不同的修复功能 - 从负载均衡器中移除失败的实例,以及替换整个实例。 我们可以通过从负载均衡器中移除实例来处理间歇性启动故障。 如果尽管采用了所有其他策略,但某个实例在启动后仍然返回故障,则运行状况检查可以从负载均衡器中移除该实例,直到该实例再次对运行状况检查请求返回状态代码 200。 因此,此功能充当了一个故障保护机制,以尽量减少启动后发生的任何停机。 如果启动后故障是暂时性的,并且不需要重启进程,则此功能非常有用。
自动修复
Windows 和 Linux 的自动修复是另一个专为应用程序正常执行而设计的功能,但也可用于改善启动行为。 如果我们知道应用程序在启动后有时会进入不可恢复的状态,那么运行状况检查就不合适了。 不过,自动修复可以自动重启工作进程,这在这种情况下可能很有用。 我们可以配置一个自动修复规则,用以监视失败的请求,并在单个实例上触发进程重启。
应用程序启动测试
对应用程序的启动进行彻底测试可能会被忽略。 结合其他因素(例如依赖项故障、库加载故障、网络问题,等等)进行启动测试 面临更大的挑战。 相对较小的启动故障率可能会被忽视,但当每个更新周期都有多个实例重启时,可能会导致较高的故障率。 如果某个计划包含 20 个实例和一个启动故障率为 5% 的应用程序,则会导致每个更新周期平均有三个实例无法启动。 每个实例通常有三次应用程序重启(每个实例 20 次实例移动和 2 次与文件服务器相关的重启)。
我们建议测试多个场景
- 常规启动测试(一次一个实例),用以确定单个实例的启动成功率。 在转向其他更复杂的场景之前,这个最简单的场景应该接近 100%。
- 模拟启动依赖项故障。 如果应用依赖于其他 Azure 或非 Azure 服务,请模拟这些依赖项中的停机时间,以了解在这些条件下的应用程序行为。
- 许多实例的同时启动 - 最好比生产环境中的实例多。 使用许多实例进行测试通常会发现依赖项故障,这些依赖项通常仅在启动期间使用,例如密钥保管库引用、应用程序配置、数据库,等等。应测试这些依赖项,以检测同时重启许多实例时产生的突发请求量。
- 在满载状态下添加实例 - 确保 AppInit 正确配置,并且在向新实例发送请求之前,应用程序可以完全初始化。 手动横向扩展是在维护期间复制实例移动的一种简单方法。
- 重叠的工作进程重启 - 再次测试 AppInit 是否已正确配置,以及在旧工作进程完成和新工作进程启动时请求是否可以成功完成。 在负载下更改环境变量可以模拟“文件服务器”更改带来的影响。
- 计划中的多个应用 - 如果同一计划中有多个应用,请在所有应用中同时执行所有这些测试。
启动日志记录
在生产环境中追溯性地排除启动故障的能力是一个与使用测试来提高启动一致性不同的考虑因素。 不过,这同样甚至更为重要,因为尽管我们付出所有努力,也可能无法在测试或质量保证环境中模拟所有类型的实际故障。 这通常也是日志记录的最薄弱环节,因为初始化日志记录基础设施是必须执行的另一项启动活动。 因此,对应用程序进行初始化的操作的顺序是一个重要的考虑因素,并且可能会成为一个先有鸡还是先有蛋的问题。 例如,如果我们需要配置基于密钥保管库引用的日志记录,但我们无法获取密钥保管库值,我们如何记录此故障? 我们可能需要考虑使用不依赖于任何其他外部因素的单独日志机制来复制启动日志记录。 例如,将这些类型的启动故障记录到本地磁盘。 简单地启用一般日志记录功能(例如 .NET Core stdout 日志记录)可能会适得其反,因为即使在启动后,这种日志记录也会不断生成日志数据,并且随着时间的推移,这可能会填满磁盘。 可以战略性地使用此功能来排除可重现的启动故障。
用于尽量减少重启的策略
以下策略可以显著减少应用程序在计划内维护期间经历的重启次数。 本部分中的一些策略还可以进一步控制这些重启何时发生。 一般来说,这些策略虽然有效,但不能完全避免重启。 主要原因是,一些重启是由于意外故障而不是计划内维护造成的。
重要
无法完全避免重启。 以下策略可帮助减少重启次数。
本地缓存
本地缓存功能可用于提高发生外部存储故障时的复原能力。 概括而言,它在运行它的实例的本地磁盘上创建应用程序内容的副本。 这将应用程序与意外的存储故障隔离开来,但也可以防止因文件服务器更改而重启。 利用此功能可以大幅减少公共维护期间的重启次数 - 通常它可以消除大约三分之二的重启。 由于它主要避免工作进程同时重启,因此我们看到的更大改善可能是应用程序启动一致性。 本地缓存确实会影响应用程序的设计并改变其行为,因此对应用程序进行全面测试以确保应用程序与此功能兼容非常重要。
计划内维护通知和配对区域
如果想要降低生产环境中与更新相关的重启风险,我们可以利用计划内维护通知来了解何时将更新任何给定的应用程序。 然后,我们可以在某个配对区域设置应用程序的副本,并在主副本维护期间将流量路由到辅助应用程序副本。 此选项可能代价高昂,因为此维护的时段相当宽,因此辅助应用程序副本需要在足够的实例上运行至少几天。 如果我们已经为常规复原设置了辅助应用程序,则此选项的成本可能会较低。 此选项可以减少重启次数,但与此类别中的其他选项一样,无法消除所有重启。