Длинные транзакции в 1С: где они рождаются в коде и как их разбить
Иван Недомолков · Chop etilgan: · Yangilangan:
Есть фраза, после которой у опытного администратора портится настроение: “давайте обработаем всё в одной транзакции, чтобы было атомарно”. Атомарно - будет. Работать - не будет: одна такая обработка на пару часов способна остановить работу всей базы, при том что сама она “просто считает”.
Что происходит на самом деле, пока живёт длинная транзакция:
- держатся блокировки на всё изменённое - остальные пользователи выстраиваются в очередь и получают таймауты;
- копятся версии строк в tempdb - version store не очищается, пока жива хоть одна старая транзакция, и служебная база пухнет;
- не очищается журнал транзакций - LDF растёт, пока не упрётся в диск;
- растёт цена сбоя - откат двухчасовой транзакции занимает сравнимое время, и прервать его нельзя.
То есть проблема не в объёме работы, а в том, что вся работа завёрнута в одну транзакцию. Ниже - где такие транзакции рождаются в коде 1С, как их найти и как разбить, не потеряв целостность.
Что считать длинной транзакцией
Ориентир простой: транзакция должна жить секунды. Проведение документа - доли секунды. Запись набора записей - миллисекунды. Всё, что живёт минутами, - уже аномалия, даже если формально работает.
Практический порог для мониторинга - 5 минут: транзакция старше пяти минут почти всегда означает либо пакетную обработку “одним куском”, либо зависшее ожидание. Оба случая требуют разбирательства, а не увеличения таймаутов. Найти держателя по техжурналу и монитору СУБД - первый шаг оптимизации 1С: пока виновник не назван поимённо, любые настройки СУБД лечат симптом.
Где длинные транзакции рождаются в коде 1С
Восемь типовых мест, которые встречаются в разборах чаще всего.
1. Транзакция вокруг всего цикла. Классика: обработать выборку из десятков тысяч объектов, обернув цикл в одну транзакцию “для атомарности”.
// Так делать не надо: одна транзакция на весь объём
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
Объект = Выборка.Ссылка.ПолучитьОбъект();
Объект.Реквизит = ПересчитатьЗначение(Объект);
Объект.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
2. Внешние вызовы внутри транзакции. HTTP-запрос к сервису, отправка письма, запись файла, COM-соединение. Транзакция открыта, а мы ждём сеть - и всё это время держим блокировки. Внешний сервис задумался на 30 секунд - вся база ждёт вместе с нами.
3. Интерактив внутри транзакции. Вопрос(), открытие формы, ожидание ответа
пользователя. Транзакция живёт, пока человек не вернётся с обеда.
4. Тяжёлый пересчёт в проведении документа. Проведение обязано быть коротким, но внутрь него нередко заезжает пересчёт себестоимости, обновление связанных регистров или формирование отчёта. Документ проводится 40 секунд - и все, кто работает с теми же остатками, стоят. Отдельная неприятность этого пункта в том, что чтение остатка внутри той же транзакции, где документ уже записал движения, порождает дедлоки на таблице итогов - там сталкиваются две копии обычного проведения, и чем ровнее идёт день, тем чаще.
5. Фоновое задание, ждущее блокировку. Транзакция открыта, задание встало в очередь за ресурсом. Оно ничего не делает - но держит и блокировки, и журнал. Механику разбирали в статье про блокировки в 1С. Если дошло уже до взаимоблокировок, начинать надо не с разбора одного случая, а со счёта частот по отчётам СУБД.
6. Обмены и загрузка данных пачкой. Приём сообщения обмена целиком в одной транзакции: тысячи объектов, минуты работы, полная остановка при конфликте. Так же ночная загрузка откатывается целиком из-за одного значения, не влезшего в колонку.
7. Неявные транзакции. Каждый Записать() объекта и каждая запись набора - это уже
транзакция, даже если вы её не открывали. Если такой код выполняется в цикле внутри
вашей транзакции, всё складывается в одну большую.
8. Регламентное задание “раз в сутки на всю базу”. Пересчёт итогов, перепроведение за период, массовое обновление реквизитов. Само по себе нужное - но выполняемое единым блоком в рабочее время.
Как найти длинные транзакции
По технологическому журналу. Самый честный источник: видно контекст - какой модуль, какая строка. Минимальная конфигурация для отлова длительных операций и ожиданий:
<log location="C:\v8log\long" history="24">
<event>
<eq property="name" value="TLOCK"/>
<ge property="duration" value="5000000"/>
</event>
<event>
<eq property="name" value="TTIMEOUT"/>
</event>
<event>
<eq property="name" value="Call"/>
<ge property="duration" value="30000000"/>
</event>
<property name="all"/>
</log>
duration здесь в микросекундах: 5 000 000 - это 5 секунд, 30 000 000 - 30 секунд.
В событиях смотрите свойство Context - оно покажет конкретную процедуру и строку, а не
абстрактное “база тормозит”.
Со стороны СУБД. Кто прямо сейчас держит самую старую транзакцию - три команды и запрос по динамическим представлениям; подробный разбор с готовыми скриптами есть в статье про ошибку 9002 и tempdb, повторяться не будем. Важно понимать связь: то, что администратор видит как “держателя журнала”, в коде почти всегда оказывается одним из восьми пунктов выше. Когда транзакция уже закрыта и смотреть некого, работает история снимков состояния кластера - как её устроить и чем снимать держателя по имени, разобрано в статье кто блокирует базу 1С.
Одно исключение из этого правила стоит держать в голове: держателем бывает сеанс, который не выполняет ничего. Спящее соединение с открытой транзакцией не держит блокировок, в отчёты “кто кого блокирует” не попадает по построению, а границу старейшей активной транзакции задаёт и не даёт усечь хранилище версий. У нас таким держателем на четверо с половиной суток оказался собственный коллектор мониторинга - там же запрос, которым такие сеансы ищутся.
По коду конфигурации. Быстрый аудит: глобальный поиск НачатьТранзакцию и проверка
каждого вхождения по трём вопросам.
- Есть ли внутри цикл по неограниченной выборке?
- Есть ли внутри обращение наружу - HTTP, файл, почта, COM,
Вопрос()? - Может ли содержимое ждать блокировку?
Любое “да” - кандидат на переработку.
Как правильно разбить на порции
Главная практическая часть. Задача - сохранить корректность, отказавшись от “всё-или-ничего” на весь объём.
Шаг 1. Порция вместо всего объёма. Транзакция открывается на порцию - например, на 1000 объектов - и закрывается. Между порциями база свободна.
// Порционная обработка: транзакция живёт на порцию, а не на весь объём
РазмерПорции = 1000;
Счётчик = 0;
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
Объект = Выборка.Ссылка.ПолучитьОбъект();
Объект.Реквизит = ПересчитатьЗначение(Объект);
Объект.Записать();
Счётчик = Счётчик + 1;
// Закрыли порцию и сразу открыли следующую
Если Счётчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
ЗаписатьПрогресс(Объект.Ссылка); // маркер для возобновления
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Шаг 2. Возобновляемость вместо атомарности. Раз транзакция больше не одна, нужен способ понять, где остановились. Рабочие варианты: реквизит-признак “обработано” у объекта, регистр сведений с отметкой, отбор “необработанные” в самой выборке. Тогда обработка становится идемпотентной: перезапустили - продолжила с места обрыва, а не начала с нуля.
Это и есть честный ответ на возражение “а как же атомарность”: вместо одной гигантской транзакции вы получаете возобновляемый процесс. Для пакетных пересчётов это почти всегда правильнее: атомарность на 500 000 строк никому не нужна - нужна гарантия, что каждая строка обработана ровно один раз.
Шаг 3. Всё внешнее - за пределы транзакции. Данные для отправки собираются внутри, а сама отправка (письма, HTTP, файлы) выполняется после фиксации. Удобный приём - очередь: внутри транзакции пишем задание в регистр, отдельное фоновое задание его разбирает.
Шаг 4. Никаких Попытка вокруг части транзакции. Частая и опасная ошибка - поймать
исключение внутри транзакции и “продолжить работу”:
// Так НЕ работает: после ошибки внутри транзакции продолжать нельзя
НачатьТранзакцию();
Для Каждого Строка Из Данные Цикл
Попытка
ЗаписатьСтроку(Строка);
Исключение
Продолжить; // ошибка "В данной транзакции уже происходили ошибки"
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
Платформа помечает транзакцию как испорченную при первой же ошибке: дальнейшие действия
в ней запрещены, а на фиксации вы получите исключение независимо от того, поймали
исходную ошибку или нет. Правильный шаблон обратный: Попытка снаружи транзакции,
а обработка “пропустить плохую строку” делается на уровне порции - плохая порция
откатывается целиком, пишется в лог ошибок и работа продолжается со следующей.
Шаг 5. Размер порции подбирается замером. Слишком большая порция возвращает нас к исходной проблеме; слишком маленькая - заваливает сервер накладными расходами на фиксацию. Практический диапазон - от 100 до 5000 объектов; выбирается по замеру на реальных данных, а не по интуиции. Как строится такой замер - в разборе приёмов оптимизации запросов и обработок.
Отдельно про обмены и загрузку
В обменах длинная транзакция особенно опасна: она держит блокировки на популярные справочники, а конфликт версий откатывает всю пачку целиком. Что помогает:
- принимать сообщение порциями по объектам, а не целиком;
- ставить
ОбменДанными.Загрузка = Истинапри записи - это отключает лишние проверки и проведения и заметно сокращает время внутри транзакции; - не запускать полный обмен в час пик - переносить в окно низкой нагрузки.
Как убедиться, что стало лучше
Три проверки, которые не позволяют обмануть себя:
- Максимальная длительность транзакции до и после - по техжурналу. Было “пиковые 40 минут”, стало “максимум 3 секунды”.
- Исчезновение
TTIMEOUTв рабочие часы: если пользователи перестали получать “конфликт блокировок” - вы сняли не симптом, а причину. - Поведение tempdb и журнала в окно обработки: перестал расти version store, перестал раздуваться LDF. Настройки, которые к этому прилагаются, - в статьях про tempdb и журнал транзакций.
Чек-лист
- Транзакция должна жить секунды. Всё, что живёт минутами, - дефект, а не нагрузка.
- Найдите все
НачатьТранзакциюи проверьте на цикл, внешние вызовы и ожидания. - Разбивайте на порции с маркером прогресса: возобновляемость полезнее атомарности.
- Внешние эффекты - только после фиксации, лучше через очередь.
Попытка- снаружи транзакции; “пропустить плохую строку” делается порцией.- Размер порции подбирается замером, а не по ощущению.
- Проверяйте результат по техжурналу: максимальная длительность и отсутствие таймаутов.
Если вы уже знаете, что у вас есть такая обработка, но непонятно, как разбить её без риска для данных, - начните с бесплатного экспресс-аудита: найдём держателей транзакций по техжурналу и монитору СУБД и предложим схему порционной переработки с проверкой эквивалентности результата. Работы по коду и производительности целиком - услуга оптимизация 1С.
Готовые обработки по теме
Найти держателей длинных транзакций и понять, почему база их плодит, помогают наши инструменты. Чек-ап СУБД под 1С проверяет настройки SQL Server, из-за которых транзакции живут дольше нужного (модель восстановления и лог, tempdb, параллелизм), с вердиктом по каждой и демо-отчётом на боевой базе. А Оптимизатор временных таблиц убирает из пакетных запросов мусор в tempdb, который затягивает время исполнения и удержания блокировок.