Ошибки при входе в приложение ASP, вызванные сбросом/созданием представлений SQL Server

Я преследовал эту проблему уже целый день, и я был в тупике, поэтому подумал, что я расскажу вам о людях для какого-то вдохновения. Я немного новичок, когда дело доходит до тупиков и режимов блокировки SQL Server, мне редко приходится вникать в это.

Рассказ:

Когда пользователь регистрируется в нашем приложении, мы хотим обновить представление SQL Server на основании того факта, что теперь у них есть "сеанс", поэтому, когда они впоследствии запускают отчет служб отчетов SQL Server на основе модели отчета, он включает параметры безопасности для своего сеанса.

Регулярный тупик, который я заметил, происходит между процессом, который DROPs и reCREATEs вид (который я вызываю AuthRuleCache), и отчет Microsoft SQL Server Reporting Services 2008 (SSRS), который пытается выбрать из представления.

Если я правильно прочитал событие тупика SQL Profiler, в AuthRuleCache есть блокировка Sch-M, и отчет имеет блокировку IS.

Код AuthRuleCache - это С# в сборке DotNet, он выполняется, когда пользователи входят в наше приложение Classic ASP.

Очевидно, я хочу избежать тупика, потому что он предотвращает вход в систему - я не возражаю, как я это достигаю, пока мне не нужно компрометировать какие-либо другие функции. Я получил полный контроль над AuthRuleCache и базой данных, но я бы сказал, что мы "легки" на экспертизе бизнес-приложений.

Вот пример события блокировки SQL Profiler:

<deadlock-list>
 <deadlock victim="process4785288">
  <process-list>
   <process id="process4785288" taskpriority="0" logused="0" waitresource="OBJECT: 7:617365564:0 " waittime="13040" ownerId="3133391" transactionname="SELECT" lasttranstarted="2013-01-07T15:16:24.680" XDES="0x8005bd10" lockMode="IS" schedulerid="8" kpid="20580" status="suspended" spid="83" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2013-01-07T15:15:55.780" lastbatchcompleted="2013-01-07T15:15:55.780" clientapp=".Net SqlClient Data Provider" hostname="MYMACHINE" hostpid="1176" loginname="MYMACHINE\MyUser" isolationlevel="read committed (2)" xactid="3133391" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="adhoc" line="2" stmtstart="34" sqlhandle="0x02000000bd919913e43fd778cd5913aabd70d423cb30904a">
SELECT
    CAST(1 AS BIT) [c0_is_agg],
    1 [agg_row_count],
    COALESCE([dbo_actions2].[ActionOverdue30days], 0) [ActionOverdue30days],
    COALESCE([dbo_actions3].[ActionOverdueTotal], 0) [ActionOverdueTotal],
    COALESCE([dbo_actions4].[ActionOverdue90daysPLUS], 0) [ActionOverdue90daysPLUS],
    COALESCE([dbo_actions5].[ActionOverdue60days], 0) [ActionOverdue60days],
    COALESCE([dbo_actions6].[ActionOverdue90days], 0) [ActionOverdue90days],
    COALESCE([dbo_actions7].[ActionPlanned30days], 0) [ActionPlanned30days],
    COALESCE([dbo_actions8].[ActionPlanned60days], 0) [ActionPlanned60days],
    COALESCE([dbo_actions9].[ActionPlanned90days], 0) [ActionPlanned90days],
    COALESCE([dbo_actions10].[ActionPlanned90daysPLUS], 0) [ActionPlanned90daysPLUS],
    COALESCE([dbo_actions11].[ActionPlannedTotal], 0) [ActionPlannedTotal],
    CASE WHEN [dbo_actions12].[CountOfFilter] > 0 THEN 'Overdue0-30days' WHEN [dbo_actions13].[CountOfFilter] > 0 THEN 'Overdue90daysPlus' WHEN [dbo_actions5].[Count     </frame>
    </executionStack>
    <inputbuf>
  SET DATEFIRST 7
  SELECT
    CAST(1 AS BIT) [c0_is_agg],
    1 [agg_row_count],
    COALESCE([dbo_actions2].[ActionOverdue30days], 0) [ActionOverdue30days],
    COALESCE([dbo_actions3].[ActionOverdueTotal], 0) [ActionOverdueTotal],
    COALESCE([dbo_actions4].[ActionOverdue90daysPLUS], 0) [ActionOverdue90daysPLUS],
    COALESCE([dbo_actions5].[ActionOverdue60days], 0) [ActionOverdue60days],
    COALESCE([dbo_actions6].[ActionOverdue90days], 0) [ActionOverdue90days],
    COALESCE([dbo_actions7].[ActionPlanned30days], 0) [ActionPlanned30days],
    COALESCE([dbo_actions8].[ActionPlanned60days], 0) [ActionPlanned60days],
    COALESCE([dbo_actions9].[ActionPlanned90days], 0) [ActionPlanned90days],
    COALESCE([dbo_actions10].[ActionPlanned90daysPLUS], 0) [ActionPlanned90daysPLUS],
    COALESCE([dbo_actions11].[ActionPlannedTotal], 0) [ActionPlannedTotal],
    CASE WHEN [dbo_actions12].[CountOfFilter] > 0 THEN 'Overdue0-30days' WHEN [dbo_actions13].[CountOfFilter] > 0 THEN 'Overdue90daysPlus' WHEN [db    </inputbuf>
   </process>
   <process id="process476ae08" taskpriority="0" logused="16056" waitresource="OBJECT: 7:1854941980:0 " waittime="4539" ownerId="3132267" transactionname="user_transaction" lasttranstarted="2013-01-07T15:16:18.373" XDES="0x9a7f3970" lockMode="Sch-M" schedulerid="7" kpid="1940" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2013-01-07T15:16:33.183" lastbatchcompleted="2013-01-07T15:16:33.183" clientapp=".Net SqlClient Data Provider" hostname="MYMACHINE" hostpid="14788" loginname="MYMACHINE\MyUser" isolationlevel="read committed (2)" xactid="3132267" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
    <executionStack>
     <frame procname="adhoc" line="3" stmtstart="202" stmtend="278" sqlhandle="0x02000000cf24d22c6cc84dbf398267db80eb194e79f91543">
  DROP VIEW [sec].[actions_authorized]     </frame>
    </executionStack>
    <inputbuf>

  IF EXISTS ( SELECT * FROM sys.VIEWS WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized]'))
  DROP VIEW [sec].[actions_authorized]
      </inputbuf>
   </process>
  </process-list>
  <resource-list>
   <objectlock lockPartition="0" objid="617365564" subresource="FULL" dbid="7" objectname="617365564" id="lock932d2f00" mode="Sch-M" associatedObjectId="617365564">
    <owner-list>
     <owner id="process476ae08" mode="Sch-M"/>
    </owner-list>
    <waiter-list>
     <waiter id="process4785288" mode="IS" requestType="wait"/>
    </waiter-list>
   </objectlock>
   <objectlock lockPartition="0" objid="1854941980" subresource="FULL" dbid="7" objectname="1854941980" id="locke6f0b580" mode="IS" associatedObjectId="1854941980">
    <owner-list>
     <owner id="process4785288" mode="IS"/>
    </owner-list>
    <waiter-list>
     <waiter id="process476ae08" mode="Sch-M" requestType="convert"/>
    </waiter-list>
   </objectlock>
  </resource-list>
 </deadlock>
</deadlock-list>

ДЛИННАЯ история:

Я решил сделать это как Q & A.

В: Почему вам приходится часто менять схемы, чтобы обеспечить безопасность отчетов?

A: Я пришел к этому только потому, что наш механизм отчетности SSRS полностью основан на моделях отчетов, и наше приложение поддерживает безопасность на уровне строк, применяя правила. Сами правила определены в базе данных как небольшие фрагменты SQL. Эти фрагменты повторно собираются во время выполнения и применяются на основе a), кто пользователь, b) что они пытаются сделать, и c) что они пытаются сделать. Таким образом, каждый пользователь может иметь уникальный вид данных на основе правил, которые применяются к ним. У нас есть пользователи, которые создают и сохраняют свои собственные отчеты, поэтому я хотел, чтобы эта система безопасности использовалась в модели, чтобы они не наткнулись на данные, к которым у них не должно быть доступа.

Задача, с которой мы столкнулись с моделями отчетов, состоит в том, что они основаны на представлении источника данных (DSV), который может состоять только из статических источников, например. таблицы, named-queries, views. Вы не можете вводить некоторый код С# в DSV, чтобы заставить его динамически реагировать на конкретного пользователя, выполняющего отчет. Вы получаете UserID на модели (SMDL), чтобы использовать ее для фильтрации. Наше решение состоит в том, чтобы заставить DSV отображать представление со всеми данными для ВСЕХ уникальных наборов правил пользователей (а именно AuthRuleCache), а затем SMDL будет отфильтровывать это обратно до уникального набора правил запрашивающего пользователя. Hey-presto, у вас есть динамическая система на уровне строк, основанная на правилах в модели отчетов SSRS!

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

Q: Какую форму принимает AuthRuleCache?

A: Это представление, объединяющее несколько других представлений. Каждый пользователь имеет свой собственный вид, например. widgets_authorized_123, где widgets - это таблица, содержащая защищенные данные, а 123 - идентификатор пользователя. Затем открывается главный вид (например, widgets_authorized), в котором UNION объединяют все пользовательские представления

В: Это звучит ужасно неэффективно, вы идиот?

A: Возможно, однако, благодаря удивительности SQL Query Processor, все это, похоже, работает хорошо и быстро для отчетов в реальном времени. Я экспериментировал с использованием таблицы кэша для фактического хранения идентификаторов записей для использования с безопасностью приложений и обнаружил, что это привело к раздутым таблицам и задержкам обновления и чтения из кеша.

Q: Хорошо, вы все равно можете быть идиоткой, но пусть изучит другой вариант. Можете ли вы перестроить AuthRuleCache асинхронно, а не ждать пользователя при входе в систему?

A: Ну, первое, что пользователь делает после входа в систему, попадает на панель мониторинга, содержащую отчеты на основе модели, поэтому нам необходимо, чтобы правила безопасности работали сразу после входа в систему.

Q: Вы изучили различные режимы блокировки и уровни изоляции?

A: Сортировка - я попытался включить изменение базы данных read_committed_snapshot ON, но это, казалось, не имело значения. В ретроспективе я думаю, что тот факт, что я пытаюсь сделать DROP/CREATE VIEW и требующий блокировки Sch-M, означает, что Read Committed Snapshot Isolation (RCSI) не помогло бы, потому что это касалось обработки concurrency операторов DML, и я делаю DDL.

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

A: Я бы не стал это делать, но я надеялся, что вы скорее найдете решение, ориентированное на приложения, а не сделайте инфраструктурные изменения. Это будет переход на использование ресурсов и накладные расходы на техническое обслуживание, которые мне нужно будет эскалации для других людей.

Q: Что-нибудь еще мы должны знать?

A: Да, процесс обновления AuthRuleCache завернут в транзакцию, потому что я хотел сделать сир, чтобы никто не смог увидеть неполный/недействительный кеш, например. widget_authorized view, ссылаясь на widget_authorized_123, когда widget_authorized_123 был удален, поскольку пользовательский сеанс истек. Я тестировал без транзакции, и тупики остановились, но я начал получать блокированные отчеты о процессах от SQL Profiler. Я видел ~ 15 секунд задержки при входе в систему, а иногда и таймауты - так что верните транзакцию.

Q: Как часто это происходит?

A: AuthRuleCache отключается в рабочей среде в настоящий момент, поэтому он не влияет на пользователей. Мое локальное тестирование 100 последовательных входов в систему показывает, что, возможно, 10% тупик или сбой. Я подозреваю, что это хуже для пользователей, у которых на панели управления есть отчет, основанный на модели отчетов.

В: Как получить моментальные снимки отчета?

A: Может быть, возможность - не знаю, насколько хорошо это работает с параметризованными отчетами. Меня беспокоит то, что у нас есть некоторые пользователи, которые будут встревожены, если они вставляют запись, но не видят ее на панели инструментов до получаса. Кроме того, я не всегда могу гарантировать, что все будут использовать снимки отчетов правильно все время, поэтому не хочу оставлять дверь открытой для тупиков, чтобы снова проникнуть на более позднюю дату.

В: Могу ли я увидеть полный T-SQL транзакции обновления AuthRuleCache?

A: Вот инструкции, выпущенные внутри одной транзакции, захваченной из SQL Profiler, для входа одного пользователя:

Ищите истекшие сеансы - мы удалим связанный с ним просмотр, если нашли

SELECT TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA + '.' + TABLE_NAME LIKE 'sec.actions_authorized_%'
  AND RIGHT(TABLE_NAME, NULLIF(CHARINDEX('_', REVERSE(TABLE_NAME)), 0) - 1) NOT IN (
    SELECT DISTINCT CAST(empid AS NVARCHAR(20))
    FROM session
    )

Отбросить любое ранее существующее представление для пользователя myuser, id 298

IF EXISTS (
    SELECT *
    FROM sys.VIEWS
    WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized_298]')
    )
  DROP VIEW [sec].[actions_authorized_298]

Создать представление для пользователя id 298

CREATE VIEW [sec].[actions_authorized_298]
AS
SELECT actid
  ,'myuser' AS username
FROM actions
WHERE actid IN (
    SELECT actid
    FROM actions
    WHERE (
        --A bunch of custom where statements generated from security rules in the system prior to this transaction starting
    )

Получить список всех пользовательских представлений для сущности действий

SELECT TABLE_SCHEMA + '.' + TABLE_NAME
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA + '.' + TABLE_NAME LIKE 'sec.actions_authorized_%'

Отбросить существующее представление основных действий

IF EXISTS (
    SELECT *
    FROM sys.VIEWS
    WHERE object_id = OBJECT_ID(N'[sec].[actions_authorized]')
    )
  DROP VIEW [sec].[actions_authorized]

Создайте новое представление основных действий, и мы закончили

CREATE VIEW [sec].[actions_authorized]
AS
SELECT actid
  ,username
FROM sec.actions_authorized_182    
UNION
SELECT actid
  ,username
FROM sec.actions_authorized_298
UNION
-- Repeat for a bunch of other per-user custom views, generated from the prior select
-- ...

Ответ 1

Спасибо всем, кто предложил предложения. Я решился на решение, которое, я думаю, будет работать для нас. Может быть какое-то время, пока я не получу окончательный код вместе, но я сделал несколько тестов и выглядел позитивно - я хотел закрыть этот вопрос с помощью моего запланированного подхода.

Во-первых, тупики являются полностью подходящим следствием того, что я пытался сделать с самого начала. Насколько я понимаю, для воссоздания представления требуется блокировка модификации схемы, и любой процесс в середине чтения из этого представления требует блокировки стабильности схемы. В зависимости от времени эти конкурирующие блокировки приводили к тупиковой ситуации примерно в 10% попыток входа во время периодов занятости.

Когда я сменил код, чтобы выполнить SERIALISABLE SET TRANSACTION ISOLATION LEVEL SERIALIZABLE перед запуском просмотра drop/rereate, тупики ушли, потому что он гораздо более ограничивает то, что может произойти одновременно, жертвуя скоростью ответа для стабильности.

К сожалению, вместо блокировки я видел блокированные отчеты о процессах, в которых процессы ожидали более 10 секунд, чтобы получить необходимые блокировки. Тем не менее, я не решаю проблему.

У меня было переосмыслить мое "странное решение" использования большого представления UNIONed для объединения нескольких видов. Позвольте мне пояснить, что я не пришел к такому подходу по своему выбору, я просто пытаюсь обойти ограничения в моделях отчетов SSRS, в которых вы не можете реализовать параметры в запросах таблиц/именованных, лежащих в основе модели.

Я нашел в документации MS, что Partitioned Views могут использовать подобную структуру при объединении строк из нескольких таблиц в один вид, например здесь: http://msdn.microsoft.com/en-us/library/ms190019(v=sql.105).aspx

Таким образом, я не одинок в использовании представлений таким образом. Мне нужно это представление UNIONed, но падение и воссоздание просмотров будет проблемой производительности. Итак, я провел некоторое тестирование с помощью Service Broker и обнаружил, что могу приостановить операцию просмотра/воссоздания представления, что позволяет пользователям быстро входить в систему, не дожидаясь завершения DDL. Я собираюсь следовать рекомендациям @usr и получать транзакцию как можно более тонкими, перемещая вещи, не имеющие решающего значения для завершения входа в систему (например, истечения старых сеансов) из транзакции.

Ответ 2

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

Когда вы используете User_ID, я предполагаю, что у вас есть другие таблицы с входами пользователя.

Пользователи (User_ID, Login) Виджеты (Widget_ID,...) widgets_authorized (User_ID, Widget_ID)

Переименуйте таблицу Widgets в AllWidgets

Создать вид Виджеты:

CREATE VIEW widgets
AS
SELECT AW.*
FROM AllWidgets AW
INNER JOIN widgets_authorized WA ON WA.Widget_ID = AW.Widget_ID
INNER JOIN Users U ON WA.User_ID = U.User_ID
WHERE U.Login = SYSTEM_USER

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

Если у вас есть проблемы с производительностью, попробуйте это, у меня возникла аналогичная проблема:

CREATE VIEW widgets
AS
SELECT AW.*
FROM AllWidgets AW
INNER JOIN widgets_authorized WA ON WA.Widget_ID = AW.Widget_ID
WHERE WA.User_ID IN (SELECT U.User_ID FROM Users U WHERE U.Login = SYSTEM_USER)

Ответ 3

Еще одно предложение ближе к вашему странному решению.

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

Запустите запрос с помощью "FROM actions_authorized", не явную схему, механизм sql будет использовать представление, принадлежащее подключенной пользовательской схеме.

Схема и ее представления могут быть созданы с помощью фонового процесса или при входе пользователя (CREATE TRIGGER... ПО ВСЕМ СЕРВЕРУ... ПОСЛЕ ЛОГОТИНА...)