[SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 16.02.15 11:09
Оценка:
Добрый день, коллеги!

Решили на пилотном проекте применить технологию в коробке от Microsoft под названием Filetable. Суть технологии заключается в использовании таблицы с заранее предопределенной схемой, которая проецируется на диск для хранения неструктурированных данных. Возможны два варианта доступа к элементам хранения через таблицу и файловую шару, всю работу по «синхронизации» берет на себя СУБД. В нашем случае использовался транзакционный доступ через таблицу, причем вставка данных выполнялась параллельно. Схема таблицы представлена по ссылке Схема FileTable. В виде DDL инструкции она выглядит так:
  DDL
create table dbo.IssueFiles2
(
    stream_id uniqueidentifier not null default (newsequentialid()) 
    ,file_stream varbinary(max) null
    ,name nvarchar(255) not null
    ,path_locator hierarchyid primary key nonclustered not null
    ,parent_path_locator hierarchyid null -- as (case when path_locator.GetLevel()=(1) then null else path_locator.GetAncestor((1)) end)
    ,file_type nvarchar(255) -- as ((getfileextension(@filename)))
    ,cached_file_size bigint -- as (datalength(@file))
    ,creation_time datetimeoffset(7) not null
    ,last_write_time datetimeoffset(7) not null
    ,last_access_time datetimeoffset(7) not null
    ,is_directory bit not null
    ,is_offline bit not null
    ,is_hidden bit not null
    ,is_readonly bit not null
    ,is_archive bit not null
    ,is_system bit not null
    ,is_temporary bit not null
)
go

alter table dbo.IssueFiles2 add unique nonclustered(stream_id)
alter table dbo.IssueFiles2 add unique nonclustered(parent_path_locator, name)
alter table dbo.IssueFiles2 add constraint FK_IssueFiles2_paren_path_locator 
    foreign key(parent_path_locator) references dbo.IssueFiles2(path_locator)
go


В целом от FILETABLE можно абстрагироваться и вести речь только про таблицу определенной структуры, с некоторыми особенностями на которые стоит обратить внимание:
1. Схема таблицы изменению не подлежит, за исключением добавления индексов.
2. В таблице отсутствует кластерный индекс
3. В таблице присутствует FK на саму себя для обеспечения целостности древовидной структуры (Примечание: таблица содержит каталоги, внутри которых хранятся файлы и каталоги)

Продолжительное время проблем с таблицей не было – пользователи вставляли данные, пользователи выбирали данные. И как водится в таких историях, неожиданно наступил день, когда массово посыпались проблемы, а точнее взаимоблокировки. Как показал последующий разбор ситуации, таблица накопила несколько сотен тысяч записей, и увеличилось как количество пользователей работающих с системой, так и количество файлов, которые они загружали. Скажу сразу, проблема решена, но остался неприятный осадок и два аспекта, которые хотелось бы обсудить и разобраться в них. На данный момент «на руках» информация по взаимоблокировкам из лога ошибок рабочей среды и тестовая среда, где можно посмотреть внутренние механизмы и поставить эксперименты. Полностью повторить ситуацию и подтвердить или опровергнуть свои догадки не представляется возможным.

Итак, по порядку.

1. Причина возникновения взаимоблокировки

Информация о взаимоблокировках из журнала ошибок рабочей среды:
  DEADLOCK

deadlock-list
deadlock victim=process23df4c188
process-list
process id=process23df4c188 taskpriority=0 logused=2136 waitresource=KEY: 8:72057626804813824 (c61cf6ccabf8) waittime=111 ownerId=103380711 transactionname=user_transaction lasttranstarted=2015-01-21T12:39:12.867 XDES=0x272275348 lockMode=S schedulerid=3 kpid=9276 status=suspended spid=67 sbid=2 ecid=0 priority=0 trancount=2 lastbatchstarted=2015-01-21T12:39:12.870 lastbatchcompleted=2015-01-21T12:39:12.870 lastattention=1900-01-01T00:00:00.870 clientapp=HS hostname=HOST hostpid=7708 loginname=user isolationlevel=read committed (2) xactid=103380711 currentdb=8 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=109 stmtstart=6522 stmtend=6830 sqlhandle=0x020000005e250e31e4ee32206dff26668bdf95328ff4916f0000000000000000000000000000000000000000
frame procname=DB.dbo.hsp_PutAndRenameDoubleIssueFiles line=109 stmtstart=6522 stmtend=6830 sqlhandle=0x030008008144c2762debc50027a4000001000000000000000000000000000000000000000000000000000000
insert into dbo.IssueFiles(name, file_stream, path_locator)
select @p_filename, @file, @path_locator
inputbuf
Proc [Database Id = 8 Object Id = 1992443009]
process id=process2455b1c38 taskpriority=0 logused=2136 waitresource=KEY: 8:72057626804813824 (82b7a1c04c5d) waittime=172 ownerId=103380443 transactionname=user_transaction lasttranstarted=2015-01-21T12:39:12.640 XDES=0x246a803a8 lockMode=S schedulerid=1 kpid=7812 status=suspended spid=113 sbid=2 ecid=0 priority=0 trancount=2 lastbatchstarted=2015-01-21T12:39:12.640 lastbatchcompleted=2015-01-21T12:39:12.640 lastattention=1900-01-01T00:00:00.640 clientapp=HS hostname=HOST hostpid=2644 loginname=user isolationlevel=read committed (2) xactid=103380443 currentdb=8 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=109 stmtstart=6522 stmtend=6830 sqlhandle=0x020000005e250e31e4ee32206dff26668bdf95328ff4916f0000000000000000000000000000000000000000
frame procname=DB.dbo.hsp_PutAndRenameDoubleIssueFiles line=109 stmtstart=6522 stmtend=6830 sqlhandle=0x030008008144c2762debc50027a4000001000000000000000000000000000000000000000000000000000000
insert into dbo.IssueFiles(name, file_stream, path_locator)
select @p_filename, @file, @path_locator
inputbuf
Proc [Database Id = 8 Object Id = 1992443009]
resource-list
keylock hobtid=72057626804813824 dbid=8 objectname=DB.dbo.IssueFiles indexname=PK__IssueFil__5A5B77D55446D258 id=lock2be37f380 mode=X associatedObjectId=72057626804813824
owner-list
owner id=process2455b1c38 mode=X
waiter-list
waiter id=process23df4c188 mode=S requestType=wait
keylock hobtid=72057626804813824 dbid=8 objectname=DB.dbo.IssueFiles indexname=PK__IssueFil__5A5B77D55446D258 id=lock22a81ed00 mode=X associatedObjectId=72057626804813824
owner-list
owner id=process23df4c188 mode=X
waiter-list
waiter id=process2455b1c38 mode=S requestType=wait



Из представленной выше информации видно, что взаимоблокировка возникает на простых операторах INSERT,
insert into dbo.IssueFiles(name, file_stream, path_locator)  select @p_filename, @file, @path_locator

причем в качестве ресурса выступает некластерный индекс, созданный на PK поля path_locator типа hierarchyid.

Вставка данных двумя процессами T1 и T2 выглядит следующим образом:
1. T1 вставляет одну запись в кучу и накладывает эксклюзивную блокировку (X) на строку не кластерного индекса PK
2. T2 вставляет одну запись в кучу и накладывает эксклюзивную блокировку (X) на строку не кластерного индекса PK
3. T1 выполняет «поиск/сканирование» первичного ключа PK для того чтобы проверить ссылочную целостность для FK и запрашивает совмещаемую (S) блокировку на строку, на которой уже лежит эксклюзивная блокировка из T2.
4. T2 выполняет «поиск/сканирование» первичного ключа PK для того чтобы проверить ссылочную целостность для FK и запрашивает совмещаемую (S) блокировку на строку, на которой уже лежит эксклюзивная блокировка из T1.
5. Для продолжения работы каждый из процессов ожидает освобождения ресурса другим процессом, как результат взаимоблокировка.

Важно отметить, что данные вставляются разные по одной записи, но предок (FK) у них один и тот же – это каталог с месяцем года (возможные значения от 1 до 12). Он не модифицируется, не вставляется, а уже существует на момент вставки записей и готов к тому, чтобы два процесса наложили на него совмещаемые (S) блокировки без конфликтов. План запроса на тестовой среде, без нагрузки выглядит следующим образом:

  PLAN


Нет ясного понимания, для каких целей СУБД обращается к записи, которая только что вставлена соседним процессом. Ведь для верификации FK достаточно выполнить index seek, по PK индексу, не затрагивая грязные данные. Единственное объяснение, которое у меня возникло – по каким-то неведомым причинам при вставке одной записи(!) оптимизатор для верификации ссылочной целостности вместо поиска предлагает использовать сканирование индекса. Возможно это влияние нагрузки, объема уже существующих данных или еще какие-то внешние факторы. Насколько я прав в своих рассуждениях, возможно истинные причины в чем-то другом и я не правильно интерпретирую взаимоблокировку? Повторить это поведение не удалось на тестовой БД без нагрузки. Даже при открытой транзакции repeatable read два процесса вставляют данные и без проблем читают одного и того же предка для верификации.

2. Решение проблемы
Второй момент, на котором хотелось бы остановиться – это способ решения проблемы. В таблице из коробки отсутствует кластерный индекс. Цель кластерного индекса, на мой взгляд – снизить время вставки данных в таблицу по сравнению с кучей, уменьшить время блокирования и использования ресурсов и как результат исключить взаимоблокировки. Единственное поле кандидат на эту роль – поле stream_id типа uniqueidentifier. Из схемы этого не видно, но практика показывает, что для заполнения этого поля используются значения из newsequentialid(), т.е. монотонно возрастающие. Действительно, создание кластерного индекса привело к успеху, взаимоблокировки исчезли, но неприятный осадок остался. Во-первых, проверка вставки на двух параллельных запросах на тесте не впечатлила. Запрос с sys.dm_tran_locks не показал уменьшения количества заблокированных ресурсов по сравнению с кучей (страницы, индексы и прочее). Возможно, я слишком «идеалистично» представлял себе этот процесс. Во-вторых, возникает резонный вопрос, а что будет дальше, если увеличится количество данных в таблице, увеличится нагрузка в виде количества пользователей и создаваемых записей? Взаимоблокировки вернутся? Пути отступления есть, что означает избежать проверки ссылочной целостности. Сделать это можно двумя способами, например, отказаться от хранения иерархий в таблице, т.е. хранить в пределах одной таблицы данные только за месяц, предок у всех записей будет null, но это потребует доработок. Второй вариант отключить все проверки на FILTABLE, но это чревато проблемами с целостностью всей таблицы в будущем.

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

Резюмируя, опишу кратко свои вопросы:
1. По каким причинам на ваш взгляд возник deadlock? Действительно ли использовался index scan для верификации ссылочной целостности и почему? Или истинная причина в чем-то другом?
2. Чем смог помочь кластерный индекс в решении проблемы взаимоблокировки? Время вставки должно было сократиться, но что произойдет, если нагрузка увеличится? Так ли это?

Спасибо.
sql server filetable deadlock кластерный индекс
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Softwarer http://softwarer.ru
Дата: 16.02.15 11:51
Оценка:
Здравствуйте, Olaf, Вы писали:

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

Поэтому если есть подозрения — имхо лучше отключить FK и не рисковать.

Для того, чтобы не пропустить ошибку, имхо уместно применить другой подход, я применял его однажды, работая с базой вообще без FK: я повесил туда набор ночных job-ов, делавших select-ы на "неправильные записи" и возвращавшие список ошибок. Это позволяло эффективно, без просадок в производительности контролировать целостность, не трогать старые записи (там, где их было уже нереально исправить) и оперативно находить и устранять источники новых проблем.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: 1303  
Дата: 16.02.15 21:51
Оценка: +1
Здравствуйте, Softwarer, Вы писали:
...
S>Для того, чтобы не пропустить ошибку, имхо уместно применить другой подход, я применял его однажды, работая с базой вообще без FK: я повесил туда набор ночных job-ов, делавших select-ы на "неправильные записи" и возвращавшие список ошибок. Это позволяло эффективно, без просадок в производительности контролировать целостность, не трогать старые записи (там, где их было уже нереально исправить) и оперативно находить и устранять источники новых проблем.

А что происходило в случае, когда "неправильные записи" приводили к проблемам уже днём-вечером, до того как выполнялись эти ночные job-ы? Надеюсь, не было ничего страшнее рассылки порно детям?
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Tigor Россия  
Дата: 17.02.15 10:06
Оценка:
Здравствуйте, Olaf, Вы писали:

Мне кажется, ты не совсем верно интерпретировал происходящее.

O>Вставка данных двумя процессами T1 и T2 выглядит следующим образом:

O>1. T1 вставляет одну запись в кучу и накладывает эксклюзивную блокировку (X) на строку не кластерного индекса PK
O>2. T2 вставляет одну запись в кучу и накладывает эксклюзивную блокировку (X) на строку не кластерного индекса PK
O>3. T1 выполняет «поиск/сканирование» первичного ключа PK для того чтобы проверить ссылочную целостность для FK и запрашивает совмещаемую (S) блокировку на строку, на которой уже лежит эксклюзивная блокировка из T2.
O>4. T2 выполняет «поиск/сканирование» первичного ключа PK для того чтобы проверить ссылочную целостность для FK и запрашивает совмещаемую (S) блокировку на строку, на которой уже лежит эксклюзивная блокировка из T1.
O>5. Для продолжения работы каждый из процессов ожидает освобождения ресурса другим процессом, как результат взаимоблокировка.

Мне кажется, что сначала должна происходить проверка ссылочной целостности, а уже потом вставка.
У тебя до оператора insert в той же транзакции ничего не происходит случайно? Или явной транзакции нету?
К сожалению, в действительности все выглядит иначе, чем на самом деле.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 17.02.15 10:27
Оценка:
Здравствуйте, Tigor, Вы писали:

T>Здравствуйте, Olaf, Вы писали:


T>Мне кажется, ты не совсем верно интерпретировал происходящее.

T>...
T>Мне кажется, что сначала должна происходить проверка ссылочной целостности, а уже потом вставка.
T>У тебя до оператора insert в той же транзакции ничего не происходит случайно? Или явной транзакции нету?

План запроса, который я приложил показывает, что сначала идет вставка в таблицу, потом идет поиск по индексу, который ищет родителя, а далее оператор assert проверяет ограничение, на основании чего принимает решение о продолжении выполнения запроса или генерации ошибки.

Транзакция перед insert присутствует, уровень изоляции по умолчанию, после нее сразу идет insert. Перед самим insert выполняются запросы на select для заполнения переменных.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: gandjustas Россия http://blog.gandjustas.ru/
Дата: 17.02.15 11:12
Оценка:
Здравствуйте, Tigor, Вы писали:

T>Мне кажется, что сначала должна происходить проверка ссылочной целостности, а уже потом вставка.

Это только кажется. SQL Server всегда вставляет данные, а потом выполняет проверки.
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: gandjustas Россия http://blog.gandjustas.ru/
Дата: 17.02.15 11:26
Оценка:
Здравствуйте, Olaf, Вы писали:

O>Добрый день, коллеги!



O>Нет ясного понимания, для каких целей СУБД обращается к записи, которая только что вставлена соседним процессом. Ведь для верификации FK достаточно выполнить index seek, по PK индексу, не затрагивая грязные данные. Единственное объяснение, которое у меня возникло – по каким-то неведомым причинам при вставке одной записи(!) оптимизатор для верификации ссылочной целостности вместо поиска предлагает использовать сканирование индекса. Возможно это влияние нагрузки, объема уже существующих данных или еще какие-то внешние факторы. Насколько я прав в своих рассуждениях, возможно истинные причины в чем-то другом и я не правильно интерпретирую взаимоблокировку? Повторить это поведение не удалось на тестовой БД без нагрузки. Даже при открытой транзакции repeatable read два процесса вставляют данные и без проблем читают одного и того же предка для верификации.


А перед insert что выполняется? Особенно интересует как вычисляется path_locator.
Может сам SQL Server выполняет проверку path_locator, обращаясь для этого к соседям?

Ну или совсем странный вариант — выполняется scan из-за устаревшей статистики.
Можно ли получить реальный план запроса при падении? или это даст слишком большую нагрузку?
Re[3]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Tigor Россия  
Дата: 17.02.15 11:40
Оценка:
Здравствуйте, Olaf, Вы писали:

O>План запроса, который я приложил показывает, что сначала идет вставка в таблицу, потом идет поиск по индексу, который ищет родителя, а далее оператор assert проверяет ограничение, на основании чего принимает решение о продолжении выполнения запроса или генерации ошибки.


Да. Это я затупил. На самом деле с учетом FK ключа на ту же самую таблицу (то есть и на ту же самую строку, в теории), это единственный вариант.
К сожалению, в действительности все выглядит иначе, чем на самом деле.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 18.02.15 07:49
Оценка:
Здравствуйте, gandjustas, Вы писали:

G>А перед insert что выполняется? Особенно интересует как вычисляется path_locator.

G>Может сам SQL Server выполняет проверку path_locator, обращаясь для этого к соседям?

Процедура выглядит так:
  SP
create procedure dbo.hsp_PutAndRenameDoubleIssueFiles(@p_filename nvarchar(255), @file varbinary(max)) as begin

set nocount on;

begin try
   declare @year varchar(4) = cast(year(getdate()) as varchar(4))
   declare @month varchar(2) = cast(month(getdate()) as varchar(2))
    
   declare @month_path_locator hierarchyid
   declare @path_locator hierarchyid
  
   -- Возьмем локатор каталога МЕСЯЦ
   select @month_path_locator = path_locator
   from dbo.IssueFiles t1
   where name = @month 
      and exists(
                  select 1 
                  from dbo.IssueFiles t2 
                  where t2.name = @year and t2.path_locator = t1.parent_path_locator
               )

   if (@month_path_locator is null)
      raiserror('Не найден каталог %s/%s для вставки файла %s', 16, 1, @year, @month, @p_filename)

   -- Определяем положение узла в иерархии 
   set @path_locator = @month_path_locator.ToString() +
            convert(varchar(20), convert(bigint, substring(convert(binary(16), newid()), 1, 6))) + '.' +
            convert(varchar(20), convert(bigint, substring(convert(binary(16), newid()), 7, 6))) + '.' +
            convert(varchar(20), convert(bigint, substring(convert(binary(16), newid()), 13, 4))) + '/'

   begin tran
      begin try
         insert into dbo.IssueFiles(name, file_stream, path_locator)
         select @p_filename, @file, @path_locator
   end try
   begin catch
      -- Если был дубликат, удаляем и делаем повторную попытку
      if (error_number() = 2627)
      begin
         delete from dbo.IssueFiles where name = @p_filename and parent_path_locator = @month_path_locator

         insert into dbo.IssueFiles(name, file_stream, path_locator)
         select @p_filename, @file, @path_locator
      end
      else
         throw;
   end catch

   commit tran

end try
begin catch

   if (@@trancount > 0)
      rollback tran;

   throw;
    
end catch

end

go

Скажу сразу количество дубликатов, которые встречаются при вставке меньше 1% от общего числа.

Поле parent_path_locator вычисляемое persisted, т.е. определяется на основе вставляемого значения path_locator. Тип hierarchyid представляет из себя CLR тип, и в целом из значения path_locator можно извлечь предка через GetAncestor. Формула для поля — (case when [path_locator].[GetLevel]()=(1) then NULL else [path_locator].[GetAncestor]((1)) end). Именно на нем висит FK.

G>Ну или совсем странный вариант — выполняется scan из-за устаревшей статистики.

G>Можно ли получить реальный план запроса при падении? или это даст слишком большую нагрузку?
Возможно вы правы, устарели статистики. Сейчас думаю, как безболезненно повторить ситуацию и извлечь реальный план.
Re[3]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: gandjustas Россия http://blog.gandjustas.ru/
Дата: 18.02.15 20:12
Оценка:
Здравствуйте, Olaf, Вы писали:

O>Здравствуйте, gandjustas, Вы писали:


G>>А перед insert что выполняется? Особенно интересует как вычисляется path_locator.

G>>Может сам SQL Server выполняет проверку path_locator, обращаясь для этого к соседям?

O>Скажу сразу количество дубликатов, которые встречаются при вставке меньше 1% от общего числа.

А уровень изоляции какой? Repeatable read? Не висят ли блокировки первого селекта до конца транзацкии?
Re[4]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 19.02.15 07:25
Оценка:
Здравствуйте, gandjustas, Вы писали:

G>А уровень изоляции какой? Repeatable read? Не висят ли блокировки первого селекта до конца транзацкии?


Уровень изоляции там по умолчанию — read committed.
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 25.02.15 04:06
Оценка: 12 (1)
Здравствуйте, Olaf, Вы писали:

O>Резюмируя, опишу кратко свои вопросы:

O>1. По каким причинам на ваш взгляд возник deadlock? Действительно ли использовался index scan для верификации ссылочной целостности и почему? Или истинная причина в чем-то другом?
O>2. Чем смог помочь кластерный индекс в решении проблемы взаимоблокировки? Время вставки должно было сократиться, но что произойдет, если нагрузка увеличится? Так ли это?

Решил пойти от обратного, т.е. выдвинув предположение, что причиной взаимоблокировки ожидания при чтении PK индекса, является операция сканирования индекса, заставил оптимизатор построить план со сканированием индекса. Цель одна – получить deadlock граф, сравнить с предыдущим для подтверждения или опровержения гипотезы. Для этого достаточно добавить опцию option (merge join) в insert — insert into dbo.IssueFiles3(name, file_stream, path_locator) select @p_filename, @file, @path_locator option(merge join).
Несмотря на то, что никакие данные не соединяются при вставке, а используются просто переменные, подсказка применяется ко всему запросу, в том числе и для верификации данных. Замена операции соединения с Nested Loop на Merge, привела к сканированию индекса, что собственно и требовалось. Далее, запустил 4 экземпляра SQLQueryStress, предварительно указав в каждом использовать 50 итераций в 100 потоков. Данная нагрузка является однобокой и полностью не воспроизводит рабочую, т.к. выполняется вставка только в одну таблицу, но задача заключается в гарантированном получении взаимоблокировки. Несмотря на все старания, потребовалось 15 минут для отлова. Реальный план запроса, полученный через sqlhandle выглядит следующим образом, что подтверждает использование скана:

  PLAN

Но вот сама взаимоблокировка изменилась...

  DEADLOCK

<deadlock>
<victim-list>
<victimProcess id="process13bedbc38" />
</victim-list>
<process-list>
<process id="process13bedbc38" taskpriority="0" logused="2388" waitresource="PAGE: 8:1:7456349 " waittime="1825" ownerId="22563764" transactionname="user_transaction" lasttranstarted="2015-02-20T12:59:37.953" XDES="0x208410d28" lockMode="S" schedulerid="2" kpid="10720" status="suspended" spid="288" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-02-20T12:59:37.937" lastbatchcompleted="2015-02-20T12:59:37.857" lastattention="1900-01-01T00:00:00.857" clientapp=".Net SqlClient Data Provider" hostname="HOST" hostpid="25124" loginname="rb" isolationlevel="read committed (2)" xactid="22563764" currentdb="8" lockTimeout="4294967295" clientoption1="671096864" clientoption2="128056">
<executionStack>
<frame procname="dbo.hsp_PutAndRenameDoubleIssueFiles" line="113" stmtstart="6798" stmtend="7146" sqlhandle="0x030008008144c27664a2c80045a4000001000000000000000000000000000000000000000000000000000000">
insert into dbo.IssueFiles3(name, file_stream, path_locator)
select @p_filename, @file, @path_locator option(merge join) </frame>
<frame procname="adhoc" line="4" stmtstart="378" sqlhandle="0x02000000f9e86b3b05d5b67babb9cd12eb0e553806a86e2d0000000000000000000000000000000000000000">
exec dbo.hsp_PutAndRenameDoubleIssueFiles @name, @data </frame>
</executionStack>
<inputbuf>
declare @name nvarchar(256) = cast(newid() as varchar(256)) + '.txt'
declare @text as varchar(max) = '12'
declare @data varbinary(max) = cast(replicate(@text, 300000) as varbinary(max))
exec dbo.hsp_PutAndRenameDoubleIssueFiles @name, @data </inputbuf>
</process>
<process id="process13005a928" taskpriority="0" logused="2388" waitresource="PAGE: 8:1:7456349 " waittime="1821" ownerId="22564193" transactionname="user_transaction" lasttranstarted="2015-02-20T12:59:38.507" XDES="0x19c77cd28" lockMode="S" schedulerid="1" kpid="11900" status="suspended" spid="101" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-02-20T12:59:38.303" lastbatchcompleted="2015-02-20T12:59:38.230" lastattention="1900-01-01T00:00:00.230" clientapp=".Net SqlClient Data Provider" hostname="HOST" hostpid="25124" loginname="rb" isolationlevel="read committed (2)" xactid="22564193" currentdb="8" lockTimeout="4294967295" clientoption1="671096864" clientoption2="128056">
<executionStack>
<frame procname="dbo.hsp_PutAndRenameDoubleIssueFiles" line="113" stmtstart="6798" stmtend="7146" sqlhandle="0x030008008144c27664a2c80045a4000001000000000000000000000000000000000000000000000000000000">
insert into dbo.IssueFiles3(name, file_stream, path_locator)
select @p_filename, @file, @path_locator option(merge join) </frame>
<frame procname="adhoc" line="4" stmtstart="378" sqlhandle="0x02000000f9e86b3b05d5b67babb9cd12eb0e553806a86e2d0000000000000000000000000000000000000000">
exec dbo.hsp_PutAndRenameDoubleIssueFiles @name, @data </frame>
</executionStack>
<inputbuf>
declare @name nvarchar(256) = cast(newid() as varchar(256)) + '.txt'
declare @text as varchar(max) = '12'
declare @data varbinary(max) = cast(replicate(@text, 300000) as varbinary(max))
exec dbo.hsp_PutAndRenameDoubleIssueFiles @name, @data </inputbuf>
</process>
</process-list>
<resource-list>
<pagelock fileid="1" pageid="7456349" dbid="8" subresource="FULL" objectname="dbo.IssueFiles3" id="lock20cf08600" mode="IX" associatedObjectId="72057626799636480">
<owner-list>
<owner id="process13005a928" mode="IX" />
<owner id="process13005a928" mode="S" requestType="convert" />
</owner-list>
<waiter-list>
<waiter id="process13bedbc38" mode="S" requestType="convert" />
</waiter-list>
</pagelock>
<pagelock fileid="1" pageid="7456349" dbid="8" subresource="FULL" objectname="dbo.IssueFiles3" id="lock20cf08600" mode="IX" associatedObjectId="72057626799636480">
<owner-list>
<owner id="process13bedbc38" mode="IX" />
<owner id="process13bedbc38" mode="S" requestType="convert" />
</owner-list>
<waiter-list>
<waiter id="process13005a928" mode="S" requestType="convert" />
</waiter-list>
</pagelock>
</resource-list>
</deadlock>


Она превратилась во взаимоблокировку преобразования, причем на уровне страницы. Т.е. при вставке в каждом из процессов создается блокировка с намерением на страницу (IX) и эксклюзивная (X) блокировка на строку. Сканирование индекса приводит к тому, что процесс пытается наложить совмещаемую блокировку (S) на страницу. В целом это нормально иметь на одном ресурсе S и IX блокировки, которые сводятся к блокировке SIX Но проблема в том, что SIX блокировки несовместимы с IX блокировками, которые наложены вторым процессом. По этой же причине второй процесс не может получить SIX блокировку, в результате взаимоблокировка.

В общем, идея с тем, что причина взаимоблокировки заключается в сканировании индекса, в первоначальной постановке проблемы, не подтвердилась. Что это было и как докопаться до истины остается загадкой.
Re[3]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Tigor Россия  
Дата: 25.02.15 14:24
Оценка:
А как ты определил, что проблемы происходят на первом операторе инсерт, а не при обработке дубликатов?
К сожалению, в действительности все выглядит иначе, чем на самом деле.
Re[4]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 26.02.15 06:59
Оценка:
Здравствуйте, Tigor, Вы писали:

T>А как ты определил, что проблемы происходят на первом операторе инсерт, а не при обработке дубликатов?


В целом количество дубликатов 1% от общего количества данных. Взаимоблокировки сыпались по несколько в одну минуту. Я рассмотрю, конечно такой вариант, но пока повторить не удалось, поэтому нельзя быть уверенным на все 100%. Но вот что касается моих экспериментов по сканированию индекса в соседней ветке, то там всё надежно, т.е. deadlock гарантировано возникает в begin try. Для этого вставляю данные полученные из deadlock в DMV и получаю инструкцию, на которой возникли проблемы.

executionStack
frame procname=Hermes.dbo.hsp_PutAndRenameDoubleIssueFiles line=34 stmtstart=2618 stmtend=2880 sqlhandle=0x030008008144c276fe8a9a004ba4000001000000000000000000000000000000000000000000000000000000
insert into dbo.IssueFiles(name, file_stream, path_locator)
select @p_filename, @file, @path_locator option(merge join)
frame procname=adhoc line=4 stmtstart=378 sqlhandle=0x0200000018db1b38194fad6aff37c71ec66d21a328e038680000000000000000000000000000000000000000


select qs.sql_handle, qs.statement_start_offset, qs.statement_end_offset,
        substring(st.text, 
                    (qs.statement_start_offset/2) + 1, 
                    ((case qs.statement_end_offset when -1 then datalength(st.text) else qs.statement_end_offset end - qs.statement_start_offset)/2) + 1
                ) as statement_text
from sys.dm_exec_query_stats as qs
cross apply sys.dm_exec_sql_text(qs.sql_handle) as st
cross apply sys.dm_exec_query_plan(qs.plan_handle) as qp
where qs.statement_start_offset = 2618 and qs.statement_end_offset = 2880
    and qp.dbid = 8 
    and qs.sql_handle = 0x030008008144c276fe8a9a004ba4000001000000000000000000000000000000000000000000000000000000
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: wildwind Россия  
Дата: 27.02.15 20:43
Оценка:
Здравствуйте, Olaf, Вы писали:

O>В общем, идея с тем, что причина взаимоблокировки заключается в сканировании индекса, в первоначальной постановке проблемы, не подтвердилась. Что это было и как докопаться до истины остается загадкой.


Тогда выскажу еще версию. Раз задействована FILETABLE, не могут ли в данном случае какие-то блокировки storage уровня (то есть NTFS) транслироваться на уровень БД? На первый взгляд не похоже, но мало ли, от MS всего можно ожидать.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: IB Австрия http://rsdn.ru
Дата: 28.02.15 05:44
Оценка:
Здравствуйте, Olaf, Вы писали:


O>Она превратилась во взаимоблокировку преобразования, причем на уровне страницы. Т.е. при вставке в каждом из процессов создается блокировка с намерением на страницу (IX) и эксклюзивная (X) блокировка на строку. Сканирование индекса приводит к тому, что процесс пытается наложить совмещаемую блокировку (S) на страницу. В целом это нормально иметь на одном ресурсе S и IX блокировки, которые сводятся к блокировке SIX Но проблема в том, что SIX блокировки несовместимы с IX блокировками, которые наложены вторым процессом. По этой же причине второй процесс не может получить SIX блокировку, в результате взаимоблокировка.

Это тот же самый дедлок, просто на другом уровне гранулярности, и причина та же. Так что это скорее подтверждение, чем опровержение )
Мы уже победили, просто это еще не так заметно...
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: IB Австрия http://rsdn.ru
Дата: 28.02.15 05:56
Оценка:
Здравствуйте, Olaf, Вы писали:

O>2. В таблице отсутствует кластерный индекс

на это есть какие-то особые причины?

O>Нет ясного понимания, для каких целей СУБД обращается к записи, которая только что вставлена соседним процессом. Ведь для верификации FK достаточно выполнить index seek, по PK индексу, не затрагивая грязные данные. Единственное объяснение, которое у меня возникло – по каким-то неведомым причинам при вставке одной записи(!) оптимизатор для верификации ссылочной целостности вместо поиска предлагает использовать сканирование индекса.

Собственно scan скорее всего и есть причина дедлока. Получить дедлок, когда два запроса сканируют одну и ту же таблицу/индекс в одной транзакции — совсем не фокус, это, можно сказать, классика.
Следовательно решение проблемы — заставить оптимизатор выбирать план в котором нет сканов в проблемных местах, а используется подходящий индекс по прямому назначению.

O>Второй момент, на котором хотелось бы остановиться – это способ решения проблемы. В таблице из коробки отсутствует кластерный индекс. Цель кластерного индекса, на мой взгляд – снизить время вставки данных в таблицу по сравнению с кучей, уменьшить время блокирования и использования ресурсов и как результат исключить взаимоблокировки.

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

O>1. По каким причинам на ваш взгляд возник deadlock? Действительно ли использовался index scan для верификации ссылочной целостности и почему?

Да, скорее всего в этом.

O>2. Чем смог помочь кластерный индекс в решении проблемы взаимоблокировки?

Изменил план запроса таким образом, что отпала необходимость сканировать индекс, а это и было причиной дедлока.

O>Время вставки должно было сократиться, но что произойдет, если нагрузка увеличится? Так ли это?

Дело не во времени вставки, а в том как именно выполняется проверка. Так что с увеличением нагрузки все должно быть в порядке.
Мы уже победили, просто это еще не так заметно...
Re[3]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 28.02.15 16:38
Оценка:
Здравствуйте, wildwind, Вы писали:

W>Тогда выскажу еще версию. Раз задействована FILETABLE, не могут ли в данном случае какие-то блокировки storage уровня (то есть NTFS) транслироваться на уровень БД? На первый взгляд не похоже, но мало ли, от MS всего можно ожидать.


С этой стороны я тоже пытался рассматривать ситуацию.
Под «капотом» FILETABLE может быть много чего. Единственное, что удалось вытащить — это таблицу filestream_tombstone% для сборщика мусора, на объектах которой тоже возникают блокировки при вставке записей в FILETABLE. Хотя в любых вариациях плана запроса она нигде не фигурирует.
Re[2]: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 28.02.15 16:53
Оценка:
Здравствуйте, IB, Вы писали:

IB>на это есть какие-то особые причины?

Причина одна — вариант поставки FILETABLE из коробки, т.е. таблица с заранее предопределенной структурой, которая создается через инструкцию create table dbo.DocumentStore as filetable; Наличие кластерного индекса просто не предусмотрено разработчиками Microsoft, а я это упустил из виду при эксплуатации.

IB>Собственно scan скорее всего и есть причина дедлока. Получить дедлок, когда два запроса сканируют одну и ту же таблицу/индекс в одной транзакции — совсем не фокус, это, можно сказать, классика.

Полностью согласен. Но запрос, на котором у меня возникает дедлок, уж очень экзотичный insert into dbo.DocumentStore(name, file_stream, path_locator) select @p_filename, @file, @path_locator – вставка одной записи под нагрузкой из переменных в таблицу с FK на саму себя. Здесь и вариантов плана то немного – вставка записи в таблицу и верификация FK через сканирование (1) или поиск (2) по индексу.

IB>Следовательно решение проблемы — заставить оптимизатор выбирать план в котором нет сканов в проблемных местах, а используется подходящий индекс по прямому назначению.

Да, но использование индекса в моем варианте находится под капотом исполнения запроса на INSERT, т.е. для верификации FK я не могу его поменять на более походящий. Максимум, что можно сделать – отключить верификацию FK совсем, либо создать благоприятные условия «снаружи» для использования поиска по PK индексу (кластерный индекс на другом поле, статистики, железо, архивирование данных и т.п.)

IB>Не совсем так. Время блокирования конечно уменьшается, но за счет того, что оптимизатор выбирает совсем другой план, где нет сканирования и, как следствие, в принципе убрана эта причина нарваться на дедлок.

Оптимизатору конечно виднее, что и когда использовать. С толку сбивали следующие факты, хотя если кто-то еще считает, что это был скан наверно есть повод уже успокоиться
1. Верификация FK осуществляется по PK не кластерному индексу. Вставляется одна запись причем из переменных. Казалось бы при таких благоприятных условиях нет повода использовать сканирование, тем не менее это возможно.
2. Кластерный индекс создан на поле, которое не является PK. Его влияние в плане минимально. Единственное место, где он может быть задействован – вставка записи. Т.е. если смотреть на план запроса, то без кластерного индекса, данные вставляются в кучу (Table Insert оператор). C кластерным индексом вставка идет в него самого. По идее, все на что может повлиять кластерный индекс физическая (логическая) организация таблицы и ее индексов. И предпосылок для смены плана нет, т.е. необходимости перехода от сканирования к поиску и наоборот.
3. В распоряжении имеется сервер и БД на котором были deadlock. Единственное отличие от картины, которая была на момент возникновения взаимоблокировок — отсутствие пользователей, т.е. нагрузка сейчас равна нулю. Так вот в таких условиях я не могу повторить ситуацию с дедлоком, как бы не старался, сколько бы соединений параллельно не открывал — ни разу не удалось его словить. Искусственный вариант с option merge не в счет.
Re: [SQL Server] FILETABLE, deadlock и кластерный индекс
От: Olaf Россия  
Дата: 17.03.15 12:02
Оценка: 162 (2)
Здравствуйте, Olaf, Вы писали:

O>Возможно, кто-то решал аналогичные проблемы или поделится своим мыслями относительно прочитанного, буду премного благодарен.


O>Резюмируя, опишу кратко свои вопросы:

O>1. По каким причинам на ваш взгляд возник deadlock? Действительно ли использовался index scan для верификации ссылочной целостности и почему? Или истинная причина в чем-то другом?
O>2. Чем смог помочь кластерный индекс в решении проблемы взаимоблокировки? Время вставки должно было сократиться, но что произойдет, если нагрузка увеличится? Так ли это?

Удалось поймать взаимоблокировку, причем необязательно было давать высокую нагрузку, достаточно ~20 параллельных выполнений запроса на INSERT. Анализ плана запроса показал, что верификация FK выполняется через поиск по индексу, никакого скана в данном случае нет. Решил включить трассировку и посмотреть на одиночном запросе, а что же происходит в БД кроме моей нагрузки, и вот что обнаружил. Между двумя событиями SQL:StmtStarting и SQL:StmtComplleted возникающими при выполнении запроса на insert into dbo.IssueFiles(name, file_stream, path_locator) select @filename, @file, @pl выполняется еще достаточное количество обращений к этой же таблице. Причем реализовано, как я понимаю это на уровне ядра БД, т.к. нигде в планах данная информация не фигурирует.

/*SQL:StmtStarting */ insert into dbo.IssueFiles(name, file_stream, path_locator) select @filename, @file, @pl

set @path_locator = convert(hierarchyid, @path_locator_bin) /* Parent validation is not necessary for top level rows. */     
if (@path_locator.GetLevel() = 1)
set @parent_is_directory = 1     
/*1*/ select @parent_is_directory = is_directory from [dbo].[IssueFiles] where path_locator = @path_locator.GetAncestor(1)
if (@parent_is_directory = 0)
if (@is_directory = 1)        
/*2*/ if exists (select 1 from [dbo].[IssueFiles] where parent_path_locator = @path_locator)
set @return_value = 1
goto complete
set @path_locator = convert(hierarchyid, @path_locator_bin)
/*3*/ select @dummy1 = sum(convert(bigint, is_archive)) from [dbo].[IssueFiles] with (xlock, rowlock, index([PK__IssueFil__5A5B77D5C0E48F84]), forceseek) 
   where path_locator.IsDescendantOf(@path_locator) = 1
set @return_value = 1

/*SQL:StmtComplleted*/  insert into dbo.IssueFiles(name, file_stream, path_locator) select @filename, @file, @pl

В целом видно, что кроме вставки в кучу и верификации FK выполняется еще три чтения FILTABLE. Причем у одного из запросов на выборку стоит подсказка к запросу xlock. C определенной долей уверенности можно сказать, что причина дедлока не в верификации FK как таковой, а в совокупности обращений к таблице, где по всей видимости присутствует сканирование.

Более детально наверно сложно проанализировать эту ситуацию, т.к. внутренние запросы, которые выполнятся дополнительно, остаются за бортом и я не вижу пока возможности проанализировать и сравнить планы выполнения в каждом из вариантов. Очевидно, что кластерный индекс кардинально меняет картину ...

Если последовательно запустить три запроса на больших объемах данных в таблице без кластерного индекса, то видно, что второй запрос использует сканирование PK индекса, хотя в таблице присутствует подходящий уникальный индекс, по которому можно выполнить поиск. Добавление кластерного индекса приводит к тому, что оптимизатор правильно разбирается в ситуации и использует поиск по соответствующему индексу, именно это и приводит к исчезновению дедлока. Вот собственно и все ответы на вопросы. Открытым остается только один вопрос, а почему Microsoft изначально не предусмотрел наличие кластерного индекса в таблице ?!
Отредактировано 18.03.2015 6:15 Olaf . Предыдущая версия .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.