Почему зависает сеанс 1С при выполнении длительной транзакции и как найти причину?

Программист 1С v8.3 (Управляемые формы) 1C:Бухгалтерия Бухгалтерский учет IT и автоматизация бизнеса
← На главную

В практике сопровождения и разработки высоконагруженных систем на платформе 1С:Предприятие 8.3 периодически встречаются сложные, плавающие проблемы производительности. Одна из наиболее неприятных ситуаций — внезапное «зависание» фонового или интерактивного сеанса в момент проведения документов или вызова метода ЗафиксироватьТранзакцию(). При этом в консоли кластера серверов 1С сеанс продолжает активно наращивать счетчик «Захвачено СУБД», значение «Заблокировано СУБД» остается равным нулю, а в журнале регистрации транзакция часами находится в статусе «Не завершена».

Разберем детально физику этого процесса, выясним, почему возникают подобные симптомы, как правильно настроить диагностику с помощью Технологического журнала и системных инструментов MS SQL Server, а также как устранить первопричину сбоя.

Симптомы проблемы и механика работы показателей кластера 1С

Рассмотрим, что на самом деле происходит в системе, когда мы наблюдаем описанное поведение в консоли администрирования 1С:

  1. Колонка «Захвачено СУБД» постоянно увеличивается: Этот показатель отражает суммарное время, в течение которого процесс кластера rphost ожидает ответа от сервера базы данных на отправленный T-SQL запрос. Если число растет, это означает, что сеанс не «уснул» и не потерял связь с СУБД, а сервер SQL в данный момент активно выполняет тяжелую инструкцию (например, сканирует миллионы записей в оперативной памяти или на диске).
  2. Колонка «Заблокировано СУБД» равна 0: Данный счетчик фиксирует ожидание сеансом снятия блокировок уровня СУБД (например, ожидания по типам LCK_M_*). Нулевое значение свидетельствует о том, что сеанс никем не заблокирован и сам выполняет ресурсоемкую операцию, удерживая монопольные ресурсы.
  3. Статус «Не завершена» в Журнале регистрации: Платформа 1С делает отметку о завершении транзакции в журнале только после успешного завершения системной команды COMMIT TRANSACTION на уровне СУБД. Пока СУБД выполняет внутренние запросы или сбрасывает страницы данных, транзакция остается открытой.

Шаг 1. Локализация проблемы с помощью Технологического журнала 1С

Когда поведение запросов нестабильно и ошибка воспроизводится только на рабочей базе под нагрузкой, классическая отладка не дает результатов. Настроим Технологический журнал (ТЖ) для отлова длительных запросов и анализа контекста исполнения.

Создадим или отредактируем файл конфигурации logcfg.xml в каталоге настроек платформы (для Windows по умолчанию: C:\Program Files\1cv8\conf\logcfg.xml). На первом этапе ограничим сбор только событиями СУБД длительностью более 10–15 секунд, чтобы не перегружать дисковую подсистему сервера журналами огромного объема:


<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
    <log location="C:\LOGS\LongQueries" history="24">
        <event>
            <eq property="Name" value="DBMSSQL"/>
            <ge property="Duration" value="10000000"/>
        </event>
        <event>
            <eq property="Name" value="SDBL"/>
            <ge property="Duration" value="10000000"/>
        </event>
        <property name="p:processName"/>
        <property name="t:computerName"/>
        <property name="t:connectID"/>
        <property name="SessionID"/>
        <property name="Usr"/>
        <property name="Context"/>
        <property name="Sql"/>
        <property name="Rows"/>
        <property name="RowsAffected"/>
    </log>
</config>

Разберем параметры этой настройки:

  1. DBMSSQL — логирует реальные T-SQL команды, передаваемые сервером 1С в СУБД Microsoft SQL Server.
  2. SDBL — фиксирует обращения на внутреннем языке запросов 1С.
  3. Duration value="10000000" — фильтрует операции длительностью более 10 секунд (длительность в ТЖ указывается в микросекундах).
  4. Context — важнейшее свойство, позволяющее точно увидеть стек вызовов кода 1С (модуль, процедуру и номер строки), породивший проблемный запрос.

Если проблемный сеанс уже известен (например, зависло регламентное задание с определенным номером сеанса в консоли кластера), настроим сбор всех событий конкретно для этого сеанса:


<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
    <log location="C:\LOGS\SessionTrace" history="4">
        <event>
            <ne property="Name" value=""/>
            <eq property="SessionID" value="332"/>
        </event>
        <property name="all"/>
    </log>
</config>

Шаг 2. Экспресс-диагностика на стороне MS SQL Server

Если сеанс завис прямо сейчас, не обязательно ждать завершения логирования ТЖ. Выясним, что делает SQL Server, с помощью динамических административных представлений (DMV). Выполним в SQL Server Management Studio (SSMS) следующий запрос:


SELECT 
    r.session_id,
    r.status,
    r.start_time,
    r.command,
    r.cpu_time,
    r.total_elapsed_time,
    r.wait_type,
    r.wait_time,
    r.last_wait_type,
    t.text AS [SQL_Text],
    p.query_plan AS [Query_Plan]
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) p
WHERE r.session_id > 50 AND r.session_id <> @@SPID;

Этот запрос мгновенно возвращает исполняемый T-SQL код, фактическое время выполнения в миллисекундах, тип текущего ожидания (например, дисковое чтение PAGEIOLATCH_SH или блокировка памяти) и графический план запроса.

Причина проблемы: неоптимальное соединение по полю «Регистратор»

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

Посмотрим на типичный ошибочный пример запроса к большому подчиненному регистру сведений:


ВЫБРАТЬ
    ТаблицаДокументов.Ссылка КАК Документ,
    ЦеныНоменклатуры.Цена КАК Цена
ИЗ
    ВременнаяТаблицаДокументов КАК ТаблицаДокументов
    ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
        ПО ТаблицаДокументов.Ссылка = ЦеныНоменклатуры.Регистратор

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

  1. Структура индексов подчиненного регистра: В подчиненных регистрах сведений платформа 1С автоматически создает кластерный индекс по полям (Период, Регистратор, НомерСтроки) либо (Регистратор, НомерСтроки). При соединении исключительно по полю Регистратор без указания периода или ведущих измерений оптимизатор СУБД не всегда может выполнить точечный поиск по дереву индекса (Index Seek).
  2. Сканирование всей таблицы (Clustered Index Scan): Вместо быстрого поиска SQL Server вынужден последовательно перебирать гигабайты страниц данных из оперативной памяти или считывать их с диска.
  3. Эффект «Parameter Sniffing»: При первом выполнении запроса с небольшим объемом выборки оптимизатор SQL Server строит план на основе алгоритма Nested Loops (вложенные циклы). Когда в процедуру проведения попадает крупный документ с сотнями строк, СУБД использует сохраненный неоптимальный план и выполняет полный скан многомиллионного регистра на каждую строку исходного документа. Запрос, выполнявшийся ранее за доли секунды, начинает выполняться десятки минут.

Шаг 3. Правильная оптимизация запросов к регистрам

Рассмотрим способы исправления неоптимальных запросов к регистрам сведений:

  1. Передача фильтров в параметры виртуальных таблиц: Если регистр является периодическим, никогда не следует соединять физическую таблицу регистра напрямую. Используем виртуальную таблицу среза с обязательной передачей условий в параметрах:
    
    ВЫБРАТЬ
        Таблица.Номенклатура КАК Номенклатура,
        Срез.Цена КАК Цена
    ИЗ
        ВременнаяТаблица КАК Таблица
        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВременнаяТаблица)) КАК Срез
            ПО Таблица.Номенклатура = Срез.Номенклатура
    
  2. Использование ведущих измерений: Если требуется выборка по конкретному регистратору из непериодического регистра сведений, убедимся, что измерение, связывающее документ с регистром, отмечено флагом «Ведущее» (в этом случае для него строится отдельный индекс).
  3. Индексация временных таблиц: Все временные таблицы, участвующие в секциях ПО и ГДЕ, должны быть явно проиндексированы по полям соединения с помощью конструкции ИНДЕКСИРОВАТЬ ПО.

Шаг 4. Опасность паттерна «длинной транзакции» и ее устранение

В исходной ситуации проведение пакета документов выполнялось в рамках одной непрерывной транзакции длительностью 50–70 секунд. Разберем, почему длинные транзакции опасны в рабочей среде:

  1. Эскалация и удержание блокировок: В течение всей транзакции все измененные таблицы и записи регистров удерживаются монопольными блокировками. Параллельные процессы (обмены РИБ, оперативная работа пользователей) встают в очередь ожидания, что приводит к тайм-аутам (ошибки Превышено время ожидания предоставления блокировки) и взаимоблокировкам (Deadlock).
  2. Нагрузка на TempDB и журнал транзакций: Все временные таблицы пакета запросов сохраняются в базе данных tempdb до вызова команды ЗафиксироватьТранзакцию(), перегружая дисковую подсистему сервера.

Для устранения этой проблемы применим паттерн «Отложенное раздельное проведение»:


// Создаем и записываем документы без проведения в коротких транзакциях
МассивСозданныхДокументов = Новый Массив;

Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
    НачатьТранзакцию();
    Попытка
        ДокОбъект = Документы.ПриходныйКассовыйОрдер.СоздатьДокумент();
        // Заполнение реквизитов документа
        ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
        
        ЗафиксироватьТранзакцию();
        МассивСозданныхДокументов.Добавить(ДокОбъект.Ссылка);
    Исключение
        ОтменитьТранзакцию();
        ЗаписьЖурналаРегистрации("СозданиеДокументов", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
    КонецПопытки;
КонецЦикла;

// Проводим каждый документ в своей индивидуальной короткой транзакции
Для Каждого СсылкаДок Из МассивСозданныхДокументов Цикл
    Попытка
        ДокОбъект = СсылкаДок.ПолучитьОбъект();
        ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
    Исключение
        ЗаписьЖурналаРегистрации("ПроведениеДокументов", УровеньЖурналаРегистрации.Предупреждение,,, "Не удалось провести: " + СсылкаДок);
    КонецПопытки;
КонецЦикла;

Итоги и рекомендации

Подведем практический итог: при возникновении симптомов зависания сеанса с растущим показателем «Захвачено СУБД» не следует перезапускать рабочие службы наугад. Правильная последовательность действий включает:

  1. Настройку Технологического журнала на события DBMSSQL и SDBL с фильтрацией по времени или номеру сеанса.
  2. Анализ исполняемых планов и текста T-SQL через DMV-представления SQL Server.
  3. Оптимизацию запросов к регистрам (исключение соединений физических таблиц по неиндексированным полям регистратора).
  4. Дробление тяжелых пакетов обработки на независимые короткие транзакции.
← На главную