Блокировки в 1С: почему в час пик кассы встают на минуту - и как это лечится
Иван Недомолков · Опубликовано: · Обновлено:
Знакомая картина: днём всё летает, а в час пик - очередь на кассе, “висит” проведение реализации, менеджеры звонят “программа встала”. Смотрите на сервер - процессор скучает на 20 %, дисков хватает, память свободна. Железо не при чём, а работать нельзя. Классический симптом того, что 1С упёрлась не в скорость, а в конкуренцию за данные: пользователи ждут не расчёт, а друг друга.
Сразу разведём два похожих, но разных явления:
- Блокировка - нормальный механизм. Одна транзакция придержала данные, другая ждёт их освобождения. Плохо, когда ждёт долго и таких ждущих выстраивается цепочка.
- Взаимоблокировка (deadlock) - тупик: две транзакции держат по ресурсу и ждут ресурс друг друга. Сама не рассосётся, поэтому СУБД одну из них принудительно откатывает, и пользователь получает ошибку.
Ниже - как всё это устроено в 1С поверх SQL Server, почему в пик встаёт даже мощный сервер, и как отличить настоящую причину от мнимой, прежде чем что-то “крутить”. В чек-листе диагностики “1С тормозит” блокировки - всего один шаг из семи; здесь разбираем этот шаг целиком.
Два уровня блокировок: управляемые и СУБД
В 1С блокировки живут на двух этажах одновременно, и путаница между ними - источник половины неверных диагнозов.
Управляемые блокировки ставит сама 1С в менеджере блокировок кластера. С версии
8.3 управляемый режим - стандарт (Свойства конфигурации → Режим управления
блокировкой данных = “Управляемый”). Платформа блокирует не строки таблиц, а
логические данные: остатки регистра по конкретной номенклатуре и складу, набор
записей регистра сведений, объект по ссылке. Блокировка живёт ровно на время
транзакции проведения. Оговорка, оплаченная чужими ревизиями: наличие
БлокировкаДанных в коде ещё не означает, что замок ставится, - у этого отказа
есть шесть типовых причин,
и все молчаливые.
Блокировки СУБД ставит SQL Server под капотом, когда 1С читает и пишет реальные таблицы. Здесь свои правила: разделяемые (S) на чтение, исключительные (X) на запись, блокировки диапазонов индекса, и - самое коварное - эскалация.
Разделяемая блокировка не мешает другим читать, но не пускает писать. Исключительная не пускает никого. Пока транзакция проведения открыта - все её блокировки удерживаются; чем дольше транзакция, тем шире окно, в котором на этих данных кто-то встанет.
Почему именно в пик и именно на этих данных
Блокировки становятся проблемой не “вообще”, а на нескольких предсказуемых узлах.
Итоги регистров накопления. Классика жанра. Чтобы провести реализацию, документ читает текущие остатки (контроль “не уйти в минус”) и пишет движения. Две продажи одной и той же ходовой позиции с одного склада конкурируют за одни и те же остатки - и одна ждёт другую. Днём продаж мало, окно короткое, никто не замечает. В пик частота растёт, окна перекрываются - образуется очередь. Штатное лекарство платформы - разделение итогов (Свойства регистра → “Разрешить разделение итогов”): 1С разносит конкурирующие записи по разным “дорожкам”, и параллельные проведения перестают драться за одну строку итога.
Оговорка про класс проблемы, она пригодится дальше. Всё описанное - про двух писателей, которые дерутся за одну строку остатка, и разделение итогов лечит ровно их. Бывает второй класс, внешне неотличимый: документ в одной транзакции и пишет движения, и сам же читает по ним итоги для контроля остатка. Тогда сталкиваются две половины одной операции, писатель и читатель, и разделение итогов тут бесполезно - почему, разбираю ниже.
Нумерация и “последние” значения. Присвоение следующего номера документа, запись “последнего курса”, единственная строка регистра сведений с настройкой - всё, к чему все обращаются одновременно, становится точкой сериализации.
Эскалация блокировок SQL Server. Когда транзакция набирает порядка 5000 блокировок на одном объекте, SQL Server ради экономии памяти меняет тысячи мелких блокировок строк на одну грубую - на всю таблицу. Формально эффективно, практически катастрофа: проведение одного “толстого” документа на 6-7 тысяч строк заблокировало таблицу целиком, и все остальные встали. Отсюда правило: большие движения - зло для конкурентности; дробите документы, выносите тяжёлые пересчёты из онлайна.
Длинные транзакции. Худшее, что можно сделать, - засунуть в транзакцию проведения обращение к внешнему сервису, отправку письма, запись в файл или долгий цикл. Пока ждём внешний ответ, все блокировки держатся. Транзакция должна быть короткой и только про данные: посчитали, записали, закрыли; всё остальное - до или после.
Отсутствие индекса. Если запрос внутри проведения вынужден сканировать таблицу вместо точечного обращения по индексу, он накладывает разделяемые блокировки на лишние строки - и цепляет тех, кто с этими строками вообще не связан. Индекс здесь не про скорость, а про то, чтобы не трогать чужое. Как это ищется - в разборе приёмов оптимизации запросов.
Парадокс параллелизма: когда “быстрее” означает “встало”
Отдельная история, на которой ломается интуиция. SQL Server по умолчанию готов распараллеливать выполнение запроса на все ядра. Для тяжёлой аналитики это благо. Для нагрузки 1С - короткие частые OLTP-запросы проведения и продаж - это вред.
Два параметра экземпляра SQL Server:
- MAXDOP (max degree of parallelism) - сколько ядер разрешено одному запросу.
По умолчанию
0- “сколько есть”. - cost threshold for parallelism - порог “стоимости” плана, выше которого включается
параллелизм. По умолчанию
5- смехотворно низкий, под него попадают даже пустяковые запросы.
На заводских настройках сервер распараллеливает то, что распараллеливать не нужно.
Появляются ожидания синхронизации потоков (CXPACKET), короткие запросы дольше держат
блокировки, а “лишние” потоки съедают воркеры сервера. Результат - тот самый парадокс:
блокировок по отчёту почти нет, а всё стоит.
У той же группы розничных компаний нагрузка приходила не только из касс: заказы с маркетплейса падали в ту же базу потоком, до двухсот штук в пиковый день - как это устроено, разобрано в кейсе интеграции с Каспи. Сам обмен в том проекте стоял на нашем модуле интеграции 1С с Kaspi: заказы приходили в базу готовыми документами, и пиковый день перестал требовать ручного ввода. Смешанная нагрузка “касса плюс поток заказов” как раз и даёт картину ниже.
У нас был ровно такой боевой случай (группа розничных компаний с сетью касс): монитор показывал минимум ожиданий блокировок, но продажи в пик замирали. Диагноз оказался не в блокировках, а в параллелизме. Привели настройки экземпляра к рекомендованным для 1С:
-- Рекомендованные значения для OLTP-нагрузки 1С (уточняйте под свою конфигурацию NUMA/ядер)
EXEC sys.sp_configure N'show advanced options', 1;
RECONFIGURE;
GO
EXEC sys.sp_configure N'max degree of parallelism', 1; -- по умолчанию 0 (все ядра)
EXEC sys.sp_configure N'cost threshold for parallelism', 50; -- по умолчанию 5
RECONFIGURE;
GO
Пиковые “замирания” ушли в тот же день. Значение MAXDOP = 1 - безопасная отправная
точка для баз, где живёт исключительно 1С; если сервер делит нагрузку с тяжёлой
отчётностью, значение подбирают по числу ядер в NUMA-узле, не оставляя заводской 0.
Суть проста и контринтуитивна: прежде чем “бороться с блокировками”, проверьте, не сам
ли сервер их создаёт лишним параллелизмом. А если на том же экземпляре живут чужие базы, кассу может душить сосед: как чек проводился двадцать минут из-за зависшей транзакции в соседней учётной базе, разобрано в кейсе про общий SQL-сервер.
Версионирование строк: чтобы читатели не мешали писателям
Ещё одна настройка уровня базы, которую 1С прямо рекомендует, - READ COMMITTED SNAPSHOT (RCSI). Без неё читающий запрос ставит разделяемые блокировки и может придержать пишущего. С RCSI SQL Server отдаёт читателю версию строки на момент начала запроса, не блокируя данные:
-- Читатели перестают блокировать писателей (выполнять в монопольном окне)
ALTER DATABASE [ваша_база] SET READ_COMMITTED_SNAPSHOT ON;
Это снимает целый класс “фантомных” ожиданий, когда отчёт или динамический список
подвешивал проведение. Плата - рост нагрузки на tempdb (там хранятся версии), поэтому
включать стоит вместе с правильной настройкой tempdb и
на обслуживаемом диске.
Взаимоблокировки: тупик и его жертва
Взаимоблокировка - не “сильная блокировка”, а логический тупик. Транзакция A заняла ресурс 1 и просит ресурс 2; транзакция B в это же время заняла ресурс 2 и просит ресурс 1. Обе ждут друг друга вечно. SQL Server находит цикл, выбирает жертву (обычно ту, чей откат дешевле), откатывает её и возвращает ошибку. В 1С это приходит ошибкой СУБД с номером 1205 и словами deadlock victim. Не путайте её с “Конфликтом блокировок при выполнении транзакции”: та приходит по таймауту управляемых блокировок, и это другой механизм с другим лечением.
Главная причина deadlock - разный порядок захвата ресурсов. Один документ при проведении сначала трогает регистр остатков, потом взаиморасчёты; другой - наоборот. Пока порядок разный, рано или поздно они встретятся в противоходе. Лечится это не “увеличением таймаута”, а приведением кода к единому порядку обхода данных: все транзакции блокируют ресурсы в одной и той же последовательности. Тогда цикл ожидания физически не может замкнуться - вторая транзакция просто подождёт на первом же ресурсе.
Оговорка, которая на одном инциденте стоила нам суток: порядок захвата не единственная причина. Если запрос из-за отсутствующего индекса сканирует таблицу целиком, он держит блокировки в сотню раз дольше нужного, и дедлоки идут при идеальном порядке обхода. Как отличить одно от другого и как считать частоты по отчётам СУБД, вместо того чтобы разбирать один случай, - в разборе про поиск настоящего виновника.
Есть и третий вариант, в котором единый порядок обхода не помогает вовсе, потому что порядок у обеих транзакций уже одинаковый. Так выглядит конфликт на таблице итогов регистра: документ в одной транзакции и пишет движения, и читает остаток, а сталкиваются между собой две копии обычного проведения. Опознаётся по графу за минуту, и лечение там из другого списка. Таймаут ожидания управляемой блокировки в 1С по умолчанию - 20 секунд. Поднимать его как “решение” - значит менять быстрый отказ на долгое зависание: пользователь всё равно не работает, только теперь ждёт минуту вместо мгновенной понятной ошибки.
Диагностика: по журналам, а не по интуиции
Единственный честный способ разобраться с блокировками - зафиксировать факт: кто кого ждал, на каком ресурсе, в каком контексте. Догадки здесь стоят дороже всего. Если вопрос стоит проще - назвать держателя по имени сейчас или за прошлую ночь, - начните с разбора кто блокирует базу 1С: там три способа снять текущий снимок и устройство истории, по которой виден ночной виновник.
Технологический журнал 1С - первый инструмент. Три события отвечают на всё:
TLOCK- установка и ожидание управляемых блокировок (кто, что, сколько ждал);TDEADLOCK- взаимоблокировки управляемых блокировок с обеими сторонами конфликта;TTIMEOUT- превышение времени ожидания (те самые “конфликт блокировок”).
Минимальная настройка logcfg.xml, чтобы поймать конкуренцию, не утопив сервер в логах:
<log location="C:\v8log\lock" history="24">
<event>
<eq property="name" value="TLOCK"/>
</event>
<event>
<eq property="name" value="TDEADLOCK"/>
</event>
<event>
<eq property="name" value="TTIMEOUT"/>
</event>
<property name="all"/>
</log>
В событии виден контекст (какой модуль и строка проведения), ресурс (регистр и измерения) и вторая сторона - этого достаточно, чтобы найти конкретную процедуру, а не “оптимизировать вообще”.
Ошибки в журнале регистрации. Всплеск записей “Конфликт блокировок при выполнении транзакции” в ЖР - прямой индикатор. Если журнал на нагруженной базе сам открывается минутами, это отдельная боль - как её лечат, разбирали в статье “Журнал регистрации 1С разросся и тормозит”.
Со стороны SQL Server - кто кого блокирует прямо сейчас и на каком ресурсе:
-- Текущие цепочки блокировок: кто заблокирован и кем
SELECT
tl.request_session_id AS ждёт_сессия,
wt.blocking_session_id AS держит_сессия,
tl.resource_type AS тип_ресурса,
tl.request_mode AS режим,
OBJECT_NAME(p.object_id) AS таблица,
wt.wait_duration_ms AS ждёт_мс
FROM sys.dm_tran_locks AS tl
JOIN sys.dm_os_waiting_tasks AS wt
ON tl.lock_owner_address = wt.resource_address
LEFT JOIN sys.partitions AS p
ON tl.resource_associated_entity_id = p.hobt_id
ORDER BY wt.wait_duration_ms DESC;
Для системного взгляда “на что вообще уходит ожидание” - статистика ожиданий: доля
LCK_M_* подтверждает блокировки, а заметный CXPACKET намекает на тот самый лишний
параллелизм.
-- Топ ожиданий сервера: LCK_M_* = блокировки, CXPACKET = параллелизм
SELECT TOP (15)
wait_type,
wait_time_ms / 1000.0 AS ожидание_сек,
waiting_tasks_count AS событий
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (N'CLR_SEMAPHORE', N'SLEEP_TASK', N'BROKER_TASK_STOP')
ORDER BY wait_time_ms DESC;
Взаимоблокировки СУБД удобно ловить отдельной сессией Extended Events (xml_deadlock_report) -
она рисует граф тупика с обеими транзакциями и точным ресурсом.
Как чинится: по уровням, от дешёвого к дорогому
Порядок действий - от настроек, которые ничего не ломают, к правкам кода:
- Настройки экземпляра SQL Server.
MAXDOPиcost threshold for parallelismпод 1С,READ COMMITTED SNAPSHOTвключён. Часто снимает большую часть “замираний” без единой правки конфигурации. - Разделение итогов на горячих регистрах накопления - параллельные проведения перестают конкурировать за одну строку остатка. Граница у приёма жёсткая, и про неё обычно забывают: он разводит писателей между собой и никак не помогает там, где документ в одной транзакции пишет движения и сам же читает по ним итоги. Чтение остатка суммирует все дорожки сразу, поэтому от писателей оно не разъезжается, а с ростом числа разделителей такой транзакции становится только хуже.
- Короткие транзакции. Из транзакции проведения вынести всё, что не про запись данных: внешние вызовы, письма, тяжёлые пересчёты, длинные циклы.
- Индексы под запросы проведения - чтобы блокировать только своё, а не скан пол-таблицы.
- Единый порядок обхода ресурсов во всех проводящих процедурах - против взаимоблокировок.
- Дробление “толстых” документов и вынос массовых пересчётов из онлайна - чтобы не ловить эскалацию блокировок на таблицу.
Чего делать не надо: поднимать таймаут блокировки “чтобы не ругалось”, отключать
управляемые блокировки, расставлять NOLOCK по отчётам ради тишины. Всё это не убирает
конкуренцию, а прячет её - и добавляет к тормозам ещё и грязные чтения.
Как отличить блокировки от “медленного запроса”
Практический признак, который экономит часы. Возьмите одну и ту же тяжёлую операцию и сравните её поведение в тишине и в пик:
- Операция одинаково небыстрая всегда, и сервер при ней загружен - это про запрос: план, индексы, объём данных. Сюда - приёмы оптимизации запросов.
- Операция быстрая в тишине и встаёт в пик, а сервер при этом не загружен - это про блокировки: конкуренция за данные. Сюда - всё, о чём выше.
Мы однажды разбирали запрос, который выполнялся 39 минут. По монитору СУБД 95 % этого времени он не занимал процессор; по расписанию шаг стоял сразу за массовой записью цен. Интуиция звала переписывать текст запроса, помогло сузить то, что он читает. Мораль прежняя: узкое место определяется по фактическим замерам, а не по ощущению, что “код кривой”.
Итог
Когда в пик встаёт даже мощный сервер, дело почти никогда не в железе и редко - в “тяжёлом запросе”. Дело в том, что пользователи ждут друг друга на общих данных: остатках регистров, номерах документов, единственных строках настроек. Разбирается это предсказуемо - управляемые и СУБД-блокировки, эскалация, взаимоблокировки, лишний параллелизм, - и лечится по уровням: сперва настройки экземпляра и версионирование, затем разделение итогов и короткие транзакции, и лишь в последнюю очередь - правки кода.
Но всё начинается с диагноза по техжурналу и монитору СУБД, а не по интуиции: без факта “кто кого ждал на каком ресурсе” любые правки - стрельба наугад.
Самый наглядный пример такой ошибки в диагнозе - тот самый 39-минутный запрос, который почти всё это время ждал. После правки того, что он читает, а не того, как считает, осталось 6 секунд. Как отличить ждущий запрос от работающего до первой правки, разобрано вместе с этим случаем.
Если у вас розница на типовой конфигурации, стоит посмотреть и шире одного симптома: где вообще проходит граница между тем, что закрывает типовая Розница для Казахстана, и тем, что останавливает магазины на практике - разбор здесь.
Если у вас в час пик встают кассы или проведение, а сервер при этом скучает - начните с бесплатного экспресс-аудита: снимем реальную картину ожиданий по монитору СУБД и техжурналу и покажем, что именно упирается в блокировки, а что - в запросы. Разбор и устранение узких мест целиком - услуга оптимизация производительности 1С.
Готовые обработки по теме
Чек-ап СУБД под 1С проверяет MAXDOP, cost threshold и другие настройки параллелизма с вердиктом по каждой (на странице демо-отчёт на боевой базе), а Оптимизатор временных таблиц убирает мусор из tempdb, из-за которого пакетные запросы держат блокировки дольше нужного.
Если вопрос стоит иначе - не “что настроено неправильно”, а “кто именно держал базу в тот момент”, - это Анализ нагрузки кластера 1С: он копит историю снимков через сервер администрирования кластера и по ней называет, кто держал базу за выбранный период: сеанс, пользователя, компьютер и приложение. К СУБД он не обращается.