Solucionar problemas do Barramento de Serviço do Azure

Este artigo aborda técnicas de investigação de falhas, simultaneidade e erros comuns na biblioteca de clientes Barramento de Serviço do Azure Java. Use essas diretrizes para identificar as causas raiz e aplicar etapas de mitigação para resolver Barramento de Serviço do Azure problemas mais rapidamente.

Habilitar e configurar o registro em log

O SDK do Azure para Java oferece uma história de log consistente para ajudar na solução de problemas de erros do aplicativo e para ajudar a agilizar sua resolução. Os logs que você produz capturam o fluxo de um aplicativo antes de atingir o estado do terminal para ajudar a localizar o problema raiz. Para obter diretrizes sobre o registro em log, consulte Configurar o registro em log no SDK do Azure para Java e Visão Geral da Solução de Problemas.

Além de habilitar o registro em log, definir o nível de log para VERBOSE ou DEBUG fornece insights sobre o estado da biblioteca. As seções a seguir mostram configurações de log4j2 e logback de exemplo para reduzir o excesso de mensagens quando o log detalhado está habilitado.

Configurar Log4J 2

Use as seguintes etapas para configurar o Log4J 2:

  1. Adicione as dependências em sua pom.xml usando as do exemplo de registro em log pom.xml, na seção "Dependências necessárias para Log4j2".
  2. Adicione log4j2.xml à sua pasta src/main/resources.

Configurar o logback

Use as seguintes etapas para configurar o logback:

  1. Adicione as dependências no seu pom.xml usando as do pom.xml de exemplo de logging, na seção "Dependências necessárias para o logback".
  2. Adicione logback.xml à sua pasta src/main/resources.

Habilitar o log de transporte do AMQP

Se a habilitação do log do cliente não for suficiente para diagnosticar seus problemas, será possível habilitar o log em um arquivo na biblioteca AMQP subjacente, Qpid Proton-J. O Qpid Proton-J usa java.util.logging. Você pode habilitar o registro em log criando um arquivo de configuração com o conteúdo mostrado na próxima seção. Ou defina proton.trace.level=ALL e as opções de configuração desejadas para a implementação do java.util.logging.Handler. Para obter as classes de implementação e suas opções, consulte pacote java.util.logging na documentação do SDK do Java 8.

Para rastrear os quadros de transporte AMQP, defina a variável de ambiente PN_TRACE_FRM=1.

Exemplo de arquivo logging.properties

O arquivo de configuração a seguir registra a saída de nível de rastreamento do Proton-J para o arquivo 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

Reduzir o desmatamento

Uma maneira de diminuir o registro é alterar o detalhamento. Outra opção é adicionar filtros que excluem logs de pacotes de nomes de logger como com.azure.messaging.servicebus ou com.azure.core.amqp. Para obter exemplos, consulte os arquivos XML nas seções Configurar Log4J 2 e Configurar Logback.

Quando você envia um bug, as mensagens de log das classes nos seguintes pacotes são interessantes:

  • com.azure.core.amqp.implementation
  • com.azure.core.amqp.implementation.handler
    • A única exceção é que você pode ignorar a mensagem de onDelivery em ReceiveLinkHandler.
  • com.azure.messaging.servicebus.implementation

Concorrência no ServiceBusProcessorClient

ServiceBusProcessorClient permite que você configure quantas chamadas para o manipulador de mensagens ocorrem simultaneamente. Essa configuração possibilita processar várias mensagens em paralelo. Para um(a) ServiceBusProcessorClient que consome mensagens de uma entidade sem sessão, você pode configurar a concorrência desejada usando a API maxConcurrentCalls. Para uma entidade com suporte a sessão, a concorrência desejada é maxConcurrentSessions vezes maxConcurrentCalls.

Se você observar menos chamadas simultâneas para o manipulador de mensagens do que a simultaneidade configurada, pode ser porque o pool de threads não é dimensionado adequadamente.

ServiceBusProcessorClient usa threads de daemon do pool de threads boundedElastic do Reactor para invocar o manipulador de mensagens. O número máximo de threads simultâneos nesse pool é limitado por um limite. Por padrão, esse limite é dez vezes o número de núcleos de CPU disponíveis. Para que o ServiceBusProcessorClient dê suporte efetivamente à simultaneidade desejada do aplicativo (maxConcurrentCalls ou maxConcurrentSessions vezes maxConcurrentCalls), você deve ter um valor de limite de pool boundedElastic maior do que a simultaneidade desejada. Você pode substituir o limite padrão definindo a propriedade do sistema reactor.schedulers.defaultBoundedElasticSize.

Ajuste o pool de threads e a alocação de CPU caso a caso. No entanto, quando você substituir o limite do pool, como ponto de partida, estabeleça o limite de threads simultâneos a aproximadamente 20 a 30 por núcleo de CPU. Limite a simultaneidade desejada por ServiceBusProcessorClient instância para aproximadamente 20-30. Crie o perfil e meça seu caso de uso específico e ajuste os aspectos de simultaneidade adequadamente. Para cenários de alta carga, considere a execução de várias instâncias ServiceBusProcessorClient em que cada instância é criada a partir de uma nova instância ServiceBusClientBuilder. Além disso, considere executar cada ServiceBusProcessorClient em um host dedicado , como um contêiner ou VM, para que o tempo de inatividade em um host não afete o processamento geral de mensagens.

Tenha em mente que definir um valor alto para o limite de pool em um host com poucos núcleos de CPU tem efeitos adversos. Alguns sinais de poucos recursos de CPU ou um pool com muitos threads em menos CPUs são: tempos limite frequentes, bloqueio perdido, deadlock ou taxa de transferência mais baixa. Se você estiver executando o aplicativo Java em um contêiner, use dois ou mais núcleos de vCPU. Não selecione nada menos que 1 núcleo de vCPU ao executar Java aplicativo em ambientes em contêineres. Para obter recomendações detalhadas sobre alocação de recursos, consulte Containerizar seus aplicativos Java.

Gargalo de compartilhamento de conexão

Todos os clientes criados a partir de uma instância compartilhada ServiceBusClientBuilder compartilham a mesma conexão com o namespace Barramento de Serviço.

O uso de uma conexão compartilhada permite operações de multiplexação entre clientes em uma conexão, mas o compartilhamento também pode se tornar um gargalo se houver muitos clientes ou os clientes juntos gerarem alta carga. Cada conexão tem um thread de E/S associado a ela. Quando você compartilha uma conexão, os clientes colocam seu trabalho na fila de trabalho desse thread de E/S compartilhado e o progresso de cada cliente depende da conclusão oportuna de seu trabalho na fila. O thread de E/S trata o trabalho enfileirado em série. Ou seja, se a fila de trabalho da thread de E/S de uma conexão compartilhada acabar tendo muito trabalho pendente para processar, os sintomas serão semelhantes aos de baixa utilização da CPU. Essa condição é descrita na seção anterior sobre simultaneidade, por exemplo, paralisação de clientes, tempo limite, bloqueio perdido ou lentidão no caminho de recuperação.

O SDK do Barramento de Serviço usa o reactor-executor-* padrão de nomenclatura para o thread de E/S de conexão. Quando o aplicativo experimenta o gargalo de conexão compartilhada, ele pode ser refletido no uso da CPU do thread de E/S. Além disso, no despejo de heap ou na memória ativa, o objeto ReactorDispatcher$workQueue é a fila de trabalho da thread de E/S. Uma longa fila de trabalho no instantâneo de memória durante o período de gargalo pode indicar que o thread de E/S compartilhado está sobrecarregado com trabalhos pendentes.

Portanto, se a carga da aplicação em um endpoint do Barramento de Serviço for razoavelmente alta em termos do número total de mensagens enviadas e recebidas ou do tamanho da carga útil, use uma instância separada do builder para cada cliente que você criar. Por exemplo, para cada entidade, fila ou tópico, você pode criar uma nova ServiceBusClientBuilder e construir um cliente a partir dela. No caso de uma carga extremamente alta para uma entidade específica, talvez você queira criar várias instâncias de cliente para essa entidade ou executar clientes em vários hosts - por exemplo, contêineres ou VMs - para balancear a carga.

Os clientes param quando usam o ponto de extremidade personalizado do Gateway de Aplicativo

O endereço do ponto de extremidade personalizado refere-se a um endereço de ponto de extremidade HTTPS fornecido pelo aplicativo que você pode resolver para Barramento de Serviço ou configurar para rotear o tráfego para Barramento de Serviço. Gateway de Aplicativo do Azure facilita a criação de um front-end HTTPS que encaminha o tráfego para Barramento de Serviço. Você pode configurar o SDK do Barramento de Serviço para que um aplicativo use um endereço IP de front-end do Application Gateway como ponto de extremidade personalizado para se conectar ao Barramento de Serviço.

O Gateway de Aplicativo oferece várias políticas de segurança que dão suporte a diferentes versões de protocolo TLS. Há políticas predefinidas que impõem o TLS 1.2 como a versão mínima e há políticas mais antigas que usam o TLS 1.0 como a versão mínima. Você aplica uma política TLS ao front-end HTTPS.

Atualmente, o SDK do Barramento de Serviço não reconhece determinados encerramentos remotos de conexões TCP pelo front-end do Application Gateway, que usa o TLS 1.0 como versão mínima. Por exemplo, se o front-end envia pacotes TCP FIN e ACK para fechar a conexão quando atualiza suas propriedades, o SDK não pode detectar esses pacotes. Portanto, ele não se reconecta e os clientes não podem mais enviar ou receber mensagens. Essa parada só acontece ao usar o TLS 1.0 como a versão mínima. Para atenuar esse problema, use uma política de segurança que define o TLS 1.2 ou superior como a versão mínima para o front-end do Gateway de Aplicativo.

O suporte para TLS 1.0 e 1.1 em todos os serviços Azure já está anunciado para terminar em 31 de outubro de 2024, portanto, faça a transição para o TLS 1.2.

Mensagem ou bloqueio de sessão foi perdido

Uma fila do Barramento de Serviço ou uma assinatura de tópico tem uma duração de bloqueio definida em nível de recurso. Quando o cliente receptor recebe uma mensagem do recurso, o agente do Barramento de Serviço aplica um bloqueio inicial à mensagem. O bloqueio inicial dura pelo período de tempo definido no nível do recurso. Se o bloqueio de mensagem não for renovado antes de expirar, o agente Barramento de Serviço liberará a mensagem para disponibilizá-la para outros receptores. Se o aplicativo tentar concluir ou abandonar uma mensagem após a expiração do bloqueio, a chamada à API falhará com o erro com.azure.messaging.servicebus.ServiceBusException: The lock supplied is invalid. Either the lock expired, or the message has already been removed from the queue.

O cliente do Barramento de Serviço oferece suporte à execução de uma tarefa de renovação de bloqueio em segundo plano que faz a renovação do bloqueio de mensagem continuamente a cada vez, antes que ele expire. Por padrão, a tarefa de renovação de bloqueio é executada por 5 minutos. Você pode ajustar a duração da renovação de bloqueio usando ServiceBusReceiverClientBuilder.maxAutoLockRenewDuration(Duration). Se você passar o valor Duration.ZERO, a tarefa de renovação de bloqueio será desabilitada.

A lista a seguir descreve alguns dos padrões de uso ou ambientes de host que podem levar ao erro de bloqueio perdido:

  • A tarefa de renovação de bloqueio está desabilitada e o tempo de processamento de mensagens do aplicativo excede a duração do bloqueio definida no nível de recurso.

  • O tempo de processamento de mensagens do aplicativo excede a duração da tarefa de renovação de bloqueio configurada. Observe que, se a duração da renovação do bloqueio não for definida explicitamente, ela será padrão para 5 minutos.

  • O aplicativo ativa o recurso Prefetch definindo o valor de pré-busca para um inteiro positivo usando ServiceBusReceiverClientBuilder.prefetchCount(prefetch). Quando o recurso Prefetch está habilitado, o cliente recupera o número de mensagens iguais ao pré-busca da entidade Barramento de Serviço - fila ou tópico - e as armazena no buffer de pré-busca na memória. As mensagens permanecem no buffer de pré-busca até que sejam recebidas no aplicativo. O cliente não estende o bloqueio das mensagens enquanto elas estão no buffer de pré-busca. Se o processamento da aplicação demorar tanto que os bloqueios das mensagens expirem enquanto elas ainda estiverem no buffer de pré-busca, a aplicação poderá adquirir as mensagens com o bloqueio expirado. Para obter mais informações, consulte Por que Prefetch não é a opção padrão?

  • O ambiente do host tem problemas ocasionais de rede - por exemplo, falha ou interrupção de rede transitória - que impedem que a tarefa de renovação de bloqueio renove o bloqueio a tempo.

  • O ambiente do host não tem CPUs suficientes ou sofre de escassez de ciclos de CPU periodicamente, o que atrasa a execução da tarefa de renovação do bloqueio no tempo previsto.

  • A hora do sistema de host não é precisa - por exemplo, o relógio está desajustado - atrasando a tarefa de renovação de bloqueio e impedindo que ela seja executada a tempo.

  • O thread de E/S de conexão está sobrecarregado, afetando sua capacidade de executar chamadas de rede de renovação de bloqueio a tempo. Os dois cenários a seguir podem causar esse problema:

    • O aplicativo está executando muitos clientes de recepção que compartilham a mesma conexão. Para obter mais informações, consulte a seção Gargalo de compartilhamento de conexão.
    • O aplicativo configura ServiceBusReceiverClient.receiveMessages ou ServiceBusProcessorClient para ter valores grandes de maxMessages ou maxConcurrentCalls. Para obter mais informações, consulte a seção Simultaneidade no ServiceBusProcessorClient.
  • Um padrão de aplicativo comum que aumenta a probabilidade de um erro de bloqueio perdido envolve o agendamento de tarefas de renovação de bloqueio de execução longa - por exemplo, tarefas com durações que abrangem várias horas. Conforme mencionado anteriormente, vários fatores fora do controle de um cliente do Barramento de Serviço podem interferir na renovação bem-sucedida do bloqueio, portanto, os designs de aplicativos devem evitar assumir a renovação garantida por longos períodos. Para evitar a necessidade de reprocessar operações de execução longa, considere dividir o trabalho em partes menores ou implementar a lógica de ponto de verificação idempotente.

O número de tarefas de renovação de bloqueio no cliente é igual aos valores de parâmetro maxMessages ou maxConcurrentCalls definidos para ServiceBusProcessorClient ou ServiceBusReceiverClient.receiveMessages. Um grande número de tarefas de renovação de bloqueio que efetuam várias chamadas de rede também pode ter um efeito adverso na limitação de namespace do Barramento de Serviço.

Se o host não tiver recursos suficientes, o bloqueio ainda poderá ser perdido mesmo se houver apenas algumas tarefas de renovação de bloqueio em execução. Se você estiver executando o aplicativo Java em um contêiner, use dois ou mais núcleos de vCPU. Não selecione nada menos que 1 núcleo de vCPU ao executar Java aplicativos em ambientes em contêineres. Para obter recomendações detalhadas sobre alocação de recursos, consulte Containerizar seus aplicativos Java.

As mesmas observações sobre bloqueios também são relevantes para uma fila do Barramento de Serviço ou uma assinatura de tópico que tem a sessão habilitada. Quando o cliente receptor se conecta a uma sessão no recurso, o agente aplica um bloqueio inicial à sessão. Para manter o bloqueio na sessão, a tarefa de renovação de bloqueio no cliente precisa continuar renovando o bloqueio de sessão antes de expirar. Para obter um recurso habilitado para sessão, as partições subjacentes às vezes se movem para obter o balanceamento de carga entre os nós do Barramento de Serviço, por exemplo, quando novos nós são adicionados para compartilhar a carga. Quando isso acontece, os bloqueios de sessão podem ser perdidos. Se o aplicativo tentar concluir ou abandonar uma mensagem depois que o bloqueio da sessão for perdido, a chamada à API falhará com o erro com.azure.messaging.servicebus.ServiceBusException: The session lock was lost. Request a new session receiver.

Próximas etapas

Se as diretrizes de solução de problemas neste artigo não ajudarem a resolver problemas ao usar o SDK do Azure para bibliotecas de cliente Java, registre um problema no SDK do Azure para Java GitHub repositório.