重试暂时性故障处理设计-常用的架构设计原则
与远程服务和资源通信的所有应用程序必须对暂时性故障敏感。 对于云中运行的应用程序尤其如此,因为其环境的性质与通过 Internet 建立连接的特点,意味着更容易遇到这种类型的故障。 暂时性故障包括组件和服务瞬间断开网络连接、服务暂时不可用,或者当服务繁忙时出现超时。 这些故障通常可自我纠正,如果在适当的延迟后重复操作,则可能会成功。
为什么云中会出现暂时性故障?
任何环境、任何平台或操作系统以及任何类型的应用程序都会发生暂时性故障。 在本地基础结构上运行的解决方案中,应用程序及其组件的性能和可用性通常是通过昂贵且经常未充分利用的硬件冗余来维护的,组件和资源彼此接近。 虽然这种方法会降低故障出现机率,但仍可能导致暂时性故障,甚至可能会因出现无法预见的事件(例如外部电源或网络问题或其他灾难方案)而中断。
云托管(包括私有云系统)可以通过使用共享资源、冗余、自动故障转移以及在多个商用计算节点之间动态分配资源,提供更高的总体可用性。 但是这些环境的性质意味着更可能发生暂时性故障。 原因包括:
-
云环境中的许多资源是共享的,为了保护这些资源,会限制对这些资源的访问。 某些服务在负载上升到特定级别时,或到达吞吐量比率的上限时,会拒绝连接以便处理现有的请求,并为所有用户维持服务性能。 限制有助于为共享资源的邻居与其他租户维持服务质量。
-
云环境是使用大量商用硬件单元构建而成的。 云环境将负载动态分散到多个计算单元和基础结构组件上以提供性能,并通过自动回收或更换故障单元来提供可靠性。 这种动态性意味着可能偶尔会发生暂时性故障和暂时连接失败。
-
在应用程序与资源及其使用的服务之间,通常有多个硬件组件,包括网络基础结构,例如路由器和负载均衡器。 这个附加的基础结构偶尔会导致额外的连接延迟与暂时性连接故障。
-
客户端与服务器之间的网络状况会不时改变,尤其是通过 Internet 通信时。 即使在本地位置,负载繁重的流量也可能会导致通信变慢并导致间歇性的连接故障。
挑战
暂时性故障对应用程序的可感知可用性有重大影响,即使已在所有可预见的情况下对其进行了全面的测试。 若要确保云托管的应用程序可靠运行,应用程序必须能够应对以下挑战:
-
应用程序必须能够检测到故障的发生,并确定这些故障可能是暂时性的、持久性的还是终端故障。 发生故障时,不同的资源可能返回不同的响应,这些响应可能会根据操作上下文而有所不同,例如,针对从存储读取时所发生错误返回的响应,与针对写入存储时所发生错误返回的响应不同。 许多资源和服务都妥善制定了暂时性故障的合约。 但是,若不提供此类信息,则很难发现故障的性质,以及故障是否是暂时性的。
-
如果确定故障可能是暂时性的,应用程序必须能够重试操作,并跟踪操作重试的次数。
-
应用程序必须使用适当的重试策略。 此策略指定应用程序应该重试的次数、每两次尝试的延迟时间,以及尝试失败后执行的操作。 适当的尝试次数以及每两次尝试的延迟时间通常难以确定,会根据资源类型以及应用程序本身的当前操作条件而有所不同。
一般指南
以下指南将帮助你为应用程序设计适当的暂时性故障处理机制:
-
确定是否存在内置的重试机制:
-
许多外部服务提供SDK或包含暂时性故障处理机制的客户端库。 服务使用的重试策略通常是根据目标服务的性质和要求定制的。 或者对于确定重试是否适当,以及在下一次尝试重试之前要等待多长时间方面,服务的REST接口可能会返回有用的信息。
-
使用内置的重试机制(如果可用),除非你具有更合适的、更合适的重试行为的特定易懂的要求。
-
-
确定操作是否适合重试:
-
只在故障是暂时性(通常可由故障的性质来判断),以及在重新尝试后操作至少有一些成功的可能性时,才应重试操作。 意义操作中不存在指示操作无效的操作,例如对不存在的项进行数据库更新,或请求出现严重错误的服务或资源的请求。
-
一般而言,只有在能够确定操作的整个影响,且状况已获得充分了解并可验证时,才实施重试。 否则,应该由调用代码来实施重试。 请记住,从无法控制的资源与服务返回的错误可能会随着时间而演进,可能需要重新访问暂时性故障检测逻辑。
-
创建服务或组件时,请考虑实施错误代码和消息,以帮助客户端确定是否应重试失败的操作。 具体而言,指明客户端是否应重试操作(也许是通过返回 isTransient 值),并建议下一次重试之前的适当延迟。 如果要构建 Web 服务,请考虑返回服务合约中定义的自定义错误。 即使一般的客户端可能无法读取这些错误,但在构建自定义客户端时,自定义错误很有帮助。
-
-
确定适当的重试计数与间隔:
-
请务必优化重试计数和用例类型的间隔。 如果重试次数不足,应用程序将无法完成操作,并且可能会失败。 如果重试次数太多或间隔太短,应用程序可能会长期保留资源(例如线程、连接和内存),这会对应用程序的运行状况造成不利影响。
-
适当的时间间隔与重试次数值取决于正在尝试的操作类型。 例如,如果操作是用户交互的一部分,则间隔应该较短,且只需重试几次,以避免让用户等待响应(这会让连接保持打开,并可能会降低其他用户的可用性)。 如果操作是长时间运行的工作流或关键工作流的一部分,在这种情况下,取消和重启进程会消耗大量资源或耗时,因此最好等待两次尝试,然后再重试。
-
确定适当的重试间隔是设计一个成功的策略时最困难的部分。 典型的策略会使用以下类型的重试间隔:
-
指数退让。 应用程序在第一次重试之前短暂地等待,每个后续重试的间隔时间呈指数增加。 例如,在 3 秒、12 秒、30 秒后重试操作。
-
增量间隔。 应用程序在第一次重试之前短暂地等待,每个后续重试的间隔时间增量递增。 例如,在 3 秒、7 秒、13 秒后重试操作。
-
固定间隔。 应用程序每次尝试的间隔时间相同。 例如,固定每 3 秒重试操作。
-
立即重试。 有时暂时性故障很短暂,原因可能是网络数据包冲突或硬件组件中的峰值。 在此情况下,适合立即重试操作,因为如果故障在操作让应用程序组合并发送下一个请求时已清除,则操作可能会成功。 但是,如果立即重试失败,应切换为备用策略,例如指数回退或回退操作,而不应超过一次立即重试次数。
-
随机化。 任何上述重试策略都可包含随机化,以防止客户端的多个实例同时发送后续重试请求。 例如,一个实例可能会在3秒、11秒、28秒后重试操作,而另一个实例可以在4秒、12秒、26秒后重试该操作。 随机化是有用的技术,可配合其他策略使用。
-
-
一般指导原则是,为后台操作使用指数退让策略,为交互式操作使用立即或固定间隔重试策略。 在上述两种情况下,应该选择延迟与重试计数,使所有重试的延迟上限都在所需的端到端延迟要求范围内。
-
请考虑到所有会对重试操作的整体超时上限造成影响的因素组合。 这些因素包括失败连接生成响应所花费的时间(通常根据客户端的超时值设置),以及重试之间的延迟和重试次数上限。 所有这些时间的总和会导致整体操作时间较长,尤其是在使用指数延迟策略时,在这种情况下,每次发生故障后重试的间隔会迅速增长。 如果某个进程必须满足 (SLA) 的特定服务级别协议,则整体操作时间(包括所有的超时和延迟)必须在 SLA 中定义的限制范围内。
-
过于频繁的重试策略(其间隔太短或重试过于频繁)会对目标资源或服务造成不利影响。 这可能会造成资源或服务无法从过载状态恢复,并且会继续阻止或拒绝请求。 这会造成恶性循环,越来越多的请求将发送到资源或服务,因而造成其恢复能力进一步降低。
-
选择重试间隔时请考虑操作的超时,以避免立即启动后续尝试(例如当超时期间与重试间隔类似时)。 还应考虑是否需要让可能期间的总和(超时值加重试间隔)短于特定的时间总和。 超时设置非常短或非常长的的操作可能会影响等待时间,以及重试操作的频率。
-
使用异常类型及其包含的任何数据,或者使用从服务返回的错误代码与消息,来优化重试的间隔和次数。 例如,某些异常或错误代码(如 HTTP 代码 503 - 服务不可用,以及响应中的 Retry-After 标头)会指示错误可能持续的时间,或服务失败且不会响应任何后续尝试。
-
-
避免反模式:
-
在绝大多数情况下,应该避免使用包含重复重试代码层的实现。 避免使用包括级联重试机制的设计,或避免使用在涉及请求层次结构的操作的每个阶段实施重试的设计,除非有特定的要求。 在这些例外的情况下下,请使用策略避免过多的重试次数和延迟期间过长,并确保了解后果。 例如,如果某个组件对另一个组件发出请求,后者再访问目标服务,并且要对这两个调用各实施重试三次,则总共会对该服务重试九次。 许多服务和资源实施内置重试机制,如果需要在较高级别实施重试,应调查如何禁用或修改此设置。
-
切勿实施永不结束的重试机制。 这很可能会导致资源或服务无法从过载情况下恢复,并造成限制与遭到拒绝的连接持续更长时间。 使用有限的重试次数或使用断路器等模式,使服务可以恢复。
-
切勿多次执行立即重试。
-
避免使用固定重试间隔,尤其是在访问公有云计算环境中的服务与资源期间要重试很多次时。 此情况下的最佳方法是指数退让策略以及断路功能。
-
防止同一个客户端有多个实例,或不同客户端有多个实例同时发送重试请求。 如果这有可能发生,请在重试间隔中引入随机化。
-
-
测试重试策略与实施:
-
确保在尽可能广泛的条件下全面测试重试策略实施,尤其是当实施使用的应用程序与目标资源或服务要承受极端负载时。 要检查测试期间的行为,可以:
-
将暂时性故障和非暂时605故障注入服务中。 例如,发送无效请求或添加代码用于检测包含不同错误类型的测试请求与响应。
-
创建资源或服务模型,用于返回真实服务可能返回的错误范围。 确保覆盖重试策略旨在检测的所有错误类型。
-
通过暂时禁用或重载服务中的任何共享资源或共享服务。
-
对于基于 HTTP 的 API,请考虑在自动化测试中使用 FiddlerCore 库来更改 HTTP 请求的结果,方法是增加额外的往返时间或更改响应(例如 HTTP 状态代码、标头、正文或其他因素)。 这样,便可以确定性地测试一部分故障状况,无论是暂时性故障还是其他类型的故障。 有关详细信息,请参阅 FiddlerCore。
-
执行高负载因子和并发测试,确保重试机制与策略在这些条件下能正常工作,且不会对客户端操作造成不良的影响或导致请求之间交叉污染。
-
-
-
管理重试策略配置:
-
重试策略 是所有重试策略元素的组合。 它定义了能确定故障是否可能是暂时性的检测机制、使用的间隔类型(例如固定、指数退让及随机化)、实际间隔值,以及重试次数。
-
必须在应用程序中的多个位置实施重试,即使是最简单的应用程序也是如此,至于较复杂的应用程序,则必须每一层都实施。 考虑使用集中点存储所有策略,而不是将每个策略的元素硬编码在多个位置。 例如,在应用程序配置文件中存储间隔和重试计数等值、在运行时读取这些值,并以编程方式构建重试策略。 这样可以更轻松地管理设置,以及修改和优化值,以应对不断变化的要求和方案。 但是,请将系统设计为存储值而不是每次都要重新读取配置文件,并确保在无法从配置中获取值时使用适当的默认值。
-
考虑在公有云服务应用程序中存储那些不变值,用于在服务配置文件中构建运行时的重试策略,使这些值不需要重新启动应用程序即可更改。
-
利用所用客户端 API 中的内置或默认重试策略,但前提是这些策略适合方案。 这些策略通常是通用的。 在某些状况下,这些策略可能都是必需的,但在某些状况下,它们可能无法提供完整的选项范围来满足特定的要求。 必须通过测试来了解设置如何影响应用程序,以确定最合适的值。
-
-
记录和跟踪暂时性故障和非暂时605故障:
-
在重试策略中包含异常处理,以及其他用于记录重试时间的工具。 尽管偶尔发生暂时性故障和重试是预期的,但并不表示存在问题,但定期重试的重试次数通常是可能导致故障的问题的指示器,或者当前正在降低应用程序的性能和可用性。
-
将暂时性故障记录为“警告”项,而不是“错误”项,以便监视系统不会将其检测为应用程序错误而触发误报。
-
考虑将值存储在日志项中,用于指示重试是由服务中的限制所造成,或是由其他类型的故障(例如连接失败)造成,使你能够在分析数据期间进行区分。 限制错误数目的增加通常表示应用程序的设计有瑕疵,或者需要改用可提供专用硬件的高级服务。
-
考虑测量和记录包含重试机制的操作所需的总体时间。 这是暂时性故障对用户响应时间、进程延迟以及应用程序用例效率造成的整体影响的良好指标。 此外,还要记录发生重试的次数,以了解影响响应时间的因素。
-
考虑实施遥测和监视系统,当故障次数与比率、重试平均次数或操作成功所需的整体时间增加时,该系统会引发警报。
-
-
管理不断失败的操作:
-
如果每次尝试后操作仍然失败,则必须考虑如何处理这种情况:
-
尽管重试策略会定义操作应重试次数的上限,但它不会防止应用程序使用与重试次数相同的次数一再重复操作。 例如,如果订单处理服务因为严重错误而失败且永久失效,则重试策略会检测连接超时,并将它视为暂时性故障。 代码将按指定的次数重试操作,然后放弃。 但是,当另一位客户下单时,会再次尝试该操作,即使该操作每次肯定都会失败。
-
为防止不断重试连续失败的操作,请考虑实施断路器模式。 在此模式中,如果在指定的时段内失败次数超过阈值,则会立即将请求返回给调用方,并将失败视为故障,而不会尝试访问失败的资源或服务。
-
应用程序可以按间歇定期测试服务,并在请求之间进行较长的间隔,以检测其何时变为可用。 适当的间隔取决于方案,例如操作的重要性和服务的性质,可能是数分钟到数个小时。 测试成功时,应用程序将恢复正常操作,并将请求传递给刚刚恢复的服务。
-
同时,可以故障回复到服务的另一个实例(也许在不同的数据中心或应用程序中)、使用提供兼容(也许是更简单)功能的类似服务,或执行某些替代操作,以期该服务很快可供使用。 例如,有时适合将服务的请求存储在队列或数据存储中,供以后重复使用。 也可以将用户重定向到应用程序的其他实例、使应用程序性能降级但仍可提供可接受的功能,或者只是将消息返回给用户,指出应用程序暂时不可用。
-
-
-
其他注意事项
-
在决定重试次数的值和策略的重试间隔时,请考虑服务或资源的操作是否是长时间运行的或多步骤的操作的一部分。 当某个操作步骤失败时,补偿其他所有操作步骤可能会很困难或代价非凡。 在此情况下,可以接受很长的间隔及大量的重试次数,前提是不会因为保留或锁定稀缺资源而阻止其他操作。
-
考虑重试相同的操作是否会导致数据不一致。 如果多步骤进程的某些部分重复,并且操作不是幂等的,则可能会导致不一致。 例如,递增值的操作如果重复,则会生成无效的结果。 重复执行将消息发送到队列的操作如果无法检测到重复消息,则可能会对消息使用者造成不一致情况。 要避免此问题,请确保将每个步骤设计成幂等操作。 有关幂等性的详细信息,请参阅 幂等性模式。
-
考虑重试操作的范围。 例如,可以更轻松地在包含数个操作的级别实施重试代码,如果其中一个操作失败,则重试所有操作。 但是,这可能会导致幂等性问题或不必要的回滚操作。
-
如果选择的重试范围包含多个操作,在确定重试间隔时、监视花费时间时,以及因失败而引发警报之前,请考虑所有操作的延迟总和。
-
考虑重试策略如何影响共享应用程序中的邻居或其他租户,以及何时使用共享资源与服务。 积极重试策略会导致其他用户以及共享资源与服务的应用程序发生越来越多的暂时性故障。 同样地,应用程序可能会受到资源与服务的其他用户所实施的重试策略的影响。 对于任务关键型应用程序,可以决定使用非共享的高级服务。 这样就可以更好地控制这些资源与服务的负载与后续限制,从而找到提高成本的理由。
-