排查 Azure 服务总线问题

本文介绍了故障调查技术、并发性、Azure 服务总线 Java 客户端库中凭据类型的常见错误,以及解决这些错误的缓解步骤。

启用和配置日志记录

Azure SDK for Java 提供了一致的日志记录案例,有助于排查应用程序错误并帮助加快解决速度。 生成的日志会在到达终端状态之前捕获应用程序的流,以帮助查找根本问题。 有关日志记录的指导,请参阅在 Azure SDK for Java 中配置日志记录故障排除概述

除了启用日志记录之外,将日志级别设置为 VERBOSEDEBUG 还可以深入了解库的状态。 以下部分演示了示例 log4j2 和 logback 配置,以减少启用详细日志记录时的过多消息。

配置 Log4J 2

使用以下步骤配置 Log4J 2:

  1. 在“Log4j2 所需的依赖项”部分中,使用日志记录示例 pom.xml 中的依赖项在 pom.xml 中添加依赖项。
  2. log4j2.xml 添加到 src/main/resources 文件夹。

配置 logback

使用以下步骤配置 logback:

  1. 在“logback 所需的依赖项”部分中,使用日志记录示例 pom.xml 中的依赖项在 pom.xml 中添加依赖项。
  2. logback.xml 添加到 src/main/resources 文件夹。

启用 AMQP 传输日志记录

如果启用客户端日志记录不足以诊断问题,则可以启用对基础 AMQP 库 Qpid Proton-J 中的文件进行日志记录。 Qpid Proton-J 使用 java.util.logging。 可以通过创建具有下一节所示内容的配置文件来启用日志记录。 或者,为 java.util.logging.Handler 实现设置 proton.trace.level=ALL 和需要的任何配置选项。 有关实现类及其选项,请参阅 Java 8 SDK 文档中的 Package java.util.logging

若要跟踪 AMQP 传输帧,请设置 PN_TRACE_FRM=1 环境变量。

示例 logging.properties 文件

以下配置文件将 Proton-J 的 TRACE 级别输出记录到文件 proton-trace.log

handlers=java.util.logging.FileHandler
.level=OFF
proton.trace.level=ALL
java.util.logging.FileHandler.level=ALL
java.util.logging.FileHandler.pattern=proton-trace.log
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=[%1$tF %1$tr] %3$s %4$s: %5$s %n

减少日志记录

减少日志记录的一种方法是更改详细程度。 另一种方法是添加筛选器,从记录器名称包(如 com.azure.messaging.servicebuscom.azure.core.amqp)中排除日志。 有关示例,请参阅配置 Log4J 2配置 logback 部分中的 XML 文件。

提交错误时,以下包中类的日志消息很有趣:

  • com.azure.core.amqp.implementation
  • com.azure.core.amqp.implementation.handler
    • 例外情况是可以忽略 ReceiveLinkHandler 中的 onDelivery 消息。
  • com.azure.messaging.servicebus.implementation

ServiceBusProcessorClient 中的并发

ServiceBusProcessorClient 使应用程序能够配置对消息处理程序的多少个调用应该并发发生。 这种配置使得并行处理多条消息成为可能。 对于来自非会话实体的 ServiceBusProcessorClient 正在使用的消息,应用程序可以使用 maxConcurrentCalls API 配置所需的并发。 对于启用会话的实体,所需的并发为 maxConcurrentSessions 乘以 maxConcurrentCalls

如果应用程序观察到对消息处理程序的并发调用少于配置的并发调用,则可能是因为线程池的大小不合适。

ServiceBusProcessorClient 使用来自 Reactor 全局 boundedElastic 线程池中的守护程序线程调用消息处理程序。 此池中并发线程的最大数量受上限限制。 默认情况下,此上限是可用 CPU 内核数量的十倍。 为了使 ServiceBusProcessorClient 有效地支持应用程序所需的并发性(maxConcurrentCallsmaxConcurrentSessions 乘以 maxConcurrentCalls),必须使 boundedElastic 池上限值高于所需的并发性。 可以通过设置系统属性 reactor.schedulers.defaultBoundedElasticSize 来覆盖默认上限。

应该根据具体情况调整线程池和 CPU 分配。 但是,当重写池上限(作为起点)时,将并发线程限制为每个 CPU 核心大约 20-30 个。 建议将每个 ServiceBusProcessorClient 实例的所需并发限制为大约 20-30。 分析并衡量特定用例,并相应地优化并发方面。 对于高负载方案,请考虑运行多个 ServiceBusProcessorClient 实例,其中每个实例都是从一个新的 ServiceBusClientBuilder 实例生成的。 此外,请考虑在专用主机(例如容器或 VM)中运行每个 ServiceBusProcessorClient,以便一个主机中的停机时间不会影响整体消息处理。

请记住,在 CPU 内核较少的主机上设置较高的池上限值会产生不利影响。 CPU 资源不足或池中线程过多且 CPU 数量较少的迹象包括:频繁超时、锁丢失、死锁或吞吐量降低。 如果要在容器上运行 Java 应用程序,建议使用两个或多个 vCPU 核心。 在容器化环境中运行 Java 应用程序时,不建议选择少于 1 个 vCPU 内核。 有关资源分配的深入建议,请参阅容器化 Java 应用程序

连接共享瓶颈

从共享 ServiceBusClientBuilder 实例创建的所有客户端都与服务总线命名空间具有相同的连接。

使用共享连接可以在一个连接上实现客户端之间的多路复用操作;但如果有许多客户端,或者客户端一起产生高负载,共享也可能成为瓶颈。 每个连接都有一个与其关联的 I/O 线程。 共享连接时,客户端将其工作放入此共享 I/O 线程的工作队列中,每个客户端的进度取决于其在队列中工作的及时完成。 I/O 线程以串行方式处理排队的工作。 也就是说,如果共享连接的 I/O 线程工作队列最终有很多待处理的工作要处理,那么症状与 CPU 不足相似。 在上一节关于并发性的内容中描述了这种情况,例如客户端停滞、超时、丢失锁或恢复路径减慢。

服务总线 SDK 对连接 I/O 线程使用 reactor-executor-* 命名模式。 当应用程序遇到共享连接瓶颈时,它可能会反映在 I/O 线程的 CPU 使用率上。 此外,在堆转储或实时内存中,对象 ReactorDispatcher$workQueue 是 I/O 线程的工作队列。 瓶颈期间内存快照中的长工作队列可能表示共享 I/O 线程因挂起的工作而过载。

因此,如果服务总线终结点的应用程序负载在发送-接收消息的总数或有效负载大小方面相当高,则应为生成的每个客户端使用单独的生成器实例。 例如,对于每个实体(队列或主题),可以创建一个新的 ServiceBusClientBuilder 并从中生成客户端。 如果对特定实体的负载非常高,可能需要为该实体创建多个客户端实例,或者在多个主机(例如容器或 VM)中运行客户端以实现负载均衡。

客户端在使用应用程序网关自定义终结点时停止

自定义终结点地址是指应用程序提供的 HTTPS 终结点地址,该地址可解析为服务总线或配置为将流量路由到服务总线。 Azure 应用程序网关使创建将流量转发到服务总线的 HTTPS 前端变得容易。 可以为应用程序配置服务总线 SDK,使其使用应用程序网关前端 IP 地址作为连接到服务总线的自定义终结点。

应用程序网关提供了多种支持不同 TLS 协议版本的安全策略。 有预定义的策略强制 TLSv1.2 作为最低版本,也有旧的策略将 TLSv1.0 作为最低版本。 HTTPS 前端将应用 TLS 策略。

现在,服务总线 SDK 无法识别应用程序网关前端的某些远程 TCP 终止,该前端使用 TLSv1.0 作为最低版本。 例如,如果前端在更新其属性时发送 TCP FIN、ACK 数据包以关闭连接,SDK 无法检测到它,因此它不会重新连接,客户端也无法再发送或接收消息。 只有在使用 TLSv1.0 作为最小版本时才会发生这种中断。 若要缓解问题,请使用具有 TLSv1.2 或更高版本的安全策略作为应用程序网关前端的最低版本。

所有 Azure 服务对 TLSv1.0 和 1.1 的支持已经宣布将于 2024 年 10 月 31 日结束,因此强烈建议过渡到 TLSv1.2。

消息或会话锁丢失

服务总线队列或主题订阅在资源级别设置了锁定持续时间。 当接收方客户端从资源拉取消息时,服务总线中转向消息应用初始锁。 初始锁在资源级别设置的锁定持续时间内持续。 如果消息锁在到期前没有续订,则服务总线中转会释放该消息,使其可供其他接收方使用。 如果应用程序在锁定到期后尝试完成或放弃消息,则 API 调用将失败,并返回错误 com.azure.messaging.servicebus.ServiceBusException: The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue

服务总线客户端支持运行后台锁续订任务,该任务在每次消息锁到期之前持续续订消息锁。 默认情况下,锁续订任务运行 5 分钟。 可以通过使用 ServiceBusReceiverClientBuilder.maxAutoLockRenewDuration(Duration) 来调整锁续订持续时间。 如果传递 Duration.ZERO 值,则锁续订任务被禁用。

以下列表描述了可能导致锁定丢失错误的一些使用模式或主机环境:

  • 锁续订任务已禁用,应用程序的消息处理时间超过了在资源级别设置的锁持续时间。

  • 应用程序的消息处理时间超过了配置的锁续订任务持续时间。 请注意,如果没有明确设置锁续订持续时间,则默认为 5 分钟。

  • 应用程序已通过使用 ServiceBusReceiverClientBuilder.prefetchCount(prefetch) 将预提取值设置为正整数来启用预提取功能。 启用预提取功能后,客户端将从服务总线实体(队列或主题)检索等于预提取的消息数,并将其存储在内存中的预提取缓冲区中。 消息将保留在预提取缓冲区中,直到它们被应用程序接收为止。 当消息在预提取缓冲区中时,客户端不会扩展消息的锁。 如果应用程序处理时间过长,导致消息锁在预提取缓冲区中过期,则应用程序可能会获取具有过期锁的消息。 有关详细信息,请参阅为什么预提取不是默认选项?

  • 主机环境偶尔会出现网络问题(例如暂时性网络故障或中断),这会阻止锁续订任务按时续订锁。

  • 主机环境缺少足够的 CPU,或者间歇性地缺少 CPU 周期,这会导致锁续订任务无法按时运行。

  • 主机系统时间不准确(例如,时钟偏斜)延迟了锁续订任务,使其无法按时运行。

  • 连接 I/O 线程已过载,这会影响其按时执行锁续订网络调用的能力。 以下两种情况可能会导致此问题:

    • 应用程序正在运行太多共享同一连接的接收器客户端。 有关详细信息,请参阅连接共享瓶颈部分。
    • 应用程序已将 ServiceBusReceiverClient.receiveMessagesServiceBusProcessorClient 配置为具有较大的 maxMessages 值或 maxConcurrentCalls 值。 有关详细信息,请参阅 ServiceBusProcessorClient 中的并发部分。

客户端中的锁续订任务数等于为 ServiceBusProcessorClientServiceBusReceiverClient.receiveMessages 设置的 maxMessagesmaxConcurrentCalls 参数值。 大量进行多个网络调用的锁续订任务也会对服务总线命名空间限制产生不利影响。

如果主机资源不足,即使只有少数锁续订任务正在运行,锁仍可能会丢失。 如果要在容器上运行 Java 应用程序,建议使用两个或多个 vCPU 核心。 在容器化环境中运行 Java 应用程序时,不建议选择少于 1 个 vCPU 内核。 有关资源分配的深入建议,请参阅容器化 Java 应用程序

关于锁的相同说明也适用于启用了会话的服务总线队列或主题订阅。 当接收方客户端连接到资源中的会话时,代理会对会话应用初始锁。 为了保持会话的锁定,客户端中的锁续订任务必须在会话锁到期之前继续续订会话锁。 对于启用会话的资源,基础分区有时会移动以实现跨服务总线节点的负载均衡,例如,当添加新节点以共享负载时。 当这种情况发生时,会话锁可能会丢失。 如果应用程序在会话锁丢失后尝试完成或放弃消息,则 API 调用将失败,并返回错误 com.azure.messaging.servicebus.ServiceBusException: The session lock was lost. Request a new session receiver

升级到 7.15.x 或最新版本

如果遇到任何问题,应首先尝试通过升级到最新版本的服务总线 SDK 来解决这些问题。 7.15.x 版本是一次重大的重新设计,解决了长期存在的性能和可靠性问题。

7.15.x 版本及更高版本减少了线程跳转,删除了锁,优化了热路径中的代码,并减少了内存分配。 这些更改使 ServiceBusProcessorClient 的吞吐量提高了 45-50 倍。

7.15.x 版本及更高版本还带来了各种可靠性改进。 它解决了多种争用情况(如预提取和信用额度计算),并改进了错误处理。 这些更改可以在各种客户端类型存在暂态问题时提高可靠性。

使用最新的客户端

在 7.15.x 版本及更高版本中,具有这些改进的新基础框架称为 V2-Stack。 此发布线包括上一代基础堆栈(7.14.x 版本使用的堆栈)和新的 V2-Stack。

默认情况下,某些客户端类型使用 V2-Stack,而其他客户端类型则要求使用 V2-Stack 选择加入。 可以通过在生成客户端时提供 com.azure.core.util.Configuration 值来实现对客户端类型的特定堆栈(V2 或上一代)的选择加入或退出。

例如,使用 ServiceBusSessionReceiverClient 的基于 V2-Stack 的会话接收需要选择加入,如下例所示:

ServiceBusSessionReceiverClient sessionReceiver = new ServiceBusClientBuilder()
    .connectionString(Config.CONNECTION_STRING)
    .configuration(new com.azure.core.util.ConfigurationBuilder()
        .putProperty("com.azure.messaging.servicebus.session.syncReceive.v2", "true") // 'false' by default, opt-in for V2-Stack.
        .build())
    .sessionReceiver()
    .queueName(Config.QUEUE_NAME)
    .buildClient();

下表列出了客户端类型和相应的配置名称,并指出客户端当前是否默认启用以在最新版本 7.17.0 中使用 V2-Stack。 对于默认情况下不在 V2-Stack 上的客户端,可以使用刚才显示的示例来选择加入。

客户端类型 配置名称 默认情况下是否在 V2-Stack 上?
发送方和管理客户端 com.azure.messaging.servicebus.sendAndManageRules.v2
非会话处理器和反应器接收器客户端 com.azure.messaging.servicebus.nonSession.asyncReceive.v2
会话处理器接收器客户端 com.azure.messaging.servicebus.session.processor.asyncReceive.v2
会话反应器接收器客户端 com.azure.messaging.servicebus.session.reactor.asyncReceive.v2
非会话同步接收器客户端 com.azure.messaging.servicebus.nonSession.syncReceive.v2
会话同步接收器客户端 com.azure.messaging.servicebus.session.syncReceive.v2

作为使用 com.azure.core.util.Configuration 的替代方法,可以通过使用环境变量或系统属性设置相同的配置名称来进行选择加入或选择退出。

后续步骤

如果本文中的故障排除指南无法帮助你解决使用 Azure SDK for Java 客户端库时出现的问题,建议您在 Azure SDK for Java GitHub 存储库提交问题