Скорошен консултантски ангажимент беше фокусиран върху блокирането на проблеми в SQL Server, които причиняваха забавяне при обработката на потребителски заявки от приложението. Когато започнахме да ровим в проблемите, с които се сблъскахме, стана ясно, че от гледна точка на SQL Server проблемът се въртеше около сесиите в състояние на заспиване, които държаха ключалки вътре в двигателя. Това не е типично поведение за SQL Server, така че първата ми мисъл беше, че има някакъв дефект в дизайна на приложението, който оставя транзакция активна в сесия, която е била нулирана за обединяване на връзки в приложението, но това бързо се оказа, че не за да е така, тъй като ключалките по-късно се освобождават автоматично, това се случва със закъснение. Така че трябваше да копаем допълнително.
Разбиране на състоянието на сесията
В зависимост от това кой DMV разглеждате за SQL Server, сесията може да има няколко различни състояния. Състоянието на заспиване означава, че двигателят е изпълнил командата, всичко между клиент и сървър е завършило взаимодействието и връзката чака следващата команда да дойде от клиента. Ако спящата сесия има отворена транзакция, тя винаги е свързана с код, а не с SQL Server. Транзакцията, която се държи отворена, може да се обясни с няколко неща. Първата възможност е процедура с изрична транзакция, която не включва настройката XACT_ABORT и след това изтече без почистването на приложението да обработва правилно, както е обяснено в тази наистина стара публикация от екипа на CSS:
- Как работи:Какво е спяща/изчакваща командна сесия
Ако процедурата беше активирала настройката XACT_ABORT, тогава тя щеше да прекъсне транзакцията автоматично, когато изтече времето за изчакване и транзакцията щеше да се върне обратно. SQL Server прави точно това, което се изисква според ANSI стандартите и да поддържа свойствата на ACID на командата, която е била изпълнена. Времето за изчакване не е свързано с SQL Server, то се задава от .NET клиента и свойството CommandTimeout, така че това също е свързано с кода, а не с поведението, свързано с SQL Engine. Това е същият проблем, за който говорих и в моята серия от разширени събития, в тази публикация в блога:
- Използване на множество цели за отстраняване на грешки в осиротели транзакции
В този случай обаче приложението не е използвало съхранени процедури за достъп до базата данни и целият код е генериран от ORM. В този момент разследването се измести от SQL Server и повече към това как приложението използва ORM и къде транзакциите ще бъдат генерирани от кодовата база на приложението.
Разбиране на .NET транзакциите
Общоизвестно е, че SQL Server обвива всяка промяна на данни в транзакция, която се извършва автоматично, освен ако опцията за набор IMPLICIT_TRANSACTIONS не е ON за сесия. След като се увери, че това не е ВКЛЮЧЕНО за която и да е част от техния код, беше доста безопасно да се предположи, че всички транзакции, останали след сесията е в спящо състояние, са резултат от изрична транзакция, отворена някъде по време на изпълнението на техния код. Сега беше само въпрос на разбиране кога, къде и най-важното, защо не беше закрит незабавно. Това води до един от няколкото различни сценария, които трябваше да търсим вътре в техния код на ниво приложение:
- Приложението, използващо TransactionScope() около операция
- Приложението, включващо SqlTransaction() за връзката
- ORM кодът, който обвива определени обаждания в транзакция вътрешно, която не е ангажирана
Документацията за TransactionScope доста бързо изключи това като възможна причина за това. Ако не успеете да завършите обхвата на транзакцията, той автоматично ще се върне назад и ще прекрати транзакцията, когато се изхвърли, така че не е много вероятно това да продължи при нулирането на връзката. По същия начин, обектът SqlTransaction автоматично ще се върне назад, ако не бъде ангажиран, когато връзката се нулира за обединяване на връзки, така че бързо се превърна в нестартер за проблема. Това просто остави генерирането на ORM код, поне така си мислех, и би било невероятно странно по-стара версия на много често срещан ORM да проявява този тип поведение от моя опит, така че трябваше да се задълбочим допълнително.
Документацията за ORM, която използват, ясно посочва, че когато възникне някакво действие с множество обекти, то се извършва вътре в транзакция. Действията с множество обекти могат да бъдат рекурсивни записвания или запазване на колекция от обекти обратно в базата данни от приложението и разработчиците се съгласиха, че тези видове операции се случват в целия им код, така че да, ORM трябва да използва транзакции, но защо са били те изведнъж се превръща в проблем.
Коренът на проблема
В този момент направихме крачка назад и започнахме да правим цялостен преглед на цялата среда, използвайки New Relic и други инструменти за наблюдение, които бяха налични, когато се появиха проблемите с блокирането. Започна да става ясно, че спящите сесии, задържащи заключвания, се появяват само когато сървърите на приложения на IIS са били под екстремно натоварване на процесора, но това само по себе си не е достатъчно, за да отчете забавянето, което се наблюдава при освобождаване на заключванията на транзакции. Оказа се също, че сървърите на приложения са виртуални машини, работещи на свръхангажиран хост на хипервизор, а времето за изчакване на CPU Ready за тях е силно увеличено по време на проблемите с блокирането въз основа на сумираните стойности, предоставени от администратора на VM.
Състоянието на заспиване ще възникне с отворена транзакция, задържаща заключвания между извикванията .SaveEntity на завършването на обектите и окончателния комит в кода, генериран отзад за обектите. Ако сървърът на VM/App е под натиск или натоварване, това може да се забави и да доведе до проблеми с блокирането, но проблемът не е в SQL Server, той прави точно това, което трябва в рамките на транзакцията. Проблемът в крайна сметка е резултат от забавянето в обработката на точката на комит от страна на приложението. Получаването на времената на завършеното изявление и завършените събития на RPC от разширени събития заедно с времето на събитието database_transaction_end показва забавянето на двупосочното пътуване от нивото на приложението, което затваря транзакцията при отворената връзка. В този случай всичко, което се вижда в SQL Server, е жертва на претоварен сървър на приложения и претоварен VM хост. Преместването/разделянето на натоварването на приложението между сървъри в NLB или хардуерна балансирана конфигурация с помощта на хостове, които не са прекомерно ангажирани при използване на процесора, бързо ще възстанови незабавното ангажимент на транзакциите и ще премахне спящите сесии, държащи заключвания в SQL Server.
И все пак още един пример за проблем с околната среда, причиняващ нещо, което изглеждаше като проблем с блокиране на работа. Винаги си струва да се проучи защо блокиращата нишка не е в състояние бързо да освободи ключалките си.