Взаимоблокировки в 1С: почему разбор одного дедлока почти всегда даёт неверный ответ
Иван Недомолков · Опубликовано: · Обновлено:
Любую отдельную взаимоблокировку всегда можно разобрать подробно и красиво. Берёте отчёт, раскладываете по ролям, получаете складную историю с виновником и рекомендацией. Проблема в том, что один случай статистически не отличается от совпадения, и вывод из него почти наверняка неверный.
Эта статья про метод, который сработал: не разбирать случай, а считать частоты. На реальном инциденте в складской системе крупной розничной сети он за полчаса дал ответ, которого мы до этого сутки искали руками. Ниже полный путь с запросами, которые можно скопировать: как снять отчёты, как посчитать, как от имени таблицы в SQL Server дойти до объекта конфигурации и что делать, когда виновник найден.
Короткая версия этой истории, без прямых обращений к служебным представлениям СУБД, опубликована на Инфостарте: “Почему бесполезно разбирать один дедлок”. Здесь версия полная.
Симптом: ошибка 1205 и что она значит
Пользователь видит, что документ не проводится. В журнале регистрации или в окне сообщений лежит примерно такое:
Ошибка при выполнении запроса. по причине:
Ошибка СУБД:
Microsoft SQL Server Native Client 11.0: Transaction (Process ID 118) was deadlocked
on lock resources with another process and has been chosen as the deadlock victim.
Rerun the transaction.
HRESULT=80040E14, SQLSrvr: SQLSTATE=40001, native=1205
Ключевое здесь - число 1205 и слово deadlock victim. Перевод на человеческий: две (или больше) транзакции встали в тупик, каждая держит то, что нужно другой, сама эта ситуация не рассосётся никогда, поэтому SQL Server выбрал одну из них жертвой и откатил. Откат мгновенный, ждать сервер не заставляет.
Слово “жертва” сбивает с толку. Жертва - это не виновник и не пострадавший по существу, это просто транзакция, откат которой сервер посчитал дешевле. Выбирают по объёму проделанной работы: у кого меньше записано в журнал транзакций, того и откатывают. Поэтому регулярно откатывается лёгкая операция, а тяжёлая, которая и создала проблему, доезжает до конца. Смотреть надо не на того, кого убили.
Дедлок СУБД и конфликт управляемых блокировок это разные болезни
Половина времени на таких инцидентах уходит на то, что лечат не ту болезнь. В 1С есть два принципиально разных механизма, и сообщения у них похожи ровно настолько, чтобы их путать.
| Конфликт управляемых блокировок | Взаимоблокировка СУБД | |
|---|---|---|
| Текст | ”Конфликт блокировок при выполнении транзакции" | "…deadlocked on lock resources…”, native=1205 |
| Кто ставит | менеджер блокировок 1С, поверх СУБД | сам SQL Server |
| Что произошло | ждала и не дождалась, вышел таймаут | тупик, ждать бессмысленно |
| Сколько ждали | обычно 20 секунд (таймаут по умолчанию) | доли секунды, откат сразу |
| Где искать | технологический журнал: TLOCK, TTIMEOUT, TDEADLOCK | system_health в SQL Server, событие xml_deadlock_report |
| Чем лечится | порядок захвата, длина транзакции, гранулярность | тем же плюс планом запроса и индексами |
Различить их можно за полминуты, ещё до всякого разбора: если пользователь смотрел на “висит” секунд двадцать и потом получил ошибку - это управляемые блокировки и таймаут. Если ошибка выпала мгновенно и в тексте есть 1205 - это дедлок СУБД, и дальше по этой статье.
Бывает и обратная ситуация: конфликтов нет вовсе, потому что управляемые замки, на которые рассчитывает код, в реальности не ставятся - процедура с правильным именем отрабатывает и ничего не блокирует. Этот класс разобран отдельно: управляемая блокировка 1С умеет молча не работать.
Есть и третий вариант, который путают с первыми двумя чаще всего. Текст “В данной транзакции уже происходили ошибки” не говорит вообще ничего о причине: платформа выдаёт его одинаково и на дедлок, и на проглоченную бизнес-проверку прикладного кода. У нас на таком инциденте разошлись два счётчика журнала - 24 записи с этим текстом и код 1205 только у 19, - и ответ оказался на дешёвой стороне.
Про первую половину, включая управляемые блокировки, эскалацию и параллелизм, у нас есть отдельный разбор: почему в час пик кассы встают на минуту. Там же про то, как ставить диагноз по техжурналу. Здесь мы весь текст сидим во второй половине - в дедлоках СУБД, и приходим к выводу, который в той статье не назван главным.
Жалоба указывает на симптом, а не на виновника
Началось всё буднично, с жалобы “реализация не создаётся, блокировка”. Складская система на 1С 8.3, СУБД MS SQL Server.
Первый же шаг стоил нам суток. Мы нашли таблицу хранения объекта из жалобы и начали разбирать на ней блокировки: раз не проводится реализация, смотреть надо реализацию, логика очевидная.
Ошиблись мы в этой гипотезе дважды. Когда сопоставляли бизнес-имя с именем таблицы, перепутали объект и разбирали таблицу приходного ордера, считая её таблицей расходного. А кроме того, виновником не оказалась даже верная таблица из жалобы.
Объект из жалобы показывает, где заболело. Виновником же может быть совсем другой объект, и чаще всего так и выходит: от пользователя приходит симптом, а диагноз ставим мы. Ничего другого он сообщить не может, и выпытывать у него подробности бесполезно.
Отсюда практическое следствие, к которому мы пришли дорогой ценой: в чужой конфигурации расследование начинают с сопоставления бизнес-имён объектов и имён таблиц хранения. Без такой карты вы будете ходить по кругу и не заметите этого.
От имени таблицы в SQL Server к объекту конфигурации
Карта строится платформенным методом, и это правильный способ - он переживает обновления и не требует прав в СУБД:
// Соответствие "объект конфигурации - имя таблицы в СУБД".
// Второй параметр Истина: вернуть имена так, как они лежат в базе данных.
Структура = ПолучитьСтруктуруХраненияБазыДанных(, Истина);
Для Каждого Строка Из Структура Цикл
Если ВРег(Строка.ИмяТаблицыХранения) = ВРег("_Document123") Тогда
Сообщить(Строка.ИмяТаблицы + " -> " + Строка.ИмяТаблицыХранения
+ " (" + Строка.Назначение + ")");
КонецЕсли;
КонецЦикла;
Выгружать всю карту разом тоже полезно: она пригодится и в следующем расследовании, и при чтении планов запросов, где имена таблиц всегда служебные. Мы это в итоге завернули в отдельную обработку - Трансформатор SQL в запрос 1С, и написана она была именно после потерянных суток.
Если доступа к базе через конфигуратор или внешнюю обработку нет, а SQL есть, минимум про таблицу можно узнать прямо из СУБД: её размер, число строк и набор колонок часто уже позволяют опознать объект.
-- Что за таблица, сколько в ней строк и места. Фильтр по текущей базе
-- обязателен: инстанс под 1С почти всегда общий на несколько баз.
SELECT t.name AS ImyaTablicy,
p.rows AS Strok,
SUM(a.total_pages) * 8 / 1024 AS MB,
i.name AS ImyaIndeksa,
i.type_desc AS TipIndeksa
FROM sys.tables AS t
JOIN sys.indexes AS i ON i.object_id = t.object_id
JOIN sys.partitions AS p ON p.object_id = t.object_id AND p.index_id = i.index_id
JOIN sys.allocation_units AS a ON a.container_id = p.partition_id
WHERE t.name = '_Document123'
GROUP BY t.name, p.rows, i.name, i.type_desc
ORDER BY MB DESC;
Снимайте system_health немедленно, буфер живёт минуты
Отчёты о взаимоблокировках в MS SQL собирает служебная сессия system_health.
Работает она по умолчанию, настраивать её не нужно, и на каждый дедлок в кольцевом
буфере появляется XML-отчёт: какие сессии столкнулись, что они выполняли и на каких
ресурсах застряли.
Главное слово здесь “кольцевой”. Размер памяти фиксирован, свежие события пишутся поверх старых. Буфер вмещает порядка сотни событий и в шторм перезаписывается за минуты, так что доказательства исчезают, пока вы решаете, смотреть ли.
Правило простое: снимать сразу, разбираться потом. Сохранённый XML никуда не денется, а буфер денется.
И отдельно про пустой буфер, потому что вывод из него делают неверный. Пустота здесь означает три разные вещи: дедлоков не было, буфер перетёрся штормом, буфер сброшен перезапуском экземпляра или переключением основного узла. Как их различить запросом до того, как объявлять, что взаимоблокировок нет, разобрано в статье про то, как диагностика показывает не то, что происходило.
Если инцидент повторяющийся и есть возможность подготовиться заранее, кольцо лучше
заменить на файл. Отдельная сессия Extended Events работает по той же механике,
что system_health, но пишет на диск и переживает и шторм, и перезапуск службы:
-- Постоянная сессия сбора дедлоков в файл. Поднимается один раз,
-- стартует вместе с сервером, живёт до 640 МБ истории (64 x 10).
CREATE EVENT SESSION [Deadlocks_1C] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file (
SET filename = N'D:\XE\deadlocks_1c.xel',
max_file_size = 64, -- МБ на файл
max_rollover_files = 10
)
WITH (
MAX_MEMORY = 4096 KB,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY = 30 SECONDS,
STARTUP_STATE = ON -- поднимется после перезапуска службы
);
GO
ALTER EVENT SESSION [Deadlocks_1C] ON SERVER STATE = START;
Читается она потом так:
-- Разбор накопленного файла. Маска со звёздочкой обязательна:
-- файлов несколько, они ротируются.
SELECT CAST(event_data AS XML) AS Graf
FROM sys.fn_xe_file_target_read_file(N'D:\XE\deadlocks_1c*.xel', NULL, NULL, NULL);
Есть и третий путь, старый и грубый: флаг трассировки 1222 заставляет SQL Server писать разбор каждой взаимоблокировки в журнал ошибок. Он удобен тем, что включается одной командой и ничего не требует настраивать, и неудобен тем, что журнал ошибок превращается в свалку, а разбирать текстовый формат тяжелее, чем XML.
DBCC TRACEON (1222, -1); -- -1 значит для всех сессий, до перезапуска службы
Считаем частоты: тридцать три отчёта вместо одного
За тридцать минут мы сняли 33 отчёта. У каждого внутри узел resource-list со
списком ресурсов, на которых застряли сессии, и суммарно упоминаний в этих списках
вышло 220, в среднем шесть-семь на отчёт.
Поначалу число настораживает: в учебном примере с двумя сессиями ресурсов бывает один-два, а здесь около семи. Объяснений два, и оба нормальные. В шторм взаимоблокировки редко бывают парными: SQL Server разрывает кольцо из трёх сессий и больше, и в список попадают все звенья. Кроме того, один объект даёт несколько строк разных типов ресурса: страницу (блок 8 КБ), ключ (строку индекса), объект целиком. Статистике это только на пользу, длинный список делает её надёжнее.
Дальше арифметика. Одна таблица собрала 187 упоминаний из 220, это 85 процентов. Принадлежит она расходному ордеру, тому самому, который в первый день мы приняли за приходный. А реализации, на которую жаловались, среди виновников не нашлось ни разу.
Вручную отчёты не разбирают, для этого есть запрос. Он разворачивает resource-list
из каждого XML в строки и группирует их:
-- Частоты по отчётам о взаимоблокировках из кольцевого буфера system_health.
WITH Grafy AS (
SELECT CAST(target_data AS XML) AS TD
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s
ON s.address = st.event_session_address
WHERE s.name = 'system_health'
AND st.target_name = 'ring_buffer'
),
Sobytiya AS (
SELECT X.value('(@timestamp)[1]', 'datetime2') AS Moment,
X.query('.') AS Graf
FROM Grafy
CROSS APPLY TD.nodes('//event[@name="xml_deadlock_report"]') AS T(X)
),
Resursy AS (
SELECT s.Moment,
R.value('@objectname', 'nvarchar(256)') AS ImyaObiekta,
R.value('local-name(.)', 'nvarchar(64)') AS TipResursa
FROM Sobytiya s
CROSS APPLY s.Graf.nodes('//resource-list/*') AS T2(R)
WHERE R.value('@objectname', 'nvarchar(256)') IS NOT NULL
)
SELECT ImyaObiekta,
TipResursa,
COUNT(*) AS Upominaniy,
COUNT(DISTINCT Moment) AS Otchetov
FROM Resursy
GROUP BY ImyaObiekta, TipResursa
ORDER BY Upominaniy DESC;
Счётных колонок две, и это важно. В колонке упоминаний видно, как часто объект встречался вообще, в колонке отчётов - число разных дедлоков, где он замешан. Когда упоминаний много, а отчётов всего два, перед вами один затык, системной проблемы тут нет. Искать надо объект, у которого оба числа большие.
Наши 187 из 220 получатся, если сделать ещё один проход и сгруппировать только по объекту, не разделяя типы ресурса:
-- Тот же расчёт, свёрнутый до объекта: доля каждого объекта во всех упоминаниях.
SELECT ImyaObiekta,
COUNT(*) AS Upominaniy,
COUNT(DISTINCT Moment) AS Otchetov,
CAST(100.0 * COUNT(*) / SUM(COUNT(*)) OVER () AS decimal(5,1)) AS ProcentOtVsego
FROM Resursy
GROUP BY ImyaObiekta
ORDER BY Upominaniy DESC;
Три грабли на этом запросе
Один объект размазывается по строкам. Если группировать по объекту вместе с типом ресурса, на страницу, ключ и объект целиком получатся отдельные строки. Сама по себе такая разбивка полезна, по ней сразу понятно, где идёт конфликт, на целых страницах или на диапазонах ключей, но долю объекта из неё не вычислить. Долю даёт второй запрос выше.
target_data приходит обрезанным. У кольцевого буфера это nvarchar(max), и на
больших буферах СУБД отдаёт его не целиком. Разбор обрезанного документа через
CAST(... AS XML) валится с ошибкой, и со стороны кажется, что неверно написан сам
запрос. Час на это потерять легко. Лечится либо снятием буфера до переполнения, либо
собственной сессией с записью в файл - той самой, что выше.
nodes() поверх CAST внутри CTE может выполниться не один раз. Когда отчётов
сотня, по времени это уже видно. Если запрос нужен регулярно, Sobytiya лучше один
раз положить во временную таблицу и разворачивать её.
Список ресурсов это факт, текст запроса это гипотеза
Отчёт о взаимоблокировке содержит обе вещи, список ресурсов и текст запроса. Их нельзя путать, но путают постоянно.
По тексту запроса видно, чем сессия занималась. В нём может встречаться десяток таблиц, и до части из них дело так и не дошло. Это гипотеза.
По списку ресурсов видно, на чём сессия застряла. Это факт, зафиксированный сервером в момент конфликта.
Считать частоты надо по ресурсам. Если считать по текстам запросов, в лидеры выйдет самая упоминаемая таблица, а не самая конфликтная, и разница между ними может быть принципиальной - у нас именно так и вышло.
Ещё одна особенность, про которую лучше знать заранее: живого пользователя SQL Server не показывает. Все сеансы ходят под одним техническим логином приложения. Сопоставить конфликт с конкретным человеком получится только по времени, сверяясь с журналом регистрации или собственным сборщиком событий.
Четыре ложных следа, каждый по несколько часов
Сначала о том, где мы искали зря, потом о находке.
Ожидания внутри параллельного плана. Когда потоки одного запроса обмениваются данными, соответствующие ожидания похожи на блокировку, хотя на деле сессия ждёт саму себя. С конкуренцией между сессиями это никак не связано.
Блокировка временной таблицы. Такую таблицу видит лишь создавшая её сессия, поэтому заблокировать кого-то чужого через неё нельзя в принципе.
Много процессора ещё не значит, что сессия кого-то блокирует. Она способна часами грузить процессор, никого не удерживая. Пока поле блокирующей сессии не проверено, это всего лишь тяжёлая сессия.
Таблица на экране без заданной ширины колонок. Длинные значения инструмент тихо обрезал, и создавалось впечатление, что данных нет. Ошибка была в нашем способе смотреть на базу, сама база была в порядке. Из ложных следов этот самый обидный и самый частый.
Почему таблица ждала саму себя: составной тип без индекса
Когда виновник назван, остаётся понять, по какому условию сессии на нём сходятся. В нашем случае отбирали по реквизиту “документ-основание”.
Тип у него составной ссылочный: в основании может стоять документ одного из нескольких видов. Хранит платформа такое поле в трёх колонках:
| Колонка | Тип | Что хранит | Байт |
|---|---|---|---|
_Fld<N>_TYPE | binary(1) | признак типа значения | 1 |
_Fld<N>_RTRef | binary(4) | номер таблицы, то есть вид документа | 4 |
_Fld<N>_RRRef | binary(16) | идентификатор самого объекта | 16 |
Отбор по такому реквизиту фильтрует сразу три колонки, а индекса по ним не было.
Следствие понятное: запрос целиком сканировал таблицу в 5,6 миллиона строк. SQL Server сам подсказывал, что индекса не хватает, оценивал выигрыш в стоимости запроса в 99,98 процента, и эта подсказка, похоже, лежала там годами. Увидеть её можно одним запросом, и посмотреть стоит на любой базе, даже если инцидента нет:
-- Что СУБД сама считает недостающим. Порядок - по ожидаемой пользе.
SELECT TOP (20)
DB_NAME(d.database_id) AS Baza,
OBJECT_NAME(d.object_id, d.database_id) AS Tablica,
d.equality_columns AS RavenstvoPo,
d.inequality_columns AS NeravenstvoPo,
d.included_columns AS Vklyuchit,
gs.user_seeks + gs.user_scans AS SkolkoRazHoteli,
CAST(gs.avg_user_impact AS decimal(5,2)) AS ProcentVyigrysha,
CAST(gs.avg_total_user_cost
* gs.avg_user_impact
* (gs.user_seeks + gs.user_scans) AS decimal(18,2)) AS Ves
FROM sys.dm_db_missing_index_group_stats AS gs
JOIN sys.dm_db_missing_index_groups AS g ON gs.group_handle = g.index_group_handle
JOIN sys.dm_db_missing_index_details AS d ON g.index_handle = d.index_handle
WHERE d.database_id = DB_ID() -- только текущая база
ORDER BY Ves DESC;
Оговорка про эти подсказки: они не рекомендация к исполнению, а след того, что оптимизатор хотел бы иметь. Их нельзя ставить пачкой. Но когда у одной строки выигрыш 99,98 процента и она же лежит на таблице, набравшей 85 процентов конфликтов, это не совпадение.
Сканируя всю таблицу, запрос удерживает блокировки на порядки дольше, чем нужно. Вот и весь механизм. Одновременная работа с такой таблицей заканчивается дедлоком из-за того, что блокировки висят в сотню раз дольше, и неудачный порядок захвата или уровень изоляции тут ни при чём. Порядок захвата при этом может быть идеальным.
Про вред составных типов написано много, и тезис давно общее место. Обычно, правда, пишут о раздутых при соединениях планах и об обращениях через точку. У нас другой сюжет: дедлоки как проявление отсутствующего индекса, с конкретным механизмом (отбор сразу по трём колонкам) и посчитанной ценой.
Замер: во что обходится скан
A/B-замер шёл на копии базы с тем же объёмом, 5,6 миллиона строк. Запрос один, разница лишь в индексе.
| Метрика | Без индекса | С индексом |
|---|---|---|
| План запроса | сканирование кластерного индекса на 8 потоков | поиск по индексу |
| Scan count (проходов чтения) | 9 | 1 |
| Логические чтения (страниц по 8 КБ) | 282 863 | 3 |
| Процессорное время | 1 375 мс | 0 мс |
| Длительность | 155 мс | 1,3 мс |
| Размер индекса | - | 156 МБ со страничным сжатием |
Логические чтения сократились примерно в 94 тысячи раз, а длительность в 119 раз.
Разрыв между этими двумя числами закономерный, и объяснить его стоит, иначе таблица выглядит недостоверной. Просканированные страницы уже лежали в памяти, узким местом был процессор, до диска дело не доходило. Проверяем: 1 375 мс процессорного времени на восемь потоков дают 172 мс на поток, а весь запрос длился 155 мс, всё сходится. Страница из памяти читается дёшево, поэтому по чтениям разница фантастическая, а в миллисекундах скромная. Дедлоки же зависят от длительности, и честный множитель здесь 119, а не 94 тысячи.
По объёму выходит 2,16 ГиБ на запрос (страниц 282 863, по 8 килобайт). И всё это ради одной проверки основания при проведении документа, никакого отчёта или закрытия месяца.
С индексом запрос читает три страницы, столько уровней у дерева индекса на таблице такого размера. В основную таблицу ходить вообще не понадобилось, все нужные запросу данные есть в самом индексе.
Пропускная способность: 2,6 против 286
Выше был одиночный замер, а дедлоки рождает конкуренция, поэтому нужен был второй эксперимент: восемь параллельных клиентов одновременно шлют запросы. С восемью потоками внутри одного плана их не путайте, восьмёрки разные.
Без индекса и с индексом вышло 2,6 и 286 запросов в секунду, разница в 110 раз. На проде в дедлоки превращается как раз эта величина.
Перемножать числа из двух экспериментов нельзя, тут нужна аккуратность. По одиночному замеру восемь клиентов должны были выдать 51,6 в секунду без индекса (8 / 0,155) и 6 154 с индексом (8 / 0,0013). На деле получили 2,6 и 286, то есть оба прогона оказались медленнее расчёта раз в 20 (19,8 раза и 21,5 раза). Поскольку множитель с обеих сторон почти один и тот же, вывод устоял: при делении сдвиг взаимно сокращается, и отношение 110 остаётся. Между экспериментами сравнивают отношения, абсолютные числа сравнивать нельзя.
Причину самого сдвига мы не измеряли. Прогретый кэш, первая мысль, не подходит по направлению: кэш ускоряет, а вышло медленнее. Рабочая версия - восемь клиентов конкурировали за одну таблицу, то есть ровно то, о чём статья. Проверяется прогоном при разном числе клиентов: увеличивается ли множитель, когда параллельность растёт. Мы этого не делали, так что это гипотеза, причиной её пока не назовёшь.
Вторая оговорка про масштаб. Выборка той же одной строки на проде с холодным кэшем шла 1,58 секунды, а на песочнице 155 миллисекунд. Десятикратную разницу списываем на кэш: данные и структура одинаковые, но на песочнице кэш был прогрет. Цифры песочницы показывают нижнюю границу проблемы, на проде всё было хуже.
Как ставить такой индекс: песочница и прод
Для замера на песочнице индекс создаётся напрямую в СУБД. Это быстро и ничего не требует от конфигурации:
-- ТОЛЬКО для песочницы и замера. На проде такой индекс проживёт
-- до ближайшего обновления структуры базы данных.
CREATE NONCLUSTERED INDEX IX_Osnovanie_Sostavnoy
ON dbo._Document123 (_Fld1234_TYPE, _Fld1234_RTRef, _Fld1234_RRRef)
WITH (DATA_COMPRESSION = PAGE, ONLINE = OFF, SORT_IN_TEMPDB = ON);
GO
-- Проверка, что оптимизатор его увидел: план должен показать Index Seek,
-- а не Clustered Index Scan.
SET STATISTICS IO, TIME ON;
-- здесь ваш запрос
SET STATISTICS IO, TIME OFF;
На проде так делать нельзя, и вот почему. Платформа считает структуру базы своей. При очередном обновлении конфигурации она приводит физическую структуру к тому, что записано в метаданных, и посторонний индекс снесёт молча, не спросив и не сообщив. Вы получите работающую систему, которая через месяц-другой необъяснимо вернётся к прежним дедлокам.
Правильный путь - включить у реквизита в конфигураторе свойство “Индексировать”. Индекс тогда входит в структуру базы, платформа создаёт его сама, и обновления он переживает. Для составного ссылочного типа она построит его по тем же трём колонкам.
Есть момент, из-за которого ваш повтор замера может дать другие цифры. Индекс у нас со страничным сжатием, и 156 мегабайт - уже сжатый размер. Сжатие данных долгое время было только в Enterprise, а в Standard появилось начиная с версии SQL Server 2016 SP1, так что в более старой Standard индекс выйдет заметно крупнее, учтите это заранее. Редакция на выигрыш по чтениям не влияет, ведь поиск по индексу в любой редакции поиск.
Цена, которую мы не измерили
Про место можно не беспокоиться. 156 мегабайт индекса на 5,6 млн строк дают около 28 байт на строку, и это правдоподобно: 21 байт занимают три колонки составной ссылки, остальное уходит на служебные байты страницы и ссылку на строку основной таблицы. Причём размер указан уже после сжатия.
Платить приходится другим: при каждом проведении документа теперь пишется и этот индекс. Проведение идёт одной транзакцией, она немного удлинилась, на сколько, мы не мерили.
Возражение здесь сильное, и его стоит произнести: чтение вы вылечили, зато запись утяжелили, а у таблицы расходных ордеров основная нагрузка как раз запись, и в часы пик она идёт сплошным потоком. Ответить пока нечем, замера нет. Сделать его несложно, прогнав партию проведений до и после на той же песочнице, и если сделаем, напишем отдельно.
И главное, чего у этой истории нет. В прод индекс так и не попал. Всё закончилось песочницей и рекомендацией, поэтому главных чисел, сколько дедлоков в сутки было до и сколько стало после, здесь не будет, и история останавливается на лабораторном замере. Врать про эффект на проде смысла нет: такие цифры проверяются одним вопросом.
Что забрать себе
- Не разбирайте случай, считайте частоты. Один отчёт - анекдот, а в тридцати трёх уже видно распределение и виновника.
- Факт - это список ресурсов, гипотеза - текст запроса. Частоты считают по ресурсам.
- Первым делом сопоставьте бизнес-имена с именами таблиц. Ошибка на этом шаге уводит всё расследование, и изнутри её трудно заметить.
- Снимайте
system_healthнемедленно, а лучше поднимите постоянную сессию в файл до инцидента. Кольцевой буфер под штормом живёт минуты. - Сначала отличите 1205 от таймаута управляемых блокировок. Это разные болезни с разным лечением, и половина потерянного времени уходит именно сюда.
- Отбор без индекса по составному ссылочному реквизиту - это скан. Три колонки, чтение всей таблицы, блокировки в сотню раз дольше нужного.
- Индекс включайте свойством “Индексировать” в конфигураторе,
CREATE INDEXв СУБД не годится: прямой индекс не переживёт обновления структуры.
Отдельно стоит сказать, чем этот вывод расходится с общим местом. Рефлекс у большинства обратный: дедлок - значит порядок захвата, значит уровень изоляции. Механизмы эти реальны, и дедлоки по их вине случаются. Но на нашей выборке виновником оказался план запроса, и признак, по которому это видно заранее, простой: если в списке ресурсов одна таблица собирает подавляющее большинство упоминаний - смотрите её индексы, а не порядок обращений.
Обратная сторона того же признака: если в ресурсе стоит не страница и не объект,
а ключ индекса таблицы итогов (имя вида _AccumRgT), вы в другом классе отказов,
и индексы там не помогут вообще. Там сталкиваются две копии обычного проведения
на диапазонах агрегата, лечится это иначе, и мы разобрали его отдельно:
дедлок при проведении, когда движения не
пересекаются.
Кстати, на этой же системе включили режим READ COMMITTED SNAPSHOT (снимок на чтение), и конфликты, в которых одна сторона только читала, исчезли. Остались те, где пишут обе стороны, а против них уровень изоляции бессилен. Им и была посвящена вся статья.
Чем это можно посмотреть у себя
Часть работы выше мы завернули в обработки, они лежат на Инфостарте:
Чек-ап СУБД под 1С - проверяет больше сорока параметров SQL Server на соответствие платформе и выдаёт вердикт с готовыми скриптами. Там же разбираются недостающие индексы и параметры параллелизма.
Трансформатор SQL в запрос 1С - та самая
карта имён: план запроса показывает _Document123, и по ней выясняется, какой объект
конфигурации прячется за этим именем.
Карта объёмов базы 1С - показывает состав базы и то, на что ушли гигабайты, в том числе в служебных таблицах платформы.
Оптимизатор временных таблиц в пакетных запросах - соседний сюжет: план запроса и там решает больше, чем кажется. Про сами приёмы переписывания у нас есть отдельный разбор с замерами.
Если разбираться самим некогда, а встаёт уже сейчас - мы делаем это как работу: снимаем отчёты, считаем частоты, находим виновника и показываем замер до и после. Что входит и сколько стоит - на странице оптимизации 1С.