处理超时问题并在 Azure 服务总线中配置重试

当请求未在配置的时间内完成时,发送和接收操作可能会超时。 大多数超时都是可恢复的,客户端库会自动重试。 当超时传递到你的应用程序代码时,要么所有重试机会都已耗尽,要么该操作的耗时超过了配置的单次尝试超时时间。

本文介绍超时的含义、如何配置控制此行为的重试选项,以及如何避免导致可避免超时的客户端模式。 它是故障排除指南服务总线 异常参考文档的补充。

超时的含义

根据操作和主机环境的不同,超时可能表现为 TimeoutExceptionOperationCanceledException,或其 ReasonServiceTimeout

指示 ServiceTimeout 服务未在预期时间内响应请求。 原因可能位于连接两侧:

  • 客户端 - 应用程序无法及时访问服务(网络路径、DNS、代理、防火墙)、客户端过载(线程池不足、CPU 过高),或者配置的尝试超时时间太短,无法执行此操作。
  • 服务端 - 服务暂时异常或更大范围的故障。 当服务出现故障时,故障通常跨多个操作关联,并在Azure门户中的命名空间资源运行状况页中可见。

使用 IsTransient 上的 ServiceBusException 属性来区分可恢复的故障。 当 IsTransienttrue 时,客户端已应用重试策略,并且每次尝试都失败。 持续出现的瞬时超时通常表明存在连接问题,或者重试配置设置过小。

配置重试选项

根据客户端上配置的重试选项,客户端库会重试暂时性故障,包括超时。 在 .NET 库中,通过 ServiceBusClientOptions 设置 ServiceBusRetryOptions

var options = new ServiceBusClientOptions
{
    RetryOptions = new ServiceBusRetryOptions
    {
        Mode = ServiceBusRetryMode.Exponential,
        MaxRetries = 3,
        Delay = TimeSpan.FromSeconds(0.8),
        MaxDelay = TimeSpan.FromSeconds(60),
        TryTimeout = TimeSpan.FromSeconds(60)
    }
};

var client = new ServiceBusClient(fullyQualifiedNamespace, credential, options);
选项 它控制的内容 默认
Mode 退避是 Fixed 还是 Exponential Exponential
MaxRetries 暂时性故障的最大重试尝试次数 3
Delay 尝试之间的基本延迟 0.8 秒
MaxDelay 回退延迟的上限 60 秒
TryTimeout 单次尝试超时前的最长持续时间 60 秒

请对照 ServiceBusRetryOptions 参考检查当前默认值,因为这些默认值可能会因库版本不同而发生变化。 JavaJavaScriptPythonGo 客户端库中存在等效的重试选项。

调整工作负载的值

  • 如果操作确实需要超过 60 秒,请增加 TryTimeout — 例如,从具有较长 maxWaitTime 的低流量队列接收消息,或者在高延迟网络上进行操作。 较大的尝试超时可减少过早超时,但也会延长真正无法访问的服务出现错误之前的时间。
  • 增加 MaxRetriesMaxDelay 以承受更长时间的瞬态情况,但代价是故障检测会变慢。
  • 请注意锁定时长。 如果 ReasonServiceBusy(节流),客户端将进行 10 秒退避。 如果实体的锁定持续时间短于退让时间,则等待期间可能会丢失消息或会话锁。 请参阅 限制

避免使用会导致超时的客户端模式

许多超时升级是由应用程序模式而不是服务引起的。 首先解决这些问题。

不要阻止异步调用(同步过度同步)

在异步 服务总线 API 上调用 .Result.Wait().GetAwaiter().GetResult() 会阻塞一个线程池线程。 在高负载下,此操作会导致线程池饥饿:用于处理已完成 I/O 的回调无法执行,因此即使服务运行正常,操作也会超时。 症状表现为超时,并且会随着并发增加而加重,在负载下降时消失。

请改为以端到端方式使用 async/await

// Avoid
ServiceBusReceivedMessage message = receiver.ReceiveMessageAsync().GetAwaiter().GetResult();

// Prefer
ServiceBusReceivedMessage message = await receiver.ReceiveMessageAsync();

将客户端作为单例重用

每个新的 ServiceBusClient 都会打开一个新的 AMQP 连接。 为每条消息或每个请求创建一个客户端会耗尽套接字,并导致连接超时。 在应用程序的生存期内创建一个客户端并重复使用它。 有关详细信息,请参阅 套接字耗尽错误

合理设置预取和并发度

较高的预提取计数,或较高的 MaxConcurrentCalls 与缓慢的消息处理相结合,可能会导致锁超时和表现为超时的处理延迟。 将预取与您的处理速率相匹配,并在处理确实需要的时间超过锁定时长时续订锁定。 请参阅 预提取消息性能最佳做法

验证网络路径

在连接时发生超时时,确认客户端可以访问服务:

  • AMQP 端口 5671 和 5672 已开放,或者通过 443 端口使用 WebSocket 传输。
  • DNS 解析命名空间,包括任何专用终结点和专用 DNS 区域配置。
  • 没有代理或防火墙正在截获连接。

有关分步网络检查,请参阅 连接、证书或超时问题

在代码中处理超时情况

针对 Reason 进行筛选,这样你就可以专门处理超时,而不是以相同的方式捕获所有故障:

try
{
    await sender.SendMessageAsync(message);
}
catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.ServiceTimeout)
{
    // The client already retried. Log for correlation, then decide whether
    // to retry at the application level or surface the failure.
}

确定是客户端还是服务

在将超时视为服务问题上报之前,请先排除客户端因素:

  1. 检查Azure门户中的命名空间资源运行状况页,了解受影响的时间范围。
  2. 检查这些超时是否与客户端 CPU 使用率高或高并发相关。
  3. 启用客户端日志,以查看是否发生了重试以及底层的 AMQP 错误是什么。 请参阅 日志记录和诊断
  4. 确认来自客户端主机的网络路径和 DNS 解析。

如果资源运行状况将命名空间显示为正常,并且超时与客户端加载或配置相关,则问题很可能在客户端上。 如果资源健康状况在整个时间窗口内都显示为降级,则更可能是服务端导致的。