В процессе разработки высоконагруженных систем на платформе 1С:Предприятие 8 программисты часто сталкиваются с необходимостью провести нагрузочное тестирование и протестировать устойчивость кода к блокировкам. Одной из самых коварных проблем является взаимная блокировка (Deadlock). В отличие от обычного таймаута, когда один сеанс просто ждет другой, взаимная блокировка — это тупиковая ситуация, когда два процесса бесконечно ждут освобождения ресурсов друг другом. Разберем подробно, как мы можем искусственно воспроизвести эту ситуацию для отладки и настройки технологического журнала.
Проанализируем механику процесса. Когда сеанс №1 блокирует ресурс «А» и пытается получить доступ к ресурсу «Б», а сеанс №2 в это же время уже заблокировал «Б» и пытается получить доступ к «А», возникает циклическая зависимость. Система (менеджер блокировок 1С или СУБД) понимает, что ни один из потоков не сможет продолжить работу, и принудительно прерывает одну из транзакций, выбирая её в качестве «жертвы» (deadlock victim). В этот момент мы получаем ошибку TDEADLOCK.
Рассмотрим самый наглядный метод, основанный на разном порядке обращения к объектам метаданных. Для этого нам понадобятся два сеанса 1С и два объекта (например, два документа или две разные записи в регистре сведений).
Разберем по шагам алгоритм действий:
В этот момент менеджер блокировок обнаруживает цикл и мгновенно «убивает» одну из транзакций. Посмотрим на пример кода, который можно использовать в обработке (в отладке также поможет специальный помощник):
// Пример для двух сеансов с разным порядком вызова
Процедура ВыполнитьБлокировку(ПервыйОбъект, ВторойОбъект)
Блокировка = Новый БлокировкаДанных;
// Блокируем первый ресурс
Элемент = Блокировка.Добавить("Справочник.Номенклатура");
Элемент.УстановитьЗначение("Ссылка", ПервыйОбъект);
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Блокировка.Заблокировать();
// Здесь делаем паузу для переключения между сеансами
Предупреждение("Ресурс 1 заблокирован. Нажмите ОК для попытки блокировки Ресурса 2", 60);
// Пытаемся заблокировать второй ресурс
Элемент2 = Блокировка.Добавить("Справочник.Номенклатура");
Элемент2.УстановитьЗначение("Ссылка", ВторойОбъект);
Элемент2.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать(); // Здесь возникнет Deadlock, если второй сеанс сделал наоборот
ЗафиксироватьТранзакцию();
КонецПроцедуры
Выясним причину возникновения блокировок при одновременном чтении и записи. Это более «тонкий» сценарий, который часто встречается в реальных конфигурациях. Суть заключается в том, что оба сеанса сначала получают разделяемую блокировку (Shared) на один и тот же ресурс, а затем оба пытаются повысить её до исключительной (Exclusive).
Рассмотрим ситуацию подробнее:
РежимБлокировкиДанных.Разделяемый на запись регистра.Разделяемый режим на эту же самую запись. Это разрешено, так как разделяемые блокировки не конфликтуют друг с другом.Исключительный режим). Он встает в очередь, ожидая, когда Сеанс №2 отпустит свою разделяемую блокировку.Система видит, что оба участника ждут друг друга для повышения уровня владения ресурсом, и фиксирует взаимную блокировку. Пример кода для эмуляции этого сценария:
Процедура ЭмуляцияПовышенияБлокировки()
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрСведений.ЦеныНоменклатуры");
Элемент.УстановитьЗначение("Номенклатура", Справочники.Номенклатура.НайтиПоКоду("001"));
НачатьТранзакцию();
// Шаг 1: Оба сеанса должны дойти сюда и выполнить это
Элемент.Режим = РежимБлокировкиДанных.Разделяемый;
Блокировка.Заблокировать();
// Пауза для синхронизации сеансов
Приостановить(10000); // Имитация задержки
// Шаг 2: Оба сеанса пытаются "повысить" блокировку до исключительной
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать(); // Менеджер блокировок вызовет TDEADLOCK
ЗафиксироватьТранзакцию();
КонецПроцедуры
Проанализируем интересное явление: иногда при включении детального сбора данных в Технологическом журнале (события SDBL, TLOCK) воспроизвести Deadlock становится сложнее. Это происходит из-за того, что запись логов вносит микроскопические задержки в работу сервера приложений (особенно если требуется убирать лишние переносы строк в ТЖ). Этих долей секунды может быть достаточно, чтобы один из сеансов успел завершить операцию чуть раньше другого, превращая взаимную блокировку в обычное ожидание (таймаут). Это стоит учитывать при настройке диагностических фильтров.
Для того чтобы убедиться, что вы поймали именно взаимную блокировку, необходимо настроить файл logcfg.xml. Нас интересуют следующие события:
TDEADLOCK — собственно факт возникновения взаимной блокировки.TLOCK — информация о том, какие управляемые блокировки устанавливались перед этим.TTIMEOUT — если вместо взаимной блокировки вы получили простое превышение времени ожидания.В тексте события TDEADLOCK платформа обычно указывает структуру WaitConnections. Это список номеров сеансов, которые участвовали в конфликте. Если вы используете MS SQL Server, дополнительную информацию можно получить через Deadlock Graph в профайлере SQL, который покажет конфликт на уровне страниц базы данных (например, если индексы неоптимальны и происходит блокировка диапазона Range Lock).
Чтобы в реальной работе не сталкиваться с тем, что мы только что эмулировали, придерживайтесь следующих правил:
Исключительный режим блокировки при чтении. Не используйте сначала разделяемую, а потом исключительную блокировку в одной транзакции.