Итак… Существует проблема, несколько пользователей в базе данных одновременно создают пакеты документов (в нашем примере «Реализация товаров и услуг»). Мы знаем, что данный документ сам по себе очень тяжелый для проведения и перепроведения. Было принято решение об оптимизации проведения данного документа.
1) Перевод типовой конфигурации в режим управляемых блокировок. Открываем свойства конфигурации и в самом низу смотрим.
ВНИМАНИЕ! Если у Вас в типовой конфигурации нет кода по работе с управляемыми блокировками, тогда Вам необходимо перевести базу на управляемые блокировки самостоятельно или пропустить данный пункт.
Как определить, есть ли модули у Вас в конфигурации по управлению блокировками? Да очень просто. В конфигураторе — правка — глобальный поиск введите «глЗначениеПеременной(«ИспользоватьБлокировкуДанных»)». Если у Вас в различных модулях присутствуют условия на значение данной переменной, это признак того, что конфигурация поддерживает управляемый режим. Более подробнее о том, что такое блокировки и взаимоблокировки, можно почитать тут http://1c.chistov.pro/2013/07/blog-post_25.html Это статья Чистова, на его сайте вообще много чего интересного есть на данную тему.
2) Необходимо проанализировать, по каким регистрам выполняется движение регистратором «Реализация товаров и услуг», и для каждого из регистров накопления поставить галочку «Разрешить разделение итогов». Источник информации http://www.gilev.ru/timeoutlock/ (Статья Гилева)
Это механизм, который обеспечивает параллельность работы (имеется в виду увеличение производительности при параллельной работе нескольких задач с одним регистром). Механизм реализован для регистров накопления и регистров бухгалтерии. Быстрей читает, медленней записывает.
Разрешить разделение итогов. Если флаг установлен в значение Истина, то будет задействован механизм разделителя итогов, который обеспечивает более высокую параллельность работы при записи в регистр. Система при одновременной записи движений несколькими сеансами не будет обновлять одни и те же записи итогов, а будет записывать изменения итогов в отдельные записи. При получении итогов эти данные складываются. Таким образом, обеспечивается и поддержание в актуальном состоянии итогов (для быстрого получения отчетов, например), и параллельность записи движений. Этот режим требует дополнительных расходов ресурсов (например, увеличивается количество данных в итоговых таблицах). Поэтому есть свойства и в конфигурации, и в языке для управления этим режимом.
Записи будут «размножаться» только при параллельно выполняемых транзакциях. Их количество по каждой комбинации измерений будет зависеть от максимального количества одновременно выполняемых транзакций. При пересчете итогов накопленные отдельные записи сворачиваются.
Также необходимо проконтролировать, чтобы в режиме пользователя данная галочка тоже была включена.
Заходим в режим 1С Предприятия — Операции — Управление итогами — закладка установка режима разделения итогов.
Галочки должны быть включены.
3) Веря данной статье http://www.gilev.ru/timeoutlock/ одной из основных проблем блокировок является запись в последовательности документов. Теперь посмотрим на документ «Реализация товаров и услуг»
Я, конечно, понимаю, что все они нужны и созданы для того, чтобы мы ими пользовались. Ну а по факту, как оказалось, мне нужна только одна последовательность ПартионныйУчетБУ (При большом желании можно обойтись и без нее, написав свою процедуру обхода документов для проведения по партиям. Чем это грозит? Тем, что себестоимость по документам, которые проведенны в одно и то же время с одной и той же номенклатурой, по одному и тому же складу будет иметь небольшую неточность). Управленческая себестоимость, как оказалось, не нужна вообще, т.к. в производственных компаниях с трудом считают себестоимость по бухгалтерскому учету, не говоря уж об управленческом.
Но все это мое мнение, вы можете его принять, а можете не принимать, за свою базу ответственность несете только Вы.
Итак… в данном примере я оставил движение только по одной последовательности.
4) Контроль остатков. Скажем так, если его нет, то и проблем практически нет ))) Но мы же понимаем, что если дать такую возможность пользователям, то в базу через месяц страшно будет заглянуть. Поэтому постараемся найти компромисс. В данном случае документ реализация контролирует остатки, грубо говоря, в трех местах:
1) Товары на складах;
2) Товары организаций;
3) Взаиморасчеты по документам расчетов (используется не так часто);
Давайте разберемся с регистрами 1 и 2. Оба регистра содержат остатки товаров, но один в разрезе организаций, а другой в разрезе складов. Теперь давайте подумаем )) Бывает такое, что склад принадлежал бы одновременно двум организациям? Да, наверное бывает, но я не встречал. Этот регистр можно вообще исключить из проведения (в том числе выключить контроль остатков по нему) в случае, если вы не ведете учет по комисионерам. В данном примере я выключаю контроль остатков по регистру «Товары организаций».
Регистр «Товары на складах», в данном случае остатки нам контролировать необходимо. Но и тут можно кое-что придумать. Если вы не используете резервирование и ордерную схему, то запрос можно оптимизировать до примитивного, см. http://1c.chistov.pro/2013/07/blog-post_25.html. Но я не стал трогать типовой запрос и оставил как есть, с оглядкой на то, что ежели что, то переделаю.
5) В базе данных не всегда проводятся новые документы, примерно 30% перепроводится. Т.е. пользователь заходит в существующий документ, что-то в нем меняет и нажимает ок. Что делает в это время система? Система в это время лезет в каждый регистр, для которого документ является регистратором, и чистит его, более того, недавно открыл для себя, что система это делает всякий раз, даже если документ не проведен, а просто записан. Теперь давайте выполним перепроведение одного документа «Реализация товаров и услуг» с замером производительности.
При замере производительности меня привлекли строки, которые отмечены красными флажками. Давайте поэтапно разберем каждую из них.
1) Вызывается из модуля «ПолныеПрава» 29 раз «НаборЗаписей.Записать()» это и есть та злосчастная процедура, которая выполняет чистку регистров и запись регистров. Инициализация вызова данной процедуры происходит из модуля объекта документа «Реализация товаров и услуг». Вот он, этот кусок кода:
Если начать копать дальше, то мы увидим, что в модуле «ОбщийМодуль» есть процедура «Процедура УдалитьЗаписанныеДвиженияДокумента(ДокументОбъект, Отказ, ВыборочноОчищатьРегистры, РежимПроведенияДокумента)», которая в свою очередь вызывает процедуру «ПолныеПрава.ЗаписатьНаборЗаписейНаСервере(ИмяРегистра, ДокументОбъект.Ссылка,, ТипРегистра);»
Если опустить все частности и вкратце изложить суть проблемы, то выглядеть это будет так:
Всякий раз при перепроведении, проведении документа система по методанным перебирает все регистры и выполняет их запись, даже если эти регистры не используются и данные там не содержатся. В документе «Реализация товаров и услуг» до его оптимизации я насчитал 41 вызов.
Если вернуться к замеру производительности и посмотреть на оставшиеся две строчки, то мы увидим, что проблема примерно таже самая. Во втором варианте из модуля «НастройкаПравДоступа» идет вызов процедуры «ПроверкаСуществующихЗаписейРегистра(НаборЗаписей, СтруктураПараметров, Отказ)» инициализирует ее подписка на событие «ПередЗаписьюРегистраНакопленияДатаЗапретаРедактирования». В данном случае выполняется запрос в транзакции на проверку периода в записях регистров.
Ну и третья строчка из замера производительности связа с обменом по организации, если у Вас такового нет, то значит Вам повезло). В конфигурации, по которой я описываю данный пример — обмен присутствует. Суть в следующем, перед записью регистра накопления выполняется его чтение и отправляется на регистрацию в другие узлы к удалению записей.
Что общего между этими тремя проблемами? А общая проблема заключается в том, что каждая из этих процедур выполняет избыточную обработку. Т.е. В первой проблеме выполняется чистка движений регистров, в которых никогда данные и не появлялись, во второй выполняется запрос к тем же регистрам, в которые документ движения не делает, ну и т.д.
Можно ли решить данную проблему? Думаю, что можно, причем многими способами. Наилучший способ — это снять движения регистратора с этих регистров, но это очень трудоемкий процесс, который может привести к очень большому количеству ошибок, а в дальнейшем Вам будет сложно задействовать како-либо типовой функционал.
Наиболее корректным способом будет являться указание конфигурации, какие регистры необходимо записывать. В новых конфигурациях на платформе 8.3 (управляемые формы) именно так и делают. Но не переписывать ведь типовой функционал УПП на платформе 8.2. Я поступил следующим образом:
1) Создал регистр сведений «Настройка записей регистров»
2) В процедуре «УстановкаПараметровСеанса» дописал следующий кусок кода:
////////////////////////////////ESC START////////////////////////////////////////
// Автор: Пастухов А.Г.
// Дата: 18.01.2014
//
// Описание: Оптимизация. Таблица служит для дальнейшего обращения
//
//
ТаблицаДокументовПодлежащихОптимизации = Новый ТаблицаЗначений;
ТаблицаДокументовПодлежащихОптимизации.Колонки.Добавить("Документ");
ТаблицаДокументовПодлежащихОптимизации.Колонки.Добавить("СписокРегистров");
ТаблицаДокументовПодлежащихОптимизации.Колонки.Добавить("Условие");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗРЕШЕННЫЕ
| НастройкиОчищенияРегистровДокументов.НаименованиеДокумента КАК НаименованиеДокумента,
| НастройкиОчищенияРегистровДокументов.НаименованиеРегистра КАК НаименованиеРегистра,
| УсловияОчищенияРегистровДокументов.Условие КАК Условие
|ИЗ
| РегистрСведений.НастройкиОчищенияРегистровДокументов КАК НастройкиОчищенияРегистровДокументов
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.УсловияОчищенияРегистровДокументов КАК УсловияОчищенияРегистровДокументов
| ПО НастройкиОчищенияРегистровДокументов.НаименованиеДокумента = УсловияОчищенияРегистровДокументов.НаименованиеДокумента
|ИТОГИ ПО
| НаименованиеДокумента,
| Условие";
Результат = Запрос.Выполнить();
ВыборкаНаименованиеДокумента = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНаименованиеДокумента.Следующий() Цикл
НоваяСтрока = ТаблицаДокументовПодлежащихОптимизации.Добавить();
НоваяСтрока.Документ = ВыборкаНаименованиеДокумента.НаименованиеДокумента;
ВыборкаУсловие = ВыборкаНаименованиеДокумента.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаУсловие.Следующий() Цикл
НоваяСтрока.Условие = ВыборкаУсловие.Условие;
СписокРегистров = Новый СписокЗначений;
Выборка = ВыборкаУсловие.Выбрать();
Пока Выборка.Следующий() Цикл
СписокРегистров.Добавить(Выборка.НаименованиеРегистра);
КонецЦикла;
НоваяСтрока.СписокРегистров = СписокРегистров;
КонецЦикла;
КонецЦикла;
Хранилище = новый ХранилищеЗначения(ТаблицаДокументовПодлежащихОптимизации);
ПараметрыСеанса.ТаблицаДокументовПодлежащихОптимизации = Хранилище;
////////////////////////////////ESC FINISH///////////////////////////////////////
Данный регистр «НастройкиОчищенияРегистровДокументов» хранит сведения о том, что очищать, а регистр сведений «УсловияОчищенияРегистровДокументов» хранит условия — в каком случае очищать.
Выглядит это примерно так:
Это означает, что оптимизированный алгоритм будет выполнен в случае, если будет выполнено условие.
Параметр сеанса необходим для того, чтобы не делать постаянны запросов к регистру сведений, иначе можем получить ошибку блокировки на новом участке.
3) Дорабатываем собственно сами процедуры, которые имеют множественные обращения к регистрам с намерению записи.
Приведу пример на процедуре:
УдалитьДвиженияРегистратора(ЭтотОбъект, Отказ, Истина, ТекущийРежимПроведенияДокумента);
Данную процедуру вы можете найти в событии «ОбработкаПроведение» модуля объекта, практически любого документа.
Переработанный код процедуры можете посмотреть ниже:
// Процедура очистки записанных движений документа
//
// Параметры:
// ДокументОбъект - документ, движения которого удаляются
// Отказ - булево, признак отказа
// ВыборочноОчищатьРегистры - булево, признак выборочной очистки наборов записей
// Если Истина - часть наборов записей не будут очищены, будут очищены только коллекции движений
// РежимПроведенияДокумента - режим проведения (оперативный / неоперативный),
// нужен для составления списка регистров, которые не надо очищать
Процедура УдалитьЗаписанныеДвиженияДокумента(ДокументОбъект, Отказ, ВыборочноОчищатьРегистры, РежимПроведенияДокумента)
// Получим перечень регистров, движения по которым нужно очистить
МассивРегистров = МассивРегистровНужноОчистить(ДокументОбъект);
Если МассивРегистров.Количество() = 0 Тогда
Возврат;
КонецЕсли;
// Если очищать регистры надо выборочно, то подготовим список таких регистров,
// которые можно не очищать при перепроведении
Если ВыборочноОчищатьРегистры Тогда
РегистрыДляОптимизацииПерезаписиДвижений = ПолучитьРегистрыДляОптимизацииПерезаписиДвижений(РежимПроведенияДокумента);
КонецЕсли;
// Переменные логики отложенного проведения
ДокументИспользуетсяВОтложенномПроведении = Ложь;
ВыполняетсяДопроведение = Ложь;
ПроведениеПоВсемВидамУчета = Ложь;
СтруктураПараметровПроведения = ОтложенноеПроведениеДокументов.ПолучитьПараметрыПроведенияДокумента(ДокументОбъект);
ДокументИспользуетсяВОтложенномПроведении = СтруктураПараметровПроведения.ДокументИспользуетсяВОтложенномПроведении;
Если ДокументИспользуетсяВОтложенномПроведении Тогда
ВыполняетсяДопроведение = СтруктураПараметровПроведения.ВыполняетсяДопроведение;
ПроведениеПоВсемВидамУчета = СтруктураПараметровПроведения.ПроведениеПоВсемВидамУчета;
РегистрыОтложенногоПроведения = ОтложенноеПроведениеДокументов.ПолучитьРегистрыОтложенногоПроведения();
КонецЕсли;
////////////////////////////////ESC START////////////////////////////////////////
// Автор: Пастухов А.Г.
// Дата: 20.01.2014
//
// Описание:
//
//
СтруктураЧисткиРегистров = ПолучитьСтруктуруОчисткиРегистров(ДокументОбъект);
////////////////////////////////ESC FINISH///////////////////////////////////////
//Обойдем список регистров, по которым существуют движения, и выполним очистку необходимых регистров
Для Каждого ПолноеИмяРегистра ИЗ МассивРегистров Цикл
// Имя регистра передается как значение,
// полученное с помощью функции ПолноеИмя() метаданных регистра
ПозицияТочки = Найти(ПолноеИмяРегистра, ".");
ТипРегистра = Лев(ПолноеИмяРегистра, ПозицияТочки - 1);
ИмяРегистра = СокрП(Сред(ПолноеИмяРегистра, ПозицияТочки + 1));
// Используется для оптимизации перезаписи движений платформой
// Если значение Ложь, набор записей не будет очищен
// По умолчанию все движения надо удалять
УдалятьДвижения = Истина;
Если ВыборочноОчищатьРегистры И РегистрыДляОптимизацииПерезаписиДвижений.Найти(ИмяРегистра) <> Неопределено Тогда
УдалятьДвижения = Ложь;
КонецЕсли;
////////////////////////////////ESC START////////////////////////////////////////
// Автор: Пастухов А.Г.
// Дата: 17.01.2014
//
// Описание: Оптимизация перезаписи регистров для документа "Реализация товаров"
//
//
Если УдалятьДвижения Тогда
Если СтруктураЧисткиРегистров.НакладыватьОтборНаЧисткуРегистров Тогда
ПрименятьУсловиеОчистки = ?(ЗначениеЗаполнено(СтруктураЧисткиРегистров.Условие),Истина,ЛОЖЬ);
Если ПрименятьУсловиеОчистки Тогда
РезультатУсловия = Ложь;
Выполнить("РезультатУсловия = "+СтруктураЧисткиРегистров.Условие);
Если РезультатУсловия Тогда
СписокРегистров = СтруктураЧисткиРегистров.СписокРегистров;
НайденноеЗначение = СписокРегистров.НайтиПоЗначению(ПолноеИмяРегистра);
Если НайденноеЗначение = Неопределено Тогда
УдалятьДвижения = ложь;
Продолжить;
КонецЕсли;
КонецЕсли;
Иначе
СписокРегистров = СтруктураЧисткиРегистров.СписокРегистров;
НайденноеЗначение = СписокРегистров.НайтиПоЗначению(ПолноеИмяРегистра);
Если НайденноеЗначение = Неопределено Тогда
УдалятьДвижения = ложь;
Продолжить;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЕсли;
////////////////////////////////ESC FINISH///////////////////////////////////////
Если ДокументИспользуетсяВОтложенномПроведении Тогда
Если ВыполняетсяДопроведение Тогда
// Если выполняется допроведение, то удаляются только движения по регистрам,
// которые формируются при отложенном проведении
Если РегистрыОтложенногоПроведения.Найти(СокрЛП(ПолноеИмяРегистра)) = Неопределено Тогда
Продолжить;
КонецЕсли;
Иначе
// Документ проводится в режиме отложенного проведения
// Если для документа выключен режим проведения "по всем видам учета",
// то всегда удаляются движения по регистрам, которые формируются при допроведении
// (независимо от того, входят ли они в список РегистрыДляОптимизацииПерезаписиДвижений)
Если НЕ УдалятьДвижения
И НЕ ПроведениеПоВсемВидамУчета
И РегистрыОтложенногоПроведения.Найти(СокрЛП(ПолноеИмяРегистра)) <> Неопределено Тогда
УдалятьДвижения = Истина;
КонецЕсли;
КонецЕсли;
КонецЕсли;
Если УдалятьДвижения Тогда
// Удаление движений происходит без контроля доступа - передаем пустую таблицу движений
ПолныеПрава.ЗаписатьНаборЗаписейНаСервере(ИмяРегистра, ДокументОбъект.Ссылка,, ТипРегистра);
Иначе
//Установим признак модифицированности набора записей, чтобы записались все коллекции движений, по которым
//были записи на момент проведения
ДокументОбъект.Движения[ИмяРегистра].Очистить();
КонецЕсли;
КонецЦикла;
КонецПроцедуры
////////////////////////////////ESC START////////////////////////////////////////
// Автор: Пастухов А.Г.
// Дата: 17.01.2014
//
// Описание:
//
//
Функция ПолучитьСтруктуруОчисткиРегистров(ДокументОбъект) Экспорт
НаименованиеДокумента = ДокументОбъект.Метаданные().Имя;
СтруктураЧисткиРегистров = Новый Структура;
СписокРегистров = Новый СписокЗначений;
СтруктураПоиска = Новый Структура;
СтруктураПоиска.Вставить("Документ",НаименованиеДокумента);
ТаблицаДокументовПодлежащихОптимизации = ПараметрыСеанса.ТаблицаДокументовПодлежащихОптимизации.Получить();
НайденныеСтроки = ТаблицаДокументовПодлежащихОптимизации.НайтиСтроки(СтруктураПоиска);
ЗначениеУсловия = "";
Если НайденныеСтроки.Количество()>0 Тогда
НайденнаяСтрока = НайденныеСтроки[0];
ЗначениеУсловия = НайденнаяСтрока.Условие;
СписокРегистров = НайденнаяСтрока.СписокРегистров;
КонецЕсли;
СтруктураЧисткиРегистров.Вставить("СписокРегистров",СписокРегистров);
СтруктураЧисткиРегистров.Вставить("Условие",ЗначениеУсловия);
Если СписокРегистров.Количество()>0 Тогда
СтруктураЧисткиРегистров.Вставить("НакладыватьОтборНаЧисткуРегистров",Истина);
Иначе
СтруктураЧисткиРегистров.Вставить("НакладыватьОтборНаЧисткуРегистров",Ложь);
КонецЕсли;
Возврат СтруктураЧисткиРегистров;
КонецФункции
////////////////////////////////ESC FINISH///////////////////////////////////////
По сути в двух словах. Данная доработка позволила выполнить проверку на предмет необходимости очистки текущего вызываемого регистра.
После повторного замера производительности вместо 49 вызовов я увидел их ровно столько, сколько указал записей в регистре сведений «Настройки очищения регистров документов». При перепроведении документов блокировок стало заметно меньше.
Данный комплекс действий позволил запустить предприятие документооборотом 5-7 тыс. документов отгрузки и 70-80 пользователями.
Необходимо отметить, что проведение по партиям является отложенным.
По всей статье я старался указывать ссылки на другие статьи, которые помогли скомпоновать мысли в кучу и придумать какой-либо выход, т.к. при пилотном запуске (в типовом виде) база данных загибалась от блокировок и взаимоблокировок.
В запасе у меня осталось еще два способа, которые я не применил, т.к. база данных стала себя вести адекватно.
1) Переход на единый регистр остатков «Свободные остатки»
Это позволит оптимизировать запросы контроля остатков. Вместо 3-4 регистров используется один.
2) Создать отложенное проведение регистра бухгалтерии «Типовой».
Как мы знаем, регистр бухгалтерии — самый медленный регистр в мире ) Наиболее часто блокировки и взаимоблокировки возникают именно на регистрах бухгалтерии. Если у Вас есть возможность применения отложенного метода проведения данного регистра — используйте ее. Суть данного метода в следующем: использование одного единственного сеанса для допроведения всех документов, связанных с проблемным регистром, для исключения проблемы параллельной записи и соответственно проблемы взаимоблокировок.
Настраивается это тут:
В заключении хотелось бы сказать, что типовая конфигурация является Оооочень универсальной и порой данный универсализм Оооочень мешает производительности системы на крупных предприятиях, поэтому подход к разработке и доработке конфигураций под таких заказчиков определяется из потребностей бизнеса, а не из целей минимальных доработок типовой конфигурации для наименьших затрат человеко-ресурсов
По поводу той конфигурации, на которой это все разрабатывалось, хочу сказать, что она обновляется без особых проблем и на данный момент предприятие работает на актуальном релизе.
Наоборот.
А показатели APDEX можно привести?
(2) iTony73, Мне отладчика было достаточно.
(1) asved.ru, Опередил меня 🙂