Nedávná konzultace byla zaměřena na blokování problémů uvnitř SQL Serveru, které způsobovaly zpoždění při zpracování požadavků uživatelů z aplikace. Když jsme se začali zabývat problémy, které se objevily, bylo jasné, že z hlediska SQL Serveru se problém točil kolem relací ve stavu Sleeping, které udržovaly zámky uvnitř enginu. Toto není typické chování pro SQL Server, takže moje první myšlenka byla, že došlo k nějaké chybě v návrhu aplikace, která nechávala transakci aktivní v relaci, která byla resetována pro sdružování připojení v aplikaci, ale rychle se ukázalo, že ne. protože zámky byly později automaticky uvolněny, došlo pouze ke zpoždění. Takže jsme se museli ponořit dál.
Porozumění stavu relace
V závislosti na tom, na který DMV se díváte pro SQL Server, může mít relace několik různých stavů. Stav spánku znamená, že stroj dokončil příkaz, vše mezi klientem a serverem dokončilo interakci a připojení čeká na další příkaz od klienta. Pokud má spící relace otevřenou transakci, vždy souvisí s kódem a ne s SQL Serverem. Transakci, která je otevřená, lze vysvětlit několika věcmi. První možností je procedura s explicitní transakcí, která nezapne nastavení XACT_ABORT a poté vyprší časový limit, aniž by aplikace správně provedla vyčištění, jak je vysvětleno v tomto opravdu starém příspěvku týmu CSS:
- Jak to funguje:Co je to spící / čekající příkazová relace
Pokud by procedura povolila nastavení XACT_ABORT, transakce by se po vypršení časového limitu automaticky přerušila a transakce by se vrátila zpět. SQL Server dělá přesně to, co je požadováno podle standardů ANSI a udržuje vlastnosti ACID příkazu, který byl proveden. Časový limit nesouvisí s SQL Serverem, je nastaven klientem .NET a vlastností CommandTimeout, takže také souvisí s kódem a ne s chováním SQL Engine. Toto je stejný druh problému, o kterém jsem mluvil ve své sérii Extended Events, v tomto příspěvku na blogu:
- Použití více cílů k ladění osamocených transakcí
V tomto případě však aplikace nepoužila uložené procedury pro přístup k databázi a veškerý kód vygeneroval ORM. V tomto bodě se vyšetřování přesunulo od SQL Serveru a více k tomu, jak aplikace používala ORM a kde by byly transakce generovány základnou kódu aplikace.
Porozumění transakcím .NET
Je všeobecně známo, že SQL Server zabalí jakoukoli změnu dat do transakce, která je automaticky potvrzena, pokud není pro relaci nastavena možnost IMPLICIT_TRANSACTIONS ON. Po ověření, že to nebylo zapnuto pro žádnou část jejich kódu, bylo docela bezpečné předpokládat, že všechny transakce zbývající po relace ve spánku byly výsledkem explicitní transakce, která byla někde otevřena během provádění jejich kódu. Teď šlo jen o to pochopit, kdy, kde a co je nejdůležitější, proč to nebylo okamžitě uzavřeno. To vede k jednomu z několika různých scénářů, které jsme museli hledat v kódu aplikační vrstvy:
- Aplikace využívající TransactionScope() kolem operace
- Aplikace zařazující SqlTransaction() do připojení
- Kód ORM, který interně zabalí určitá volání do transakce, která není potvrzena
Dokumentace pro TransactionScope to docela rychle vyloučila jako možnou příčinu. Pokud se vám nepodaří dokončit rozsah transakce, automaticky se vrátí zpět a přeruší transakci, když se zlikviduje, takže není příliš pravděpodobné, že by to přetrvávalo i přes resetování připojení. Podobně se objekt SqlTransaction automaticky vrátí zpět, pokud není potvrzen, když je připojení resetováno pro sdružování připojení, takže se rychle stalo nestartujícím problémem. Toto právě opustilo generování kódu ORM, alespoň jsem si to myslel, a bylo by neuvěřitelně zvláštní, aby starší verze velmi běžného ORM vykazovala tento typ chování podle mých zkušeností, takže jsme museli zapátrat dále.
Dokumentace pro ORM, který používají, jasně uvádí, že když dojde k jakékoli akci s více entitami, je provedena uvnitř transakce. Akce s více entitami by mohly být rekurzivní ukládání nebo ukládání kolekce entit zpět do databáze z aplikace a vývojáři se shodli, že tyto typy operací probíhají v celém jejich kódu, takže ano, ORM musí používat transakce, ale proč byly najednou problém.
Kořen problému
V tomto bodě jsme udělali krok zpět a začali dělat holistický přehled celého prostředí s využitím New Relic a dalších monitorovacích nástrojů, které byly k dispozici, když se objevily problémy s blokováním. Začalo být jasné, že spící relace držící zámky se vyskytovaly pouze tehdy, když byly aplikační servery IIS pod extrémní zátěží CPU, ale to samo o sobě nestačilo k vysvětlení zpoždění, které bylo pozorováno u uvolnění zámků provedených transakcemi. Ukázalo se také, že aplikační servery byly virtuální stroje běžící na přetíženém hostiteli hypervizoru a čekací doby CPU Ready pro ně byly v době problémů s blokováním značně prodlouženy na základě hodnot součtu poskytnutých administrátorem VM.
Stav spánku nastane s otevřenou transakcí, která drží zámky mezi voláním .SaveEntity dokončením objektů a konečným potvrzením v kódu generovaném za objekty. Pokud je server VM/App pod tlakem nebo zatížením, může se to zpozdit a vést k problémům s blokováním, ale problém není v SQL Serveru, ten dělá přesně to, co má v rámci transakce. Problém je nakonec výsledkem zpoždění při zpracování bodu potvrzení na straně aplikace. Získání časování dokončení příkazu a dokončených událostí RPC z Extended Events spolu s časováním události database_transaction_end ukazuje zpoždění zpáteční cesty od úrovně aplikace, která uzavírá transakci na otevřeném připojení. V tomto případě je vše, co je vidět na serveru SQL, obětí přetíženého aplikačního serveru a přetíženého hostitele virtuálního počítače. Přesunutí/rozdělení zatížení aplikace mezi servery v konfiguraci NLB nebo hardwarové zátěži pomocí hostitelů, kteří nejsou příliš zavázáni na využití CPU, by rychle obnovilo okamžité potvrzení transakcí a odstranilo spící relace držící zámky na serveru SQL.
Ještě jeden příklad problému životního prostředí způsobujícího to, co vypadalo jako problém s běžným blokováním. Vždy se vyplatí prozkoumat, proč blokující vlákno není schopno rychle uvolnit své zámky.