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 文件

以下配置文件将 TRACE 级别输出从 Proton-J 记录到文件 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)将预提取值设置为正整数来启用 Prefetch 功能。 启用预提取功能后,客户端将从服务总线实体(队列或主题)检索等于预提取的消息数,并将其存储在内存中预提取缓冲区中。 消息会一直保留在预提取缓冲区中,直到被应用程序接收。 当消息在预提取缓冲区中时,客户端不会延长其锁定。 如果应用程序处理时间过长,导致消息锁在预提取缓冲区中过期,则应用程序可能会获取具有过期锁的消息。 有关详细信息,请参阅为什么预提取不是默认选项?

  • 主机环境偶尔会出现网络问题,例如暂时性网络故障或中断,导致锁续订任务无法按时完成。

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

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

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

    • 应用程序运行了太多共享同一连接的接收器客户端。 有关详细信息,请参阅 连接共享瓶颈 部分。
    • 应用程序已将 ServiceBusReceiverClient.receiveMessagesServiceBusProcessorClient 配置为具有较大 maxMessagesmaxConcurrentCalls 的值。 有关详细信息,请参阅 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 存储库提交问题