Ошибки при входе в приложение 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... ПО ВСЕМ СЕРВЕРУ... ПОСЛЕ ЛОГОТИНА...)