你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
本指南介绍在云应用中处理暂时性故障的建议。 与远程服务和资源通信的所有应用程序必须对暂时性故障敏感。 对于云中运行的应用程序尤其如此,因为其环境的性质与通过 Internet 建立连接的特点,更容易遇到这种类型的故障。 暂时性故障包括组件和服务瞬间断开网络连接、服务暂时不可用,以及当服务繁忙时出现超时。 这些故障通常可自我纠正,因此,如果在适当的延迟后重复操作,则操作可能会成功。
本文提供有关暂时性故障处理的一般指南。 有关在应用程序代码中实现重试以处理暂时性故障的信息,请参阅 重试模式 ,以及使用 Azure 服务时,请参阅 Azure 服务的重试指南。
暂时性故障
任何环境、任何平台或操作系统以及任何类型的应用程序都会发生暂时性故障。 对于在本地基础结构上运行的解决方案,应用程序及其组件的性能和可用性通常通过昂贵且通常未使用的硬件冗余进行维护,组件和资源彼此靠近。 此方法使故障的可能性较低,但暂时性故障仍可能发生,因为外部电源或网络问题或灾难方案等不可预见的事件可能导致中断。
云托管(包括私有云系统)可以使用共享资源、冗余、自动故障转移和跨许多商品计算节点的动态资源分配来提供更高的整体可用性。 但是,由于云环境的性质,暂时性故障更有可能发生。 有以下几个原因:
在云环境中,许多资源是共享的,并且为了保护这些资源,访问会受到速率限制。 某些服务在负载提升到特定级别或达到最大吞吐量速率时拒绝连接,以允许处理现有请求并维护所有用户的服务性能。 限流有助于维护使用共享资源的邻居和其他租户的服务质量。
云环境使用大量商品硬件单元。 它们通过跨多个计算单元和基础结构组件动态分配负载来提供性能。 它们通过自动回收或更换失败的单位来提供可靠性。 由于这种动态性质,暂时性故障和临时连接故障可能会偶尔发生。
应用程序与它使用的资源和服务之间通常有更多的硬件组件,包括网络基础结构(如路由器和负载均衡器)。 这种额外的基础结构偶尔可能会引入额外的连接延迟和暂时性连接故障。
客户端和服务器之间的网络条件可能可变,尤其是在通信通过 Internet 时。 即使在本地位置,大量流量负载也会降低通信速度,并导致间歇性连接故障。
暂时性故障可能会对应用程序的感知可用性产生重大影响,即使它在所有可预见的情况下都经过了全面测试。 若要确保云托管的应用程序能够可靠地运行,需要确保它们能够应对以下挑战:
应用程序必须能够检测到故障的发生,并确定这些故障可能是暂时性的、持久性的还是终端故障。 发生故障时,不同的资源可能返回不同的响应,这些响应可能会根据操作上下文而有所不同。 例如,当应用程序从存储读取时出错的响应可能与写入存储时出错的响应不同。 许多资源和服务都有详细记录的瞬时故障协议。 但是,当这些信息不可用时,很难发现故障的性质以及它是否可能是暂时性的。
如果确定故障可能是暂时性的,应用程序必须能够重试操作。 它还需要跟踪重试作的次数。
应用程序必须使用适当的重试策略。 此策略指定应用程序应该重试的次数、每次尝试之间的延迟时间,以及尝试失败后执行的操作。 适当的尝试次数以及每次尝试之间的延迟时间通常难以确定。 策略因资源类型以及资源和应用程序的当前操作条件而异。
以下准则有助于为应用程序设计合适的暂时性故障处理机制。
设置重试机制
确定是否存在内置的重试机制
许多服务提供包含暂时性故障处理机制的 SDK 或客户端库。 服务使用的重试策略通常是根据目标服务的性质和要求定制的。 或者,对于确定重试是否适当,以及在下一次尝试重试之前要等待多长时间方面,服务的 REST 接口可能会返回有用的信息。
除非有具体且明确的要求需要其他更适当的重试行为,否则应适时使用提供的内置重试机制。
确定操作是否适合重试
只在故障是暂时性(通常可由故障的性质来判断),以及在重试时操作至少有一定成功的可能性时,才执行重试操作。 重试那些尝试进行无效操作的操作没有意义,例如数据库更新不存在的项,或者对遭遇致命错误的服务或资源的请求。
通常,仅当可以确定执行此作的完整效果以及条件得到充分理解且可验证时,才实施重试。 否则,让调用代码实现重试。 请记住,从无法控制的资源与服务返回的错误可能会随着时间而演进,可能需要重新访问暂时性故障检测逻辑。
创建服务或组件时,请考虑实现错误代码和消息,帮助客户端确定是否应重试失败的作。 具体而言,指示客户端是否应重试作(可能通过返回 isTransient 值)并建议在下一次重试尝试之前进行适当的延迟。 如果要构建 Web 服务,请考虑返回服务合约中定义的自定义错误。 尽管通用客户端可能无法读取这些错误,但它们在创建自定义客户端时很有用。
确定适当的重试计数与间隔
优化用例类型的重试计数和间隔。 如果未重试足够的次数,应用程序可能无法完成该操作,并且可能会失败。 如果重试次数过多,或者尝试间隔时间过短,应用程序可能会长时间保留线程、连接和内存等资源,这会对应用程序的运行状况产生不利影响。
根据操作类型调整时间间隔与重试次数值。 例如,如果操作是用户交互的一部分,则间隔应该较短,且只需重试几次。 使用此方法,可以避免让用户等待响应,该响应保留打开的连接,并减少其他用户的可用性。 如果操作是长时间运行或关键工作流的一部分,其中取消和重启进程成本高昂或耗时,则最好在尝试之间等待更长时间,重试次数更多。
请注意,确定适当的重试间隔是设计一个成功的策略时最困难的部分。 典型的策略会使用以下类型的重试间隔:
指数退避。 应用程序在第一次重试之前短暂地等待,然后每个后续重试的间隔时间会呈指数增加。 例如,在 3 秒、12 秒、30 秒后重试操作。 若要进一步改进此策略,可以将抖动添加到指数退避。 抖动会为每个重试尝试引入随机延迟,这有助于防止多个客户端同时重试并导致负载高峰
增量间隔。 应用程序在第一次重试之前等待一小段时间,然后增量增加每次后续重试之间的时间。 例如,它可以在 3 秒、7 秒、13 秒等后重试作。
定期间隔。 应用程序每次尝试的间隔时间相同。 例如,它可能会每隔 3 秒重试一次作。
立即重试。 有时暂时性故障是短暂的,可能是由网络数据包冲突或硬件组件峰值等事件引起的。 在这种情况下,立即重试操作是合适的,因为如果在应用程序组装并发送下一个请求时故障已经清除,则操作可能会成功。 不过,立即重试次数不得超过一次。 如果立即重试未成功,应考虑采用其他策略,例如指数退避或回退操作。
随机化。 前面列出的任何重试策略都可以包括随机化,以防止客户端的多个实例同时发送后续重试尝试。 例如,一个实例可能会在 3 秒、11 秒、28 秒等后重试该作,而另一个实例可能会在 4 秒、12 秒、26 秒后重试该作。 随机化是一种有用的技术,可以与其他策略结合使用。
作为一般准则,对后台操作使用指数退避和抖动策略,并针对交互式操作使用即时重试或定期间隔重试策略。 在上述两种情况下,应该选择延迟与重试计数,使所有重试的延迟上限都在所需的端到端延迟要求范围内。
考虑所有因素的组合,这些因素导致重试操作的总的最大超时时间。 这些因素包括失败的连接生成响应所需的时间(通常由客户端中的超时值设置)、重试尝试之间的延迟和最大重试次数。 所有这些时间的总和会导致整体操作时间变得漫长,尤其是在使用指数退让策略时,因为每次故障后重试的间隔时间会快速增加。 如果进程必须满足特定的服务级别协议 (SLA),则总体操作时间(包括所有超时和延迟)必须在 SLA 中定义的限制范围内。
不要实施过于激进的重试策略。 这些策略的间隔过短或重试过于频繁, 可能会对目标资源或服务产生不利影响。 这些策略可能会造成资源或服务无法从过载状态恢复,并且会继续阻止或拒绝请求。 这种情况会导致恶性循环,越来越多的请求将发送到资源或服务。 因此造成其恢复能力进一步降低。
在选择重试间隔时,考虑操作的超时时间,以避免立即发起后续尝试(例如,如果超时时段与重试间隔相似)。 此外,请考虑是否需要将可能的总时间段(超时加上重试间隔)保持在特定总时间以下。 如果一个操作的超时时间异常短或长,这个超时可能会影响等待时间以及重试该操作的频率。
使用异常的类型及其包含的任何数据,或者从服务返回的错误代码和消息,以优化重试次数及其之间的间隔。 例如,某些异常或错误代码(如 HTTP 代码 503 - 服务不可用,以及响应中的 Retry-After 标头)会指示错误可能持续的时间,或服务失败且不会响应任何后续尝试。
请考虑使用死信队列方法来确保传入调用中的所有信息在所有重试尝试都用尽后不会丢失。
避免反模式
在大多数情况下,避免使用包含重复重试代码层的实现。 避免在设计中包含级联重试机制或在涉及请求层次结构的操作的每个阶段进行重试,除非有特定要求必须这样做。 在这些例外的情况下,请使用策略避免过多的重试次数和延迟期间过长,并确保了解后果。 例如,假设某个组件对另一个组件发出请求,后者再访问目标服务。 如果要对这两个调用各实施重试三次,则总共会对该服务重试九次。 许多服务和资源实施内置重试机制。 如果需要在更高级别实施重试,应研究如何禁用或修改这些机制。
切勿实施永不结束的重试机制。 这样做可能会导致资源或服务无法从过载情况下恢复,并造成限制与遭到拒绝的连接持续更长时间。 使用有限次数的重试,或实现类似于 断路器 的模式,以确保服务能够恢复。
切勿多次执行立即重试。
在访问 Azure 上的服务和资源时,请避免使用定期重试间隔,尤其是在重试尝试次数较多时。 此方案中的最佳方法是具有断路器功能的指数退避策略。
防止同一客户端的多个实例或不同客户端的多个实例同时发送重试。 如果这种情况可能发生,请在重试间隔中引入随机化。
测试重试策略及其实现
在尽可能广泛的条件下全面测试重试策略,尤其是当使用的应用程序与目标资源或服务要承受极端负载时。 要检查测试期间的行为,可以:
通过在非生产环境和生产环境中故意引入暂时性故障,将其纳入到 混沌工程和故障注入 实践中。 例如,发送无效请求或添加代码用于检测包含不同错误类型的测试请求与响应。
创建资源或服务模型,用于返回真实服务可能返回的错误范围。 覆盖重试策略旨在检测的所有错误类型。
对于创建和部署的自定义服务,通过暂时禁用或重载该服务来强制发生暂时性错误。 (不应尝试使 Azure 中的任何共享资源或共享服务重载。)
使用拦截和修改网络流量的库或解决方案来模拟自动化测试中的不利情境。 例如,测试可以添加额外的往返时间、删除数据包、修改标头,甚至更改请求本身的正文。 这样做可以确定性地测试一部分故障条件,用于暂时性故障和其他类型的故障。
执行高负载因子和并发测试,以确保重试机制和策略在这些条件下正常工作。 这些测试还有助于确保重试不会对客户端的操作产生不利影响,也不会在请求之间造成交叉污染。
管理重试策略配置
重试策略是重试策略所有元素的组合。 它定义了检测机制,该机制确定故障是否可能是暂时性的、要使用的间隔类型(如常规、指数退避和随机化)、实际间隔值以及重试次数。
在许多位置(即使在最简单的应用程序中)以及更复杂的应用程序的每一层中实现重试。 请考虑使用中心点来存储所有策略,而不是对多个位置的每个策略的元素进行硬编码。 例如,在应用程序配置文件中存储间隔和重试计数等值,在运行时读取它们,然后以编程方式生成重试策略。 这样做可以更轻松地管理设置和修改和微调值,以响应不断变化的要求和方案。 但是,设计系统来存储值,而不是每次重新读取配置文件,并在无法从配置中获取值时使用适当的默认值。
在应用程序的配置系统中存储用于在运行时生成重试策略的值,以便无需重启应用程序即可对其进行更改。
利用你使用的客户端 API 中提供的内置或默认重试策略,但前提是它们适合你的方案。 这些策略通常是通用的。 在某些状况下,这些策略可能都是必需的,但在其他状况下,它们可能无法提供完整的选项范围来满足特定的要求。 要确定最合适的值,需要执行测试来了解设置如何影响应用程序。
记录和跟踪暂时性与非暂时性故障
在重试策略中包含异常处理,以及其他用于记录重试尝试的检测。 偶尔会出现暂时性故障和重试,这并不表示存在问题。 但是,固定递增的重试次数通常表示出现了可能会造成故障或降低应用程序性能与可用性的问题。
将暂时性故障记录为警告项,而不是错误项,以便监视系统不会将其检测为应用程序错误而触发误报。
考虑将值存储在日志项中,用于指示重试是由服务中的限制所造成,还是由其他类型的故障(例如连接失败)造成,使你能够在分析数据期间进行区分。 节流错误数量的增加通常表明应用程序存在设计缺陷,或者需要切换到提供专用硬件的高级服务。
考虑测量和记录包含重试机制的操作总耗时。 此指标是暂时性故障对用户响应时间、进程延迟和应用程序用例效率的总体影响的良好指标。 此外,请记录发生的重试次数,以便了解导致响应时间的因素。
考虑实施遥测和监视系统,当故障次数与比率、重试平均次数或操作成功所需的整体时间增加时,该系统会引发警报。
管理不断失败的操作
请考虑如何处理在每次尝试时继续失败的操作。 这种情况是不可避免的。
尽管重试策略定义了应重试作的最大次数,但它不会阻止应用程序使用相同数量的重试再次重复该作。 例如,如果订单处理服务因永久失效的致命错误而失败,重试策略可能会将检测到的连接超时视为暂时性故障。 代码对操作重试指定次数,然后放弃。 但是,当另一个客户下订单时,即使在每次都会失败的情况下,仍会再次尝试该操作。
若要防止对反复失败的操作进行持续重试,应考虑实现 断路器模式。 使用此模式时,如果指定时间范围内失败次数超过阈值,则请求会立即作为错误返回到调用方,并且不会尝试访问失败的资源或服务。
应用程序将定期测试服务,并间歇性地(请求之间的间隔非常长)检测服务何时可供使用。 适当的间隔取决于操作的重要性和服务的性质等因素。 它可能是几分钟到几小时之间之间的任何时间。 测试成功时,应用程序将恢复正常操作,并将请求传递给刚刚恢复的服务。
同时,你可能能够切换回其他服务实例(可能位于不同的数据中心或应用程序中),使用提供兼容(可能更简单)功能的类似服务,或者根据服务可能很快恢复可用,执行一些替代操作。 例如,有时适合将服务的请求存储在队列或数据存储中,供以后重试。 或者,可以将用户重定向到应用程序的备用实例,降低应用程序的性能,但仍提供可接受的功能,或者只向用户返回一条消息以指示应用程序当前不可用。
优化重试实现
确定重试次数的值和策略的重试间隔时,请考虑服务或资源的操作是否是长时间运行或多步骤操作的一部分。 补偿一个步骤失败时已成功的所有其他操作步骤可能很困难或成本高昂。 在这种情况下,只要该策略不会因持有或锁定稀缺资源而阻碍其他操作,使用很长的间隔和大量的重试是可以接受的。
考虑重试相同的操作是否会导致数据不一致。 如果多步骤过程中的某些部分是重复的,并且操作不是幂等的,则可能会出现不一致情况。 例如,如果一个递增值的操作被重复执行,则会生成无效的结果。 如果使用者无法检测到重复的消息,重复向队列发送消息的操作可能会导致消息消费者不一致。 为了防止这些场景,设计每个步骤为一个幂等操作。 有关详细信息,请参阅 幂等模式。
考虑重试操作的范围。 例如,可以更轻松地在包含数个操作的级别实施重试代码,如果其中一个操作失败,则重试所有操作。 但是,这可能会导致幂等性问题或不必要的回滚操作。
如果选择包含多个操作的重试范围,在确定重试间隔、监控操作的运行时间以及发出故障警报之前,请考虑所有这些操作的总延迟。
考虑重试策略如何影响共享应用程序中的邻居和其他租户,以及使用共享资源和服务时。 主动重试策略可能会导致这些其他用户和共享资源和服务的应用程序出现越来越多的暂时性故障。 同样,应用程序可能会受到资源和服务其他用户实施的重试策略的影响。 对于业务关键型应用程序,你可能想要使用未共享的高级服务。 这样做可以让你更好地控制这些资源和服务的负载以及随之而来的速率限制,从而帮助你证明额外成本的合理性。
注释
有关权衡和风险的进一步指导,请参阅重试模式文章中的 问题和注意事项 。
Azure 便利化
大多数 Azure 服务和客户端 SDK 都提供重试机制。 但是,这些机制不同,因为每个服务都有不同的特征和要求,并且每个重试机制都优化到特定的服务。 本部分总结了一些常用 Azure 服务的重试机制功能。
| 服务 | 重试功能 | 策略配置 | Scope | 遥测功能 |
|---|---|---|---|---|
| Microsoft Entra ID | Microsoft 身份验证库(MSAL)中的本机支持 | 嵌入到 MSAL 库中 | 内部 | None |
| Azure Cosmos DB | 服务中的原生 | 不可配置 | 全球 | TraceSource |
| Azure Data Lake Storage | 客户端中的本地 | 不可配置 | 独立操作 | None |
| Azure 事件中心 | 客户端中的原生 | Programmatic | 客户 | None |
| Azure IoT 中心 | 客户端 SDK 中的原生 | Programmatic | 客户 | None |
| 用于 Redis 的 Azure 缓存 | 在客户端本地运行 | Programmatic | 客户 | TextWriter |
| Azure 认知搜索 | 客户端中的原生 | Programmatic | 客户 | ETW 或自定义 |
| Azure 服务总线 | 客户端中的原生组件 | Programmatic | NamespaceManager、MessagingFactory 和客户端 | ETW |
| Azure Service Fabric | 客户端中的本机 | Programmatic | 客户 | None |
| 具有 ADO.NET 的 Azure SQL 数据库 | Polly | 声明性和编程性 | 单个语句或代码块 | Custom |
| 使用 Entity Framework 的 SQL 数据库 | 客户端内的原生功能 | Programmatic | 每个 AppDomain 的全局 | None |
| 具有 Entity Framework Core 的 SQL 数据库 | 客户端中的原生 | Programmatic | 每个 AppDomain 的全局设置 | None |
| Azure 存储 | 客户端中的原生 | Programmatic | 客户端和个人操作 | TraceSource |
注释
对于大多数 Azure 内置重试机制,目前无法对不同类型的错误或异常应用不同的重试策略。 应配置提供最佳平均性能和可用性的策略。 微调策略的一种方法是分析日志文件,以确定发生的暂时性故障类型。
Example
有关使用本文中讨论的许多模式的示例,请参阅 适用于 .NET 的 Reliable Web 应用模式 。 GitHub 上还有 一个参考实现 。
相关链接
可靠性清单
请参阅完整的建议集。