sql >> Base de Datos >  >> RDS >> Database

Efectos secundarios no deseados:sesiones de sueño con cerraduras

Un contrato de consultoría reciente se centró en el bloqueo de problemas dentro de SQL Server que causaban retrasos en el procesamiento de las solicitudes de los usuarios desde la aplicación. A medida que comenzamos a profundizar en los problemas experimentados, quedó claro que, desde el punto de vista de SQL Server, el problema giraba en torno a las sesiones en estado de suspensión que mantenían bloqueos dentro del motor. Este no es un comportamiento típico de SQL Server, por lo que lo primero que pensé fue que había algún tipo de falla en el diseño de la aplicación que dejaba una transacción activa en una sesión que se había restablecido para la agrupación de conexiones en la aplicación, pero rápidamente se demostró que no. Para ser el caso, dado que los bloqueos se liberaron automáticamente más tarde, solo hubo un retraso en que esto ocurriera. Entonces, tuvimos que profundizar más.

Comprensión del estado de la sesión

Según el DMV que consulte para SQL Server, una sesión puede tener algunos estados diferentes. Un estado de suspensión significa que el motor ha completado el comando, todo entre el cliente y el servidor ha completado la interacción, y la conexión está esperando el siguiente comando del cliente. Si la sesión de suspensión tiene una transacción abierta, siempre está relacionada con el código y no con SQL Server. La transacción que se mantiene abierta puede explicarse por un par de cosas. La primera posibilidad es un procedimiento con una transacción explícita que no activa la configuración XACT_ABORT y luego se agota sin que la aplicación maneje la limpieza correctamente, como se explica en esta publicación muy antigua del equipo de CSS:

  • Cómo funciona:¿Qué es una sesión de comando en espera/en espera?

Si el procedimiento hubiera habilitado la configuración XACT_ABORT, habría abortado la transacción automáticamente cuando se agotó el tiempo de espera y la transacción se habría revertido. SQL Server está haciendo exactamente lo que debe hacer según los estándares ANSI y mantener las propiedades ACID del comando que se ejecutó. El tiempo de espera no está relacionado con SQL Server, lo establece el cliente .NET y la propiedad CommandTimeout, por lo que también está relacionado con el código y no con el comportamiento del motor SQL. Este es el mismo tipo de problema del que hablé en mi serie de eventos extendidos, en esta publicación de blog:

  • Uso de múltiples objetivos para depurar transacciones huérfanas

Sin embargo, en este caso la aplicación no utilizó procedimientos almacenados para acceder a la base de datos y todo el código fue generado por un ORM. En este punto, la investigación se alejó de SQL Server y se centró más en cómo la aplicación usaba el ORM y dónde generaría las transacciones la base del código de la aplicación.

Comprensión de las transacciones .NET

Es de conocimiento general que SQL Server envuelve cualquier modificación de datos en una transacción que se confirma automáticamente a menos que la opción de configuración IMPLICIT_TRANSACTIONS esté activada para una sesión. Después de verificar que esto no estaba activado para ninguna parte de su código, era bastante seguro asumir que cualquier transacción que quedara después de que una sesión estuviera suspendida era el resultado de una transacción explícita que se abrió en algún lugar durante la ejecución de su código. Ahora solo era cuestión de entender cuándo, dónde y, lo que es más importante, por qué no se cerró de inmediato. Esto lleva a uno de los pocos escenarios diferentes que íbamos a tener que buscar dentro de su código de nivel de aplicación:

  • La aplicación que usa un TransactionScope() alrededor de una operación
  • La aplicación que incluye una SqlTransaction() en la conexión
  • El código ORM que envuelve ciertas llamadas en una transacción interna que no se está confirmando

La documentación de TransactionScope lo descartó rápidamente como una posible causa de esto. Si no puede Completar el alcance de la transacción, automáticamente retrocederá y anulará la transacción cuando se deseche, por lo que no es muy probable que esto persista a través de los restablecimientos de la conexión. Del mismo modo, el objeto SqlTransaction se revertirá automáticamente si no se confirma cuando la conexión se restablece para la agrupación de conexiones, por lo que rápidamente se convirtió en un punto muerto para el problema. Esto solo dejó la generación de código ORM, al menos eso fue lo que pensé, y sería increíblemente extraño que una versión anterior de un ORM muy común exhibiera este tipo de comportamiento según mi experiencia, por lo que tuvimos que profundizar más.

La documentación para el ORM que están utilizando establece claramente que cuando se produce una acción de varias entidades, se realiza dentro de una transacción. Las acciones de múltiples entidades podrían ser guardados recursivos o guardar una colección de entidades en la base de datos desde la aplicación, y los desarrolladores acordaron que este tipo de operaciones ocurren en todo su código, así que sí, el ORM debe estar usando transacciones, pero ¿por qué? de repente convertirse en un problema.

La raíz del problema

En este punto, dimos un paso atrás y comenzamos a hacer una revisión holística de todo el entorno utilizando New Relic y otras herramientas de monitoreo que estaban disponibles cuando aparecían los problemas de bloqueo. Empezó a quedar claro que las sesiones de suspensión que retenían bloqueos solo ocurrían cuando los servidores de aplicaciones de IIS estaban bajo una carga de CPU extrema, pero eso por sí solo no era suficiente para explicar el retraso que se observaba en las confirmaciones de transacciones que liberan bloqueos. También resultó que los servidores de aplicaciones eran máquinas virtuales que se ejecutaban en un host de hipervisor sobrecomprometido, y los tiempos de espera de CPU Ready para ellos eran muy elevados en los momentos de los problemas de bloqueo según los valores de suma proporcionados por el administrador de VM.

El estado de suspensión ocurrirá con una transacción abierta que mantiene bloqueos entre las llamadas .SaveEntity de los objetos que se completan y la confirmación final en el código generado detrás de los objetos. Si el servidor de VM/aplicaciones está bajo presión o carga, esto podría retrasarse y generar problemas con el bloqueo, pero el problema no está en SQL Server, está haciendo exactamente lo que debería dentro del alcance de la transacción. El problema es, en última instancia, el resultado de la demora en el procesamiento del punto de confirmación del lado de la aplicación. Obtener los tiempos de la declaración completada y los eventos RPC completados de Extended Events junto con el tiempo del evento database_transaction_end muestra el retraso de ida y vuelta desde el nivel de la aplicación que cierra la transacción en la conexión abierta. En este caso, todo lo que se ve en SQL Server es víctima de un servidor de aplicaciones sobrecargado y un host de VM sobrecargado. Mover/dividir la carga de la aplicación entre servidores en una configuración equilibrada de carga de hardware o NLB utilizando hosts que no están sobrecomprometidos en el uso de la CPU restauraría rápidamente la confirmación inmediata de las transacciones y eliminaría las sesiones de suspensión que mantienen bloqueos en SQL Server.

Otro ejemplo más de un problema ambiental que causa lo que parecía un problema de bloqueo común y corriente. Siempre vale la pena investigar por qué el hilo de bloqueo no puede liberar sus bloqueos rápidamente.