Ett konsultuppdrag nyligen fokuserade på att blockera problem inuti SQL Server som orsakade förseningar i behandlingen av användarförfrågningar från applikationen. När vi började gräva i de problem som upplevdes blev det tydligt att ur SQL Server-synpunkt kretsade problemet kring sessioner i en sovande status som höll lås inuti motorn. Detta är inte ett typiskt beteende för SQL Server, så min första tanke var att det fanns någon form av applikationsdesignfel som lämnade en transaktion aktiv på en session som hade återställts för anslutningspooling i applikationen, men detta visade sig snabbt inte för att vara fallet eftersom låsen senare släpptes automatiskt, var det bara en fördröjning i att detta inträffade. Så vi var tvungna att gräva vidare.
Förstå sessionsstatus
Beroende på vilken DMV du tittar på för SQL Server kan en session ha lite olika status. En Sleeping-status betyder att motorn har slutfört kommandot, allt mellan klient och server har slutförts interaktionsmässigt och anslutningen väntar på att nästa kommando ska komma från klienten. Om den sovande sessionen har en öppen transaktion är den alltid relaterad till kod och inte SQL Server. Transaktionen som hålls öppen kan förklaras av ett par saker. Den första möjligheten är en procedur med en explicit transaktion som inte aktiverar XACT_ABORT-inställningen och sedan timeout utan att applikationen hanterar rensningen korrekt som förklaras i detta riktigt gamla inlägg av CSS-teamet:
- Hur det fungerar:Vad är en sovande/väntande kommandosession
Om proceduren hade aktiverat XACT_ABORT-inställningen skulle den ha avbrutit transaktionen automatiskt när den tog timeout och transaktionen skulle ha återställts. SQL Server gör exakt vad den krävs för att göra enligt ANSI-standarder och för att behålla ACID-egenskaperna för kommandot som kördes. Timeouten är inte SQL Server-relaterad, den ställs in av .NET-klienten och CommandTimeout-egenskapen, så det är också kodrelaterat och inte SQL Engine-relaterat beteende. Det här är samma typ av problem som jag också pratade om i min serie Extended Events, i det här blogginlägget:
- Använda flera mål för att felsöka föräldralösa transaktioner
Men i det här fallet använde inte applikationen lagrade procedurer för åtkomst till databasen, och all kod genererades av en ORM. Vid det här laget gick undersökningen bort från SQL Server och mer mot hur applikationen använde ORM och var transaktioner skulle genereras av applikationskodbasen.
Förstå .NET-transaktioner
Det är allmänt känt att SQL Server omsluter alla dataändringar i en transaktion som automatiskt genomförs om inte inställningsalternativet IMPLICIT_TRANSACTIONS är PÅ för en session. Efter att ha verifierat att detta inte var PÅ för någon del av deras kod, var det ganska säkert att anta att alla transaktioner som återstår efter att en session låg i viloläge var resultatet av att en explicit transaktion öppnades någonstans under exekveringen av deras kod. Nu var det bara en fråga om att förstå när, var och viktigast av allt, varför det inte stängdes ut omedelbart. Detta leder till ett av några olika scenarier som vi skulle behöva leta efter i deras programnivåkod:
- Applikationen använder en TransactionScope() runt en operation
- Applikationen som använder en SqlTransaction() på anslutningen
- ORM-koden som omsluter vissa samtal i en transaktion internt som inte begås
Dokumentationen för TransactionScope uteslöt ganska snabbt det som en möjlig orsak till detta. Om du misslyckas med att slutföra transaktionsomfånget kommer den automatiskt att rulla tillbaka och avbryta transaktionen när den avyttras, så det är inte särskilt troligt att detta kommer att kvarstå vid anslutningsåterställningar. På samma sätt kommer SqlTransaction-objektet automatiskt att rulla tillbaka om det inte har begåtts när anslutningen återställs för anslutningspoolning, så det blev snabbt en icke-startare för problemet. Detta lämnade precis ORM-kodgenereringen, åtminstone var det vad jag trodde, och det skulle vara otroligt konstigt för en äldre version av en mycket vanlig ORM att uppvisa den här typen av beteende från min erfarenhet, så vi var tvungna att gräva vidare.
Dokumentationen för ORM som de använder anger tydligt att när en multi-entity-åtgärd inträffar, utförs den i en transaktion. Åtgärder med flera enheter kan vara rekursiva lagringar eller att spara en entitetssamling tillbaka till databasen från applikationen, och utvecklarna var överens om att dessa typer av operationer sker över hela deras kod, så ja, ORM måste använda transaktioner, men varför gjorde de blir helt plötsligt ett problem.
Roten till problemet
Vid det här laget tog vi ett steg tillbaka och började göra en holistisk översyn av hela miljön med hjälp av New Relic och andra övervakningsverktyg som var tillgängliga när blockeringsproblemen dök upp. Det började bli tydligt att de sovande sessionerna som innehöll lås endast inträffade när IIS Application-servrarna var under extrem CPU-belastning, men att det i sig inte var tillräckligt för att ta hänsyn till eftersläpningen som sågs i transaktionsförpliktelser som släppte lås. Det visade sig också att applikationsservrarna var virtuella maskiner som kördes på en överengagerad hypervisorvärd, och de CPU Ready väntetiderna för dem var kraftigt förhöjda vid tidpunkterna för blockeringsproblemen baserat på summeringsvärdena som tillhandahålls av VM-administratören.
Sleeping-statusen kommer att inträffa med en öppen transaktion som håller lås mellan .SaveEntity-anropen för objekten som slutförs och den sista commit i den kodgenererade koden bakom objekten. Om VM/App-servern är under tryck eller belastning kan detta försenas och leda till problem med blockering, men problemet finns inte i SQL Server, den gör precis vad den ska inom ramen för transaktionen. Problemet är i slutändan resultatet av förseningen i behandlingen av commit-punkten på ansökningssidan. Att få tidpunkterna för uttalandet slutfört och RPC-slutförda händelser från Extended Events tillsammans med databas_transaction_end-händelsens timing visar fördröjningen tur och retur från appnivån som stänger transaktionen på den öppna anslutningen. I det här fallet är allt som ses i SQL Server offer för en överbelastad applikationsserver och en överbelastad VM-värd. Att flytta/dela applikationsbelastningen över servrar i en NLB- eller hårdvarubelastningsbalanserad konfiguration med värdar som inte är överbegärda på CPU-användning skulle snabbt återställa den omedelbara commit av transaktionerna och ta bort de sovande sessionerna som håller lås i SQL Server.
Ännu ett exempel på en miljöfråga som orsakar vad som såg ut som ett heltäckande blockeringsproblem. Det lönar sig alltid att undersöka varför den blockerande tråden inte kan släppa sina lås snabbt.