Ein kürzlich durchgeführter Beratungsauftrag konzentrierte sich auf das Blockieren von Problemen innerhalb von SQL Server, die zu Verzögerungen bei der Verarbeitung von Benutzeranfragen aus der Anwendung führten. Als wir begannen, uns mit den aufgetretenen Problemen zu befassen, wurde klar, dass sich das Problem aus Sicht von SQL Server um Sitzungen im Ruhezustand drehte, die Sperren innerhalb der Engine aufrechterhielten. Dies ist kein typisches Verhalten für SQL Server, daher war mein erster Gedanke, dass es einen Fehler im Anwendungsdesign gab, der eine Transaktion in einer Sitzung aktiv ließ, die für das Verbindungspooling in der Anwendung zurückgesetzt worden war, aber das wurde schnell bewiesen Da die Sperren später automatisch freigegeben wurden, kam es nur zu einer zeitlichen Verzögerung. Also mussten wir weiter nachforschen.
Sitzungsstatus verstehen
Je nachdem, welche DMV Sie sich für SQL Server ansehen, kann eine Sitzung einige unterschiedliche Status haben. Ein Schlafstatus bedeutet, dass die Engine den Befehl abgeschlossen hat, alles zwischen Client und Server in Bezug auf die Interaktion abgeschlossen ist und die Verbindung auf den nächsten Befehl wartet, der vom Client kommt. Wenn die schlafende Sitzung eine offene Transaktion hat, bezieht sie sich immer auf Code und nicht auf SQL Server. Die offen gehaltene Transaktion kann durch ein paar Dinge erklärt werden. Die erste Möglichkeit ist eine Prozedur mit einer expliziten Transaktion, die die XACT_ABORT-Einstellung nicht aktiviert und dann das Zeitlimit überschreitet, ohne dass die Anwendung die Bereinigung korrekt verarbeitet, wie in diesem wirklich alten Beitrag des CSS-Teams erklärt:
- So funktioniert es:Was ist eine schlafende/wartende Befehlssitzung
Wenn die Prozedur die XACT_ABORT-Einstellung aktiviert hätte, hätte sie die Transaktion automatisch abgebrochen, wenn sie abgelaufen wäre, und die Transaktion wäre rückgängig gemacht worden. SQL Server tut genau das, was nach ANSI-Standards erforderlich ist, und die ACID-Eigenschaften des ausgeführten Befehls beizubehalten. Das Timeout bezieht sich nicht auf SQL Server, es wird vom .NET-Client und der CommandTimeout-Eigenschaft festgelegt, sodass es sich auch um ein codebezogenes und nicht um ein SQL Engine-bezogenes Verhalten handelt. Dies ist die gleiche Art von Problem, über die ich auch in meiner Serie „Extended Events“ in diesem Blogbeitrag gesprochen habe:
- Verwenden mehrerer Ziele zum Debuggen verwaister Transaktionen
In diesem Fall verwendete die Anwendung jedoch keine gespeicherten Prozeduren für den Zugriff auf die Datenbank, und der gesamte Code wurde von einem ORM generiert. An diesem Punkt verlagerte sich die Untersuchung weg von SQL Server und mehr darauf, wie die Anwendung das ORM verwendet und wo Transaktionen von der Anwendungscodebasis generiert würden.
Verstehen von .NET-Transaktionen
Es ist allgemein bekannt, dass SQL Server jede Datenänderung in eine Transaktion einschließt, die automatisch festgeschrieben wird, es sei denn, die Option IMPLICIT_TRANSACTIONS set ist für eine Sitzung aktiviert. Nachdem überprüft wurde, dass dies für keinen Teil ihres Codes aktiviert war, war es ziemlich sicher anzunehmen, dass alle Transaktionen, die nach einer Sitzung im Ruhezustand verbleiben, das Ergebnis einer expliziten Transaktion waren, die irgendwo während der Ausführung ihres Codes geöffnet wurde. Jetzt ging es nur noch darum zu verstehen, wann, wo und vor allem warum nicht sofort geschlossen wurde. Dies führt zu einem der wenigen unterschiedlichen Szenarien, nach denen wir im Code ihrer Anwendungsebene suchen mussten:
- Die Anwendung, die ein TransactionScope() um eine Operation verwendet
- Die Anwendung, die eine SqlTransaction() für die Verbindung einträgt
- Der ORM-Code, der bestimmte Aufrufe intern in eine Transaktion einschließt, die nicht festgeschrieben wird
Die Dokumentation für TransactionScope schloss das ziemlich schnell als mögliche Ursache dafür aus. Wenn Sie den Transaktionsbereich nicht abschließen, wird die Transaktion automatisch zurückgesetzt und abgebrochen, wenn sie verworfen wird. Daher ist es nicht sehr wahrscheinlich, dass dies über das Zurücksetzen der Verbindung hinweg bestehen bleibt. Ebenso wird das SqlTransaction-Objekt automatisch zurückgesetzt, wenn es nicht festgeschrieben wird, wenn die Verbindung für das Verbindungspooling zurückgesetzt wird, sodass es schnell zu einem Nichtstarter für das Problem wurde. Damit blieb nur die ORM-Codegenerierung übrig, zumindest dachte ich das, und es wäre meiner Erfahrung nach unglaublich seltsam, wenn eine ältere Version eines sehr verbreiteten ORM diese Art von Verhalten zeigen würde, also mussten wir uns weiter vertiefen.
Die Dokumentation für das von ihnen verwendete ORM besagt eindeutig, dass jede Aktion mit mehreren Entitäten innerhalb einer Transaktion ausgeführt wird. Multi-Entity-Aktionen könnten rekursive Speicherungen oder das Zurückspeichern einer Entity-Sammlung aus der Anwendung in die Datenbank sein, und die Entwickler waren sich einig, dass diese Art von Operationen im gesamten Code stattfinden, also ja, das ORM muss Transaktionen verwenden, aber warum sollten sie plötzlich zum Problem.
Die Wurzel des Problems
An diesem Punkt traten wir einen Schritt zurück und begannen mit einer ganzheitlichen Überprüfung der gesamten Umgebung unter Verwendung von New Relic und anderen Überwachungstools, die verfügbar waren, als die Blockierungsprobleme auftauchten. Es wurde deutlich, dass die Sperren im Ruhezustand nur dann auftraten, wenn die IIS-Anwendungsserver unter extremer CPU-Last standen, aber das allein reichte nicht aus, um die Verzögerung zu erklären, die bei Transaktions-Commits beim Freigeben von Sperren zu beobachten war. Es stellte sich auch heraus, dass die Anwendungsserver virtuelle Maschinen waren, die auf einem überlasteten Hypervisor-Host ausgeführt wurden, und die CPU-Bereitschaftswartezeiten für sie waren zu den Zeiten der Blockierungsprobleme stark erhöht, basierend auf den vom VM-Administrator bereitgestellten Summenwerten.
Der Sleeping-Status tritt mit einer offenen Transaktion auf, die Sperren zwischen den .SaveEntity-Aufrufen der abgeschlossenen Objekte und der endgültigen Festschreibung im Code erzeugt, der Code Behind für die Objekte hält. Wenn der VM/App-Server unter Druck oder Last steht, kann sich dies verzögern und zu Blockierungsproblemen führen, aber das Problem liegt nicht in SQL Server, er tut genau das, was er im Rahmen der Transaktion tun sollte. Das Problem ist letztendlich das Ergebnis der Verzögerung bei der Verarbeitung des anwendungsseitigen Festschreibungspunkts. Das Abrufen der Zeitpunkte der Ereignisse „Anweisung abgeschlossen“ und „RPC abgeschlossen“ aus erweiterten Ereignissen zusammen mit dem Zeitpunkt des Ereignisses „database_transaction_end“ zeigt die Roundtrip-Verzögerung von der Anwendungsschicht, die die Transaktion auf der offenen Verbindung schließt. In diesem Fall ist alles, was in SQL Server zu sehen ist, das Opfer eines überlasteten Anwendungsservers und eines überlasteten VM-Hosts. Das Verschieben/Aufteilen der Anwendungslast auf Server in einer NLB- oder Hardwarelastenausgleichskonfiguration unter Verwendung von Hosts, die bei der CPU-Auslastung nicht übermäßig festgeschrieben sind, würde das sofortige Festschreiben der Transaktionen schnell wiederherstellen und die schlafenden Sitzungen entfernen, die Sperren in SQL Server halten.
Noch ein weiteres Beispiel für ein Umweltproblem, das scheinbar ein 08/15-Verstopfungsproblem verursacht. Es lohnt sich immer zu untersuchen, warum der blockierende Thread seine Sperren nicht schnell freigeben kann.