Um recente compromisso de consultoria concentrou-se no bloqueio de problemas dentro do SQL Server que estavam causando atrasos no processamento de solicitações de usuários do aplicativo. À medida que começamos a investigar os problemas que estavam ocorrendo, ficou claro que, do ponto de vista do SQL Server, o problema girava em torno de sessões em um status de suspensão que mantinham bloqueios dentro do mecanismo. Este não é um comportamento típico para o SQL Server, então meu primeiro pensamento foi que havia algum tipo de falha de design de aplicativo que estava deixando uma transação ativa em uma sessão que havia sido redefinida para pool de conexão no aplicativo, mas isso foi rapidamente comprovado que não para ser o caso, uma vez que os bloqueios foram posteriormente liberados automaticamente, houve apenas um atraso para que isso ocorresse. Então, tivemos que cavar mais.
Compreendendo o status da sessão
Dependendo de qual DMV você procura para o SQL Server, uma sessão pode ter alguns status diferentes. Um status Adormecido significa que o mecanismo concluiu o comando, tudo entre cliente e servidor foi concluído em termos de interação e a conexão está aguardando o próximo comando vir do cliente. Se a sessão adormecida tiver uma transação aberta, ela sempre estará relacionada ao código e não ao SQL Server. A transação mantida aberta pode ser explicada por algumas coisas. A primeira possibilidade é um procedimento com uma transação explícita que não ativa a configuração XACT_ABORT e, em seguida, expira sem que o aplicativo manipule a limpeza corretamente, conforme explicado neste post muito antigo da equipe de CSS:
- Como funciona:o que é uma sessão de comando adormecida/aguardando
Se o procedimento tivesse habilitado a configuração XACT_ABORT, ele teria abortado a transação automaticamente quando o tempo limite expirasse e a transação teria sido revertida. O SQL Server está fazendo exatamente o que é necessário fazer de acordo com os padrões ANSI e para manter as propriedades ACID do comando que foi executado. O tempo limite não está relacionado ao SQL Server, ele é definido pelo cliente .NET e pela propriedade CommandTimeout, de modo que também está relacionado ao código e não ao comportamento relacionado ao SQL Engine. Este é o mesmo tipo de problema que eu falei na minha série Extended Events, nesta postagem do blog:
- Usando vários destinos para depurar transações órfãs
Porém, neste caso a aplicação não utilizou stored procedures para acesso ao banco de dados, e todo o código foi gerado por um ORM. Neste ponto, a investigação mudou do SQL Server e mais para como o aplicativo usava o ORM e onde as transações seriam geradas pela base de código do aplicativo.
Compreendendo as transações .NET
É de conhecimento geral que o SQL Server encapsula qualquer modificação de dados em uma transação que é confirmada automaticamente, a menos que a opção de conjunto IMPLICIT_TRANSACTIONS esteja ATIVADA para uma sessão. Depois de verificar que isso não estava ativado para nenhuma parte de seu código, era bastante seguro assumir que qualquer transação restante após uma sessão estar dormindo era o resultado de uma transação explícita sendo aberta em algum lugar durante a execução de seu código. Agora era apenas uma questão de entender quando, onde e, mais importante, por que não estava sendo fechado imediatamente. Isso leva a um dos poucos cenários diferentes que teríamos que procurar dentro do código da camada do aplicativo:
- O aplicativo usando um TransactionScope() em torno de uma operação
- O aplicativo que inscreve um SqlTransaction() na conexão
- O código ORM que envolve determinadas chamadas em uma transação internamente que não está sendo confirmada
A documentação do TransactionScope rapidamente descartou isso como uma possível causa disso. Se você não concluir o escopo da transação, ele reverterá automaticamente e abortará a transação quando for descartada, portanto, não é muito provável que isso persista nas redefinições de conexão. Da mesma forma, o objeto SqlTransaction reverterá automaticamente se não for confirmado quando a conexão for redefinida para o pool de conexões, de modo que rapidamente se tornou um não inicial para o problema. Isso acabou de deixar a geração de código ORM, pelo menos foi o que eu pensei, e seria incrivelmente estranho para uma versão mais antiga de um ORM muito comum exibir esse tipo de comportamento da minha experiência, então tivemos que nos aprofundar mais.
A documentação do ORM que eles estão usando afirma claramente que, quando ocorre qualquer ação de várias entidades, ela é executada dentro de uma transação. As ações de várias entidades podem ser salvas recursivas ou salvar uma coleção de entidades de volta ao banco de dados do aplicativo, e os desenvolvedores concordaram que esses tipos de operações acontecem em todo o código, então sim, o ORM deve estar usando transações, mas por que eles foram? de repente se tornando um problema.
A raiz do problema
Nesse ponto, demos um passo para trás e começamos a fazer uma revisão holística de todo o ambiente, utilizando o New Relic e outras ferramentas de monitoramento que estavam disponíveis quando os problemas de bloqueio estavam aparecendo. Começou a ficar claro que as sessões adormecidas com bloqueios ocorriam apenas quando os servidores de aplicativos IIS estavam sob carga extrema da CPU, mas isso por si só não era suficiente para explicar o atraso que estava sendo visto nas confirmações de transações liberando bloqueios. Descobriu-se também que os servidores de aplicativos eram máquinas virtuais executadas em um host hipervisor sobrecarregado, e os tempos de espera de CPU Ready para eles eram severamente elevados nos momentos dos problemas de bloqueio com base nos valores de soma fornecidos pelo administrador da VM.
O status Adormecido ocorrerá com uma transação aberta mantendo bloqueios entre as chamadas .SaveEntity dos objetos sendo concluídas e a confirmação final no código gerado por trás do código para os objetos. Se o servidor VM/App estiver sob pressão ou carga, isso pode atrasar e levar a problemas de bloqueio, mas o problema não está no SQL Server, ele está fazendo exatamente o que deveria dentro do escopo da transação. O problema é, em última análise, o resultado do atraso no processamento do ponto de confirmação do lado do aplicativo. Obter os tempos da instrução concluída e eventos RPC concluídos de Eventos Estendidos junto com o tempo do evento database_transaction_end mostra o atraso de ida e volta da camada do aplicativo fechando a transação na conexão aberta. Nesse caso, tudo o que está sendo visto no SQL Server é vítima de um servidor de aplicativos sobrecarregado e de um host de VM sobrecarregado. Mover/dividir a carga do aplicativo entre servidores em uma configuração de balanceamento de carga de hardware ou NLB usando hosts que não estão sobrecarregados no uso da CPU restauraria rapidamente a confirmação imediata das transações e removeria as sessões de suspensão que mantêm bloqueios no SQL Server.
Mais um exemplo de um problema ambiental causando o que parecia ser um problema de bloqueio comum. Sempre vale a pena investigar por que o thread de bloqueio não consegue liberar seus bloqueios rapidamente.