Retry Pattern

Retry模式能够通过重复之前失败的操作来处理那些在调用远端服务或者网络资源的时候发生的一些可以预期的临时性的错误。Retry模式可以提高应用的稳定性。

问题

应用中,负责链接其他服务的组件必须要对环境中可能发生的临时性错误十分敏感。这些错误包括瞬间的网络连接丢失,服务的暂时不可用,或者是服务繁忙导致的超时等等。这些错误都属于不需要额外操作就能够自我修复的错误,只需要过了一定的时间延迟再重复之前的失败操作就能轻易修复。举例来说,如果有一个数据库服务处理大量的并发请求,该服务实现了一个简单的熔断策略,当到达阈值后,会直接拒绝多余的请求。那么,如果开发者的应用调用这个服务被拒绝了,那么只要等待一段时间再进行重试即可修复这个问题。

解决方案

在云环境中,临时性的错误是较为常见的,应用应该在设计的时候就应该能够处理这类错误,以减少这类错误对于业务的影响。

如果应用在通过请求访问远程服务的时候,检测到了错误,可以通过如下的策略来处理这些错误:

对于大多数的常见的短暂性错误,第一次失败操作和第二次重试操作之间的时间间隔应该合理配置,来尽可能让应用的不同实例(云中的不同应用服务器)尽量保持发送请求的时间间隔。这样做可以防止服务过于繁忙而造成过载。如果应用的多个实例不断的高频率的发送重试的请求,远端服务的恢复时间可能会更久。

如果请求仍然失败,应用可以等待更久的时间然后再进行下一次的重试。如果有必要的话,重试的失败后可以增加延迟时间,直到这个时间到达一个阈值,然后再将服务置为失败。重试的间隔时间可以是逐渐递增的,也可以使用其他的时间策略,比如指数增长,或者是根据失败请求的一些特性来配置都是可以的。

下图展示了Retry模式的流程,如果远端服务的请求失败了,则继续重复请求,一旦失败的次数超过了预设的失败次数,应用就视该次请求为异常情况,然后在进行后续的处理。

  1. 应用调用宿主的服务,请求失败了,响应的是HTTP500,是服务器内部错误。
  2. 应用等待了一段时间,然后继续重复之前的失败请求。请求仍然失败了,仍然返回了HTTP500。
  3. 应用等待了更长的时间再次重试,这次请求成功了,服务返回HTTP200.

应用应该将所有尝试访问远程的服务都通过Retry模式的策略来包裹起来。而访问不同的远端服务,也应该使用不同的时间策略,有些宿主服务还会提供封装了重试的库。而这些实现库通过参数来配置策略,而开发者可以通过指定这些参数来配置重试次数,时间策略等信息。

应用中的检测错误和重试失败操作的代码应该记录所有失败操作的日志信息。这些信息对于后续的维护是很有用的。如果应用的日志经常显示远端服务不可用或者繁忙,通常可能是因为远端服务的资源耗尽了。开发者可以通过对远端服务的扩展来降低这些错误。比如,如果一个数据库服务经常过载,可以将数据库进行分离,将负载分散到多个服务器来缓解这个问题。

Windows Azure提供了很多对于Retry模式的支持。短暂错误处理模式和实践一文中描述了在Windows Azure服务中可以使能很多重试策略来处理短暂性错误。 微软的Entity Framework version 6一书中提供了很多关于重试数据库操作的工具。另外,很多Windows Azure的服务和存储API都实现了重试模式的。

需要考虑的问题

在实现Retry模式的时候也需要考虑如下一些问题:

何时使用该模式

何时该使用Retry模式:

何时不该使用Retry模式:

Retry模式使用举例

下面的例子展示了实现Retry模式的一个方案。下面的OperationWithBasicRetryAsync方法,通过TransientOperationAsync调用了一个外部的异步服务。

C#
private int retryCount = 3;
...
public async Task OperationWithBasicRetryAsync()
{
    int currentRetry = 0;
    for (; ;)
    {
        try
        {
            // Calling external service.
            await TransientOperationAsync();
            // Return or break.
            break;
        }
        catch (Exception ex)
        {
            Trace.TraceError("Operation Exception");
            currentRetry++;
            // Check if the exception thrown was a transient exception
            // based on the logic in the error detection strategy.
            // Determine whether to retry the operation, as well as how
            // long to wait, based on the retry strategy.
            if (currentRetry > this.retryCount || !IsTransient(ex))
            {
                // If this is not a transient error
                // or we should not retry re-throw the exception.
                throw;
            }
        }

        // Wait to retry the operation.
        // Consider calculating an exponential delay here and
        // using a strategy best suited for the operation and fault.
        Await.Task.Delay();
    }
}

// Async method that wraps a call to a remote service (details not shown).
private async Task TransientOperationAsync()
{
    ...
}

封装在try-catch代码块中的方法调用被封装到一个for循环中。for循环只有当TransientOperationAsync方法成功的返回或者抛出异常才能退出for循环。如果TransientOperationAsync方法失败了,那么catch代码块会检查产生错误的原因,如果错误属于临时性错误,那么应用会等待一会,然后再重新进行之前的操作。

for循环也会监控外部调用的调用的次数,如果代码失败次数超过了3次,那么应用就会认为错误还会继续持续一段时间。如果异常并非是暂时性错误,或者这个错误持续的时间可能比较久,catch代码块就会直接抛出异常。这个异常也一样可以退出for循环,并且这个异常最好由调用OperationWithBasicRetryAsync方法的地方来处理。

下面的IsTransient方法会检查异常的类型,来判断异常是否属于内部错误还是属于WebException或者是临时性错误。其中的OperationTransientException就代表调用的操作发生了临时性错误。参考如下代码:

private bool IsTransient(Exception ex)
{
    // Determine if the exception is transient.
    // In some cases this may be as simple as checking the exception type, in other
    // cases it may be necessary to inspect other properties of the exception.
    if (ex is OperationTransientException)
        return true;

    var webException = ex as WebException; if (webException != null)
    {
        // If the web exception contains one of the following status values
        // it may be transient.
        return new[] {
            WebExceptionStatus.ConnectionClosed,
            WebExceptionStatus.Timeout,
            WebExceptionStatus.RequestCanceled }. Contains(webException.Status);
    }

    // Additional exception checking logic goes here.
    return false;
}

相关的其它模式

在文章中有提到过最多的就是Circuit-Breaker模式