Автоматизация удаления забытых транзакций

  • Tutorial

Предисловие


Достаточно нередко бывают ситуации, когда транзакция в MS SQL Server бывает забытой тем, кто ее запустил. Самый частый пример этому — запуск скрипта в SSMS, где явно открывается транзакция инструкцией begin tran, затем происходит ошибка, а вот commit или rollback tran не происходит, а инициатор запуска благополучно отошел надолго от этого запроса. В результате с течением времени возникает все больше флуктуации в плане блокировок на запросы, которые запрашивают доступ к заблокированным ресурсам (таблицам, ресурсам сервера (ОЗУ, ЦП, система ввода-вывода).

В данной статье будет разобран пример автоматизации удаления забытых транзакций.

Решение


Под забытой транзакцией будем понимать ту активную (запущенную) транзакцию, в которой на протяжении какого-то достаточно большого промежутка времени T нет активных (выполняемых) запросов.

Сначала приведем общий алгоритм по удалению таких транзакций:

  1. Создаем таблицу для хранения и анализа информации по текущим забытым транзакциям, а также таблицу для архивации по действиям удаления выбранных из первой таблицы транзакций для последующего разбора
  2. Собираем информацию (транзакции и их сессии, у которых нет запросов, т е транзакции запущенные и забытые в течении определенного времени T)
  3. Обновляем таблицу текущих забытых транзакций из п.1 (если у забытой транзакции появился активный запрос, то такая транзакция становится незабытой и удаляется из первой таблицы п.1)
  4. Собираем те сессии, которые нужно убить (у сессии есть хотя бы одна транзакция, которая попала определенное кол-во раз K как забытая в таблицу из п.1 и столько же раз у самой сессии нет активных запросов)
  5. Архивируем что собираемся удалить (детальная информация про удаляемые сессии, подключения и транзакции)
  6. Удаляем выбранные сессии
  7. Удаляем обработанные записи, а также те, что невозможно удалить и они находятся слишком долго в таблице п.1

Далее приведем пример реализации данного выше алгоритма.
Создадим таблицу для хранения и анализа информации по текущим забытым транзакциям следующим образом:

Таблица попадания транзакций как забытых с их сессиями
USE [ИМЯ_БД]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [srv].[SessionTran](
	[SessionID] [int] NOT NULL,
	[TransactionID] [bigint] NOT NULL,
	[CountTranNotRequest] [tinyint] NOT NULL,
	[CountSessionNotRequest] [tinyint] NOT NULL,
	[TransactionBeginTime] [datetime] NOT NULL,
	[InsertUTCDate] [datetime] NOT NULL,
	[UpdateUTCDate] [datetime] NOT NULL,
 CONSTRAINT [PK_SessionTran] PRIMARY KEY CLUSTERED 
(
	[SessionID] ASC,
	[TransactionID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [srv].[SessionTran] ADD  CONSTRAINT [DF_SessionTran_Count]  DEFAULT ((0)) FOR [CountTranNotRequest]
GO

ALTER TABLE [srv].[SessionTran] ADD  CONSTRAINT [DF_SessionTran_CountSessionNotRequest]  DEFAULT ((0)) FOR [CountSessionNotRequest]
GO

ALTER TABLE [srv].[SessionTran] ADD  CONSTRAINT [DF_SessionTran_InsertUTCDate]  DEFAULT (getutcdate()) FOR [InsertUTCDate]
GO

ALTER TABLE [srv].[SessionTran] ADD  CONSTRAINT [DF_SessionTran_UpdateUTCDate]  DEFAULT (getutcdate()) FOR [UpdateUTCDate]
GO

Здесь:

1) SessionID — идентификатор сессии
2) TransactionID — идентификатор забытой транзакции
3) CountTranNotRequest — кол-во раз зафиксированного факта о том, что транзакция была забытой
4) CountSessionNotRequest — кол-во раз зафиксированного факта о том, что сессия была без активных запросов и содержала забытую транзакцию
5) TransactionBeginTime — дата и время начала забытой транзакции
6) InsertUTCDate — дата и время создания записи в UTC
7) UpdateUTCDate — дата и время изменения записи в UTC

Создадим таблицу для архивации по действиям удаления выбранных из первой таблицы транзакций для последующего разбора:

Таблица для архивации удаленных сессий по забытым транзакциям
USE [ИМЯ_БД]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [srv].[KillSession](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[session_id] [smallint] NOT NULL,
	[transaction_id] [bigint] NOT NULL,
	[login_time] [datetime] NOT NULL,
	[host_name] [nvarchar](128) NULL,
	[program_name] [nvarchar](128) NULL,
	[host_process_id] [int] NULL,
	[client_version] [int] NULL,
	[client_interface_name] [nvarchar](32) NULL,
	[security_id] [varbinary](85) NOT NULL,
	[login_name] [nvarchar](128) NOT NULL,
	[nt_domain] [nvarchar](128) NULL,
	[nt_user_name] [nvarchar](128) NULL,
	[status] [nvarchar](30) NOT NULL,
	[context_info] [varbinary](128) NULL,
	[cpu_time] [int] NOT NULL,
	[memory_usage] [int] NOT NULL,
	[total_scheduled_time] [int] NOT NULL,
	[total_elapsed_time] [int] NOT NULL,
	[endpoint_id] [int] NOT NULL,
	[last_request_start_time] [datetime] NOT NULL,
	[last_request_end_time] [datetime] NULL,
	[reads] [bigint] NOT NULL,
	[writes] [bigint] NOT NULL,
	[logical_reads] [bigint] NOT NULL,
	[is_user_process] [bit] NOT NULL,
	[text_size] [int] NOT NULL,
	[language] [nvarchar](128) NULL,
	[date_format] [nvarchar](3) NULL,
	[date_first] [smallint] NOT NULL,
	[quoted_identifier] [bit] NOT NULL,
	[arithabort] [bit] NOT NULL,
	[ansi_null_dflt_on] [bit] NOT NULL,
	[ansi_defaults] [bit] NOT NULL,
	[ansi_warnings] [bit] NOT NULL,
	[ansi_padding] [bit] NOT NULL,
	[ansi_nulls] [bit] NOT NULL,
	[concat_null_yields_null] [bit] NOT NULL,
	[transaction_isolation_level] [smallint] NOT NULL,
	[lock_timeout] [int] NOT NULL,
	[deadlock_priority] [int] NOT NULL,
	[row_count] [bigint] NOT NULL,
	[prev_error] [int] NOT NULL,
	[original_security_id] [varbinary](85) NOT NULL,
	[original_login_name] [nvarchar](128) NOT NULL,
	[last_successful_logon] [datetime] NULL,
	[last_unsuccessful_logon] [datetime] NULL,
	[unsuccessful_logons] [bigint] NULL,
	[group_id] [int] NOT NULL,
	[database_id] [smallint] NOT NULL,
	[authenticating_database_id] [int] NULL,
	[open_transaction_count] [int] NOT NULL,
	[most_recent_session_id] [int] NULL,
	[connect_time] [datetime] NULL,
	[net_transport] [nvarchar](40) NULL,
	[protocol_type] [nvarchar](40) NULL,
	[protocol_version] [int] NULL,
	[encrypt_option] [nvarchar](40) NULL,
	[auth_scheme] [nvarchar](40) NULL,
	[node_affinity] [smallint] NULL,
	[num_reads] [int] NULL,
	[num_writes] [int] NULL,
	[last_read] [datetime] NULL,
	[last_write] [datetime] NULL,
	[net_packet_size] [int] NULL,
	[client_net_address] [nvarchar](48) NULL,
	[client_tcp_port] [int] NULL,
	[local_net_address] [nvarchar](48) NULL,
	[local_tcp_port] [int] NULL,
	[connection_id] [uniqueidentifier] NULL,
	[parent_connection_id] [uniqueidentifier] NULL,
	[most_recent_sql_handle] [varbinary](64) NULL,
	[LastTSQL] [nvarchar](max) NULL,
	[transaction_begin_time] [datetime] NOT NULL,
	[CountTranNotRequest] [tinyint] NOT NULL,
	[CountSessionNotRequest] [tinyint] NOT NULL,
	[InsertUTCDate] [datetime] NOT NULL,
 CONSTRAINT [PK_KillSession] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [srv].[KillSession] ADD  CONSTRAINT [DF_KillSession_InsertUTCDate]  DEFAULT (getutcdate()) FOR [InsertUTCDate]
GO

Здесь все поля из системных представлений sys.dm_exec_sessions и sys.dm_exec_connections, а InsertUTCDate — дата и время создания записи в UTC.

Далее, для реализации оставшихся пунктов реализуем хранимую процедуру [srv].[AutoKillSessionTranBegin] следующим образом:

Реализация хранимой процедуры [srv].[AutoKillSessionTranBegin]
USE [ИМЯ_БД]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE   PROCEDURE [srv].[AutoKillSessionTranBegin]
	@minuteOld int, --старость запущенной транзакции (T мин)
	@countIsNotRequests int --кол-во попаданий в таблицу (K)
AS
BEGIN
	SET NOCOUNT ON;
	SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

    	declare @tbl table (
						SessionID int,
						TransactionID bigint,
						IsSessionNotRequest bit,
						TransactionBeginTime datetime
					   );
	
	--собираем информацию (транзакции и их сессии, у которых нет запросов, т е транзакции запущенные и забытые)
	insert into @tbl (
						SessionID,
						TransactionID,
						IsSessionNotRequest,
						TransactionBeginTime
					 )
	select t.[session_id] as SessionID
		 , t.[transaction_id] as TransactionID
		 , case when exists(select top(1) 1 from sys.dm_exec_requests as r where r.[session_id]=t.[session_id]) then 0 else 1 end as IsSessionNotRequest
		 , (select top(1) ta.[transaction_begin_time] from sys.dm_tran_active_transactions as ta where ta.[transaction_id]=t.[transaction_id]) as TransactionBeginTime
	from sys.dm_tran_session_transactions as t
	where t.[is_user_transaction]=1
	and not exists(select top(1) 1 from sys.dm_exec_requests as r where r.[transaction_id]=t.[transaction_id]);
	
	--обновляем таблицу запущенных транзакций, у которых нет активных запросов
	;merge srv.SessionTran as st
	using @tbl as t
	on st.[SessionID]=t.[SessionID] and st.[TransactionID]=t.[TransactionID]
	when matched then
		update set [UpdateUTCDate]			= getUTCDate()
				 , [CountTranNotRequest]	= st.[CountTranNotRequest]+1			
				 , [CountSessionNotRequest]	= case when (t.[IsSessionNotRequest]=1) then (st.[CountSessionNotRequest]+1) else 0 end
				 , [TransactionBeginTime]	= t.[TransactionBeginTime]
	when not matched by target then
		insert (
				[SessionID]
				,[TransactionID]
				,[TransactionBeginTime]
			   )
		values (
				t.[SessionID]
				,t.[TransactionID]
				,t.[TransactionBeginTime]
			   )
	when not matched by source then delete;

	--список сессий для удаления (содержащие забытые транзакции)
	declare @kills table (
							SessionID int
						 );

	--детальная информация для архива
	declare @kills_copy table (
							SessionID int,
							TransactionID bigint,
							CountTranNotRequest tinyint,
							CountSessionNotRequest tinyint,
							TransactionBeginTime datetime
						 )

	--собираем те сессии, которые нужно убить
	--у сессии есть хотя бы одна транзакция, которая попала @countIsNotRequests раз как без активных запросов и столько же раз у самой сессии нет активных запросов
	insert into @kills_copy	(
							SessionID,
							TransactionID,
							CountTranNotRequest,
							CountSessionNotRequest,
							TransactionBeginTime
						 )
	select SessionID,
		   TransactionID,
		   CountTranNotRequest,
		   CountSessionNotRequest,
		   TransactionBeginTime
	from srv.SessionTran
	where [CountTranNotRequest]>=@countIsNotRequests
	  and [CountSessionNotRequest]>=@countIsNotRequests
	  and [TransactionBeginTime]<=DateAdd(minute,-@minuteOld,GetDate());

	  --архивируем что собираемся удалить (детальная информация про удаляемые сессии, подключения и транзакции)
	  INSERT INTO [srv].[KillSession]
           ([session_id]
           ,[transaction_id]
           ,[login_time]
           ,[host_name]
           ,[program_name]
           ,[host_process_id]
           ,[client_version]
           ,[client_interface_name]
           ,[security_id]
           ,[login_name]
           ,[nt_domain]
           ,[nt_user_name]
           ,[status]
           ,[context_info]
           ,[cpu_time]
           ,[memory_usage]
           ,[total_scheduled_time]
           ,[total_elapsed_time]
           ,[endpoint_id]
           ,[last_request_start_time]
           ,[last_request_end_time]
           ,[reads]
           ,[writes]
           ,[logical_reads]
           ,[is_user_process]
           ,[text_size]
           ,[language]
           ,[date_format]
           ,[date_first]
           ,[quoted_identifier]
           ,[arithabort]
           ,[ansi_null_dflt_on]
           ,[ansi_defaults]
           ,[ansi_warnings]
           ,[ansi_padding]
           ,[ansi_nulls]
           ,[concat_null_yields_null]
           ,[transaction_isolation_level]
           ,[lock_timeout]
           ,[deadlock_priority]
           ,[row_count]
           ,[prev_error]
           ,[original_security_id]
           ,[original_login_name]
           ,[last_successful_logon]
           ,[last_unsuccessful_logon]
           ,[unsuccessful_logons]
           ,[group_id]
           ,[database_id]
           ,[authenticating_database_id]
           ,[open_transaction_count]
           ,[most_recent_session_id]
           ,[connect_time]
           ,[net_transport]
           ,[protocol_type]
           ,[protocol_version]
           ,[encrypt_option]
           ,[auth_scheme]
           ,[node_affinity]
           ,[num_reads]
           ,[num_writes]
           ,[last_read]
           ,[last_write]
           ,[net_packet_size]
           ,[client_net_address]
           ,[client_tcp_port]
           ,[local_net_address]
           ,[local_tcp_port]
           ,[connection_id]
           ,[parent_connection_id]
           ,[most_recent_sql_handle]
           ,[LastTSQL]
           ,[transaction_begin_time]
           ,[CountTranNotRequest]
           ,[CountSessionNotRequest])
	select ES.[session_id]
           ,kc.[TransactionID]
           ,ES.[login_time]
           ,ES.[host_name]
           ,ES.[program_name]
           ,ES.[host_process_id]
           ,ES.[client_version]
           ,ES.[client_interface_name]
           ,ES.[security_id]
           ,ES.[login_name]
           ,ES.[nt_domain]
           ,ES.[nt_user_name]
           ,ES.[status]
           ,ES.[context_info]
           ,ES.[cpu_time]
           ,ES.[memory_usage]
           ,ES.[total_scheduled_time]
           ,ES.[total_elapsed_time]
           ,ES.[endpoint_id]
           ,ES.[last_request_start_time]
           ,ES.[last_request_end_time]
           ,ES.[reads]
           ,ES.[writes]
           ,ES.[logical_reads]
           ,ES.[is_user_process]
           ,ES.[text_size]
           ,ES.[language]
           ,ES.[date_format]
           ,ES.[date_first]
           ,ES.[quoted_identifier]
           ,ES.[arithabort]
           ,ES.[ansi_null_dflt_on]
           ,ES.[ansi_defaults]
           ,ES.[ansi_warnings]
           ,ES.[ansi_padding]
           ,ES.[ansi_nulls]
           ,ES.[concat_null_yields_null]
           ,ES.[transaction_isolation_level]
           ,ES.[lock_timeout]
           ,ES.[deadlock_priority]
           ,ES.[row_count]
           ,ES.[prev_error]
           ,ES.[original_security_id]
           ,ES.[original_login_name]
           ,ES.[last_successful_logon]
           ,ES.[last_unsuccessful_logon]
           ,ES.[unsuccessful_logons]
           ,ES.[group_id]
           ,ES.[database_id]
           ,ES.[authenticating_database_id]
           ,ES.[open_transaction_count]
           ,EC.[most_recent_session_id]
           ,EC.[connect_time]
           ,EC.[net_transport]
           ,EC.[protocol_type]
           ,EC.[protocol_version]
           ,EC.[encrypt_option]
           ,EC.[auth_scheme]
           ,EC.[node_affinity]
           ,EC.[num_reads]
           ,EC.[num_writes]
           ,EC.[last_read]
           ,EC.[last_write]
           ,EC.[net_packet_size]
           ,EC.[client_net_address]
           ,EC.[client_tcp_port]
           ,EC.[local_net_address]
           ,EC.[local_tcp_port]
           ,EC.[connection_id]
           ,EC.[parent_connection_id]
           ,EC.[most_recent_sql_handle]
           ,(select top(1) text from sys.dm_exec_sql_text(EC.[most_recent_sql_handle])) as [LastTSQL]
           ,kc.[TransactionBeginTime]
           ,kc.[CountTranNotRequest]
           ,kc.[CountSessionNotRequest]
	from @kills_copy as kc
	inner join sys.dm_exec_sessions ES with(readuncommitted) on kc.[SessionID]=ES.[session_id]
	inner join sys.dm_exec_connections EC  with(readuncommitted) on EC.session_id = ES.session_id;

	--собираем сессии
	insert into @kills (
							SessionID
						 )
	select [SessionID]
	from @kills_copy
	group by [SessionID];
	
	declare @SessionID int;

	--непосредственное удаление выбранных сессий
	while(exists(select top(1) 1 from @kills))
	begin
		select top(1)
		@SessionID=[SessionID]
		from @kills;
    
    BEGIN TRY
		EXEC sp_executesql N'kill @SessionID',
						   N'@SessionID INT',
						   @SessionID;
    END TRY
    BEGIN CATCH
    END CATCH

		delete from @kills
		where [SessionID]=@SessionID;
	end

	select st.[SessionID]
		  ,st.[TransactionID]
		  into #tbl
	from srv.SessionTran as st
	where st.[CountTranNotRequest]>=250
	   or st.[CountSessionNotRequest]>=250
	   or exists(select top(1) 1 from @kills_copy kc where kc.[SessionID]=st.[SessionID]);

	--удаление обработанных записей, а также те, что невозможно удалить и они находятся слишком долго в таблице на рассмотрение
	delete from st
	from #tbl as t
	inner join srv.SessionTran as st on t.[SessionID]	 =st.[SessionID]
									and t.[TransactionID]=st.[TransactionID];

	drop table #tbl;
END
GO

Здесь п.7 алгоритма реализован по достижению одного из счетчиков CountTranNotRequest или CountSessionNotRequest значения 250.

Результат


В данной статье был рассмотрен пример реализации автоматизированного удаления забытых транзакций.

Данный метод позволяет автоматизировать процесс удаления забытых транзакций, результатом которого является пресечение попыток возрастания флуктуации в плане блокировок, исходящие от таких транзакций. В итоге достигается защита производительности СУБД от таких действий инициаторов по запуску транзакций, которые в будущем могут стать забытыми.

Источники:


» sys.dm_exec_requests
» sys.dm_tran_active_transactions
» sys.dm_tran_session_transactions
» sys.dm_exec_sql_text
» sys.dm_exec_sessions
» sys.dm_exec_connections
» KILL
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 8
  • +1
    Стоп. У вас программная ошибка, какой-то скрипт что-то запустил и подвис и/или вылетел уже, а вы вместо того, чтобы отправить алерт в мониторинг, тупо пришибаете все следы бага?
    Энтерпрайзненько, ничего не скажешь…
    • 0
      Для этого и существует архивная таблица-кто-то ночью что-то запулил и забыл. Система сама удалит эту транзакцию, а Вы утром посмотрите и дадите по рукам тому, кто это сделал. А так (без автоматизации решения данной проблемы)-уже до утра пользователи пострадают от того, что транзакция вызовет общую блокировку и запросы все встанут и Вас поднимут ночью и плюс в деньгах компания потеряет
    • +1
      Насчет забытых транзакций в SSMS при ошибках, давно привык всегда после открытия транзакции писать set xact_abort on. Не шумит визуально, как try/catch, а делает в общем-то то что и надо в большинстве случаев — как только ошибка, сразу откат транзакции.
      • 0
        Да, это самый простой способ.
        Жаль, что им не все пользуются-и не заставишь
      • 0

        Эмм. А что — в больших базах данных нет возможности сделать default timeout на транзакцию?


        Типа, кому надо длинную транзакцию делать — пусть в явном виде указывает большой тайм-аут, а для обычных «зависших» транзакций система сама их пришибёт через какое-то время (конечно, с записью в лог этого славного деяния).

        • –1
          Да, есть такая настройка
          Но по опыту скажу-это плохая настройка как и автосжатие, т к важный для бизнеса запрос может в редких случаях выполняться в разы дольше по разным причинам и тому виной не только возникшие проблемы в железе, а запрос должен в любом случае выполняться+ эта настройка не в буквальном смысле говорит сколько в сек должен выполняться запрос, а сколько оценит оптимизатор ещё до начала выполнения этого запроса, и часто оптимизатор ошибается из-за многих влияющих внешних факторов
          Так что по осторожнее с этой настройкой
          • 0
            Насчет настройки — да, я с вами согласен: любая настройка вносящая существенное изменение поведения системы довольна чувствительна.

            По мне — так зависшие транзакции — это существенное неправильное поведение системы, оно не должно происходить в штатном режиме. Как раз настройка СУБД для «прибивания» такой ситуации позволит таки высвободить задействованные ресурсы, и продолжить работу системы. Однако нужно понимать, что произошел сбой — который желательно отработать на прикладном уровне.

            Подход в статье предполагает отработку ситуации на прикладном уровне — да, это предпочтительнее. Просто это нужно реализовывать, а настроука СУБД может защитить от ситуации «из коробки», а «разгребание» последствий — дело DBA.

            Конечно, дело вкуса что использовать. Я бы совместил — настроку в СУБД, и, по возможности, отработку зависания транзакций в прикладном решении (в админке?)
            • 0
              Со временем, когда вся СУБД будет исследована, я обычно выставляю эту настройку, выставив значение в несколько раз больше, чем максимально выполняемая по времени транзакция
              Но пока не полностью покрыта СУБД или условия часто меняются (взаимодействие с другими системами), порой даже не согласовав вносимые изменения с DBA, с этой настройкой нужно быть крайне осторожным

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое