Un récent engagement de conseil s'est concentré sur le blocage des problèmes à l'intérieur de SQL Server qui entraînaient des retards dans le traitement des demandes des utilisateurs à partir de l'application. Au fur et à mesure que nous commencions à creuser les problèmes rencontrés, il est devenu clair que du point de vue de SQL Server, le problème tournait autour des sessions en état de veille qui détenaient des verrous à l'intérieur du moteur. Ce n'est pas un comportement typique pour SQL Server, donc ma première pensée a été qu'il y avait une sorte de défaut de conception d'application qui laissait une transaction active sur une session qui avait été réinitialisée pour le regroupement de connexions dans l'application, mais cela s'est rapidement avéré non pour être le cas puisque les verrous ont ensuite été libérés automatiquement, il n'y a eu qu'un retard. Nous avons donc dû creuser davantage.
Comprendre le statut de la session
Selon le DMV que vous consultez pour SQL Server, une session peut avoir plusieurs statuts différents. Un état de veille signifie que le moteur a terminé la commande, que tout entre le client et le serveur a terminé l'interaction et que la connexion attend la prochaine commande provenant du client. Si la session en veille a une transaction ouverte, elle est toujours liée au code et non à SQL Server. La transaction maintenue ouverte peut s'expliquer par plusieurs choses. La première possibilité est une procédure avec une transaction explicite qui n'active pas le paramètre XACT_ABORT, puis expire sans que l'application ne gère correctement le nettoyage, comme expliqué dans ce très ancien article de l'équipe CSS :
- Comment ça marche ? Qu'est-ce qu'une session de commande en attente ou en veille ?
Si la procédure avait activé le paramètre XACT_ABORT, elle aurait automatiquement abandonné la transaction lorsqu'elle a expiré et la transaction aurait été annulée. SQL Server fait exactement ce qu'il doit faire selon les normes ANSI et pour maintenir les propriétés ACID de la commande qui a été exécutée. Le délai d'attente n'est pas lié à SQL Server, il est défini par le client .NET et la propriété CommandTimeout, de sorte qu'il est également lié au code et non au comportement lié au moteur SQL. C'est le même type de problème dont j'ai également parlé dans ma série d'événements étendus, dans cet article de blog :
- Utilisation de plusieurs cibles pour déboguer les transactions orphelines
Cependant, dans ce cas, l'application n'a pas utilisé de procédures stockées pour accéder à la base de données et tout le code a été généré par un ORM. À ce stade, l'enquête s'est éloignée de SQL Server et s'est davantage intéressée à la manière dont l'application utilisait l'ORM et où les transactions seraient générées par la base de code de l'application.
Comprendre les transactions .NET
Il est de notoriété publique que SQL Server encapsule toute modification de données dans une transaction qui est automatiquement validée, sauf si l'option définie IMPLICIT_TRANSACTIONS est activée pour une session. Après avoir vérifié que ce n'était pas activé pour aucune partie de leur code, il était assez sûr de supposer que toutes les transactions restantes après qu'une session était en veille étaient le résultat d'une transaction explicite ouverte quelque part pendant l'exécution de leur code. Maintenant, c'était juste une question de comprendre quand, où et surtout, pourquoi il n'était pas fermé immédiatement. Cela conduit à l'un des quelques scénarios différents que nous allions devoir rechercher à l'intérieur de leur code de niveau d'application :
- L'application utilisant un TransactionScope() autour d'une opération
- L'application inscrivant une SqlTransaction() sur la connexion
- Le code ORM encapsulant certains appels dans une transaction en interne qui n'est pas validée
La documentation de TransactionScope a assez rapidement exclu cela comme une cause possible de cela. Si vous ne parvenez pas à terminer la portée de la transaction, elle annulera automatiquement et abandonnera la transaction lors de sa suppression, il est donc peu probable que cela persiste lors des réinitialisations de connexion. De même, l'objet SqlTransaction sera automatiquement annulé s'il n'est pas validé lorsque la connexion est réinitialisée pour le regroupement de connexions, ce qui est rapidement devenu un non-démarreur du problème. Cela vient de quitter la génération de code ORM, du moins c'est ce que je pensais, et il serait incroyablement étrange qu'une version plus ancienne d'un ORM très courant présente ce type de comportement d'après mon expérience, nous avons donc dû creuser davantage.
La documentation de l'ORM qu'ils utilisent indique clairement que lorsqu'une action multi-entités se produit, elle est effectuée à l'intérieur d'une transaction. Les actions multi-entités peuvent être des sauvegardes récursives ou la sauvegarde d'une collection d'entités dans la base de données à partir de l'application, et les développeurs ont convenu que ces types d'opérations se produisent partout dans leur code, donc oui, l'ORM doit utiliser des transactions, mais pourquoi étaient-ils devient tout d'un coup un problème.
La racine du problème
À ce stade, nous avons pris du recul et avons commencé à faire un examen holistique de l'ensemble de l'environnement en utilisant New Relic et d'autres outils de surveillance disponibles lorsque les problèmes de blocage se sont manifestés. Il a commencé à devenir clair que les sessions de sommeil détenant des verrous ne se produisaient que lorsque les serveurs d'application IIS étaient soumis à une charge CPU extrême, mais cela ne suffisait pas à lui seul à tenir compte du décalage observé dans les validations de transaction libérant des verrous. Il s'est également avéré que les serveurs d'applications étaient des machines virtuelles s'exécutant sur un hôte hyperviseur surchargé, et les temps d'attente CPU Ready pour eux étaient considérablement élevés au moment des problèmes de blocage en fonction des valeurs de sommation fournies par l'administrateur de la machine virtuelle.
Le statut Sleeping va se produire avec une transaction ouverte détenant des verrous entre les appels .SaveEntity des objets se terminant et la validation finale dans le code généré derrière pour les objets. Si le serveur VM/App est sous pression ou chargé, cela peut être retardé et entraîner des problèmes de blocage, mais le problème n'est pas dans SQL Server, il fait exactement ce qu'il doit faire dans le cadre de la transaction. Le problème est finalement le résultat du retard dans le traitement du point de validation côté application. L'obtention des minutages de l'instruction terminée et des événements RPC terminés à partir des événements étendus avec le minutage de l'événement database_transaction_end montre le délai aller-retour à partir du niveau d'application fermant la transaction sur la connexion ouverte. Dans ce cas, tout ce qui est vu dans SQL Server est victime d'un serveur d'applications surchargé et d'un hôte VM surchargé. Déplacer/diviser la charge de l'application sur les serveurs dans une configuration NLB ou à charge matérielle équilibrée à l'aide d'hôtes qui ne sont pas surchargés d'utilisation du processeur restaurerait rapidement la validation immédiate des transactions et supprimerait les sessions en veille détenant des verrous dans SQL Server.
Encore un autre exemple d'un problème environnemental causant ce qui ressemblait à un problème de blocage banal. Il est toujours avantageux de rechercher pourquoi le thread bloquant n'est pas en mesure de libérer ses verrous rapidement.