Методика, представленная в статье, предназначена для конфигурации Управление холдингом, однако может использоваться для любой конфигурации 1С, в которой используется стандартный механизм обновления БСП. Статья написана, чтобы помочь людям, столкнувшимся с аналогичной проблемой, достаточно просто решить эту задачу. Методика не предлагается как окончательная и единственно верная. Конструктивная критика и рекомендации по улучшению приветствуются. Тупой хэйт будет игнорироваться или удаляться. Надеюсь, совместными усилиями мы доведём методику до совершенства.
В нашей компании используется конфигурация 1С: Управление холдингом. В конце 2023 была поставлена задача УХ с версии 3.2.3.58 до 3.2.5.32.
Проблема заключалась в том, что обновление в режиме предприятия выполнялось, исключительно долго. Пришлось вмешиваться и разбираться. Оказалось дело том, что в обновлении есть ряд тяжелых обработчиков, которые выполнялись каждый по 6-12 часов, но один обработчик особо отличился, скорость выполнения которого равна 1 объект в секунду, а таких объектов около 2,5 млн., то есть расчетное время выполнения которого должно составлять 28,93 суток (2,5 млн /(60*60*24)). Весь процесс обновления должны был выполняться за 31 сутки. Но надо отметить это на тестовом сервере. В продакшене производительность выше в 2 раза, то есть расчетное время примерно 15 суток, что никак не решало проблему. Технологическое окно для этой базы составляет 2,5 суток. На праздниках может быть больше, но это тоже не выход. При этом следует заметить, что процесс обновления очень слабо нагружает сервер 1С и сервер БД.
Таким образом было решено сделать многопоточное обновление, используя средства БСП. Почему средствами БСП, спросите вы? Потому что из коробки очень простой многопоточный механизм, который мы уже использовали в своих разработках.
Постановка задачи:
9 длительных обработчиков обновлений, один из которых выполняется 28,93 суток, а суммарно 31 сутки. Целевое время выполнения не более 2 суток. Из этого следует, что обновление надо выполнять минимум в 15 потоков.
Поиск тяжелых обработчиков
Тут всё просто, в тестовой базе запускал обновление.
- Подключался отладчиком: меню Отладка-Подключение-Автоматическое подключение, выбрать флажки Клиентские и внешние соединения, Фоновые задания.
- Периодически мониторю журнал регистрации, когда в ЖР вижу более получаса однотипные записи, останавливаю отладку (Отладка-Остановить), в фоновом задании, в котором выполняется обновление анализирую стек вызовов, нахожу процедуру-обработчик обновления. Понять это можно по общему модулю, в котором находится процедура, название будет начинаться с ОбновлениеИнформационнойБазы. В случае УХ они называться: ОбновлениеИнформационнойБазыУХ и ОбработчикиОбновленияУХКазначейство (хотя это не по стандарту, но в УХ много чего не по стандарту).
Как правило, обработчики обновления заполняют новые реквизиты в документах, справочниках, регистрах. Алгоритм у низ следующий:
- Выбрать данные запросом
- Обход запроса в цикле
- Заполнение нового реквизита
- Запись объекта
Например, описанной выше методикой нашли обработчик ОбработчикиОбновленияУХКазначейство.ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета(). Он заполняет в документе СписаниеСРасчетногоСчета реквизит ДокументПланирования. Ниже приведен код обработчика:
Процедура ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СписаниеСРасчетногоСчета.Ссылка КАК Ссылка,
| СписаниеСРасчетногоСчета.УдалитьДокументПланирования КАК УдалитьДокументПланирования,
| СписаниеСРасчетногоСчета.ДокументПланирования КАК ДокументПланирования
|ИЗ
| Документ.СписаниеСРасчетногоСчета КАК СписаниеСРасчетногоСчета
|ГДЕ
| СписаниеСРасчетногоСчета.УдалитьДокументПланирования <> НЕОПРЕДЕЛЕНО
| И СписаниеСРасчетногоСчета.ДокументПланирования = НЕОПРЕДЕЛЕНО";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
ДокументОбъект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
ДокументОбъект.ДокументПланирования = ВыборкаДетальныеЗаписи.УдалитьДокументПланирования;
ОбновлениеИнформационнойБазы.ЗаписатьОбъект(ДокументОбъект);
КонецЦикла;
КонецПроцедуры
Перепишем код обработчика в многопоточную реализацию.
Создадим расширение МногопоточноеОбновление_УХ32358_УХ32532.
В нём общий серверный модуль УХ32Ускор_МногопоточноеПроведение с вызовом сервера.
Пример реализации:
- В модуле УХ32Ускор_МногопоточноеПроведение напишем процедуру СоздадимПотокиНаСервереДляТЗ, которая раскладывает на потоки данных из таблицы значений (аргумент ТЗ) и запускает фоновые задания, плюс сервисную процедурау ЗаполнитьПараметрыПотока. Процедуры универсальная, подходит для любого обработчика обновления, работающего с одной выборкой данных, пишется один раз.
Функция СоздадимПотокиНаСервереДляТЗ(УникальныйИдентификатор, ИмяФункцииИлиПроцедуры, ТЗ)
КонстКоличествоПотоков = Константы.КоличествоПотоковДлительныхОпераций.Получить();
КоличествоПотоков = ?(КонстКоличествоПотоков=0, 4, КонстКоличествоПотоков); // Минимально хотя бы 4 потока
МинПорция = 100; // Если порция маленькая, нет смысла запускать в потоках
КолОбъектов = ТЗ.Количество();
КоличествоПотоков = ?(КолОбъектов<=МинПорция, 1, КоличествоПотоков);
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(УникальныйИдентификатор);
ПараметрыВыполнения.НаименованиеФоновогоЗадания = СтрШаблон("Многопоточно выполнение: %1", ИмяФункцииИлиПроцедуры);
ПараметрыВыполнения.ЗапуститьВФоне = Истина;
РазмерПорции = Окр(КолОбъектов / КоличествоПотоков, 0);
МассивПорции = Новый Массив;
СчПорции = 0;
НомерПотока = 1;
ПараметрыПотоков = Новый Соответствие();
ПоследнийПоток = Ложь; // Чтобы остатки элементов для обработки положить в последний поток
Для Каждого стрТЗ из ТЗ Цикл
МассивПорции.Добавить(ОбщегоНазначения.СтрокаТаблицыЗначенийВСтруктуру(стрТЗ));
СчПорции = СчПорции+1;
Если СчПорции%РазмерПорции=0 и НЕ ПоследнийПоток Тогда
ЗаполнитьПараметрыПотока(МассивПорции, НомерПотока, ПараметрыПотоков); // Положим порцию в ПараметрыПотоков
// Инициализация нового потока
МассивПорции = Новый Массив;
НомерПотока = НомерПотока+1;
КонецЕсли;
КонецЦикла;
// Весь остаток объектов положим в последний поток
Если МассивПорции.Количество()>0 Тогда
ЗаполнитьПараметрыПотока(МассивПорции, НомерПотока, ПараметрыПотоков);
КонецЕсли;
ФоновоеЗадание = ДлительныеОперации.ВыполнитьПроцедуруВНесколькоПотоков(ИмяФункцииИлиПроцедуры, ПараметрыВыполнения, ПараметрыПотоков);
Если ФоновоеЗадание.Статус = "Ошибка" Тогда
Сообщить(СокрЛП(ФоновоеЗадание.Статус));
КонецЕсли;
Возврат ФоновоеЗадание;
КонецФункции
Процедура ЗаполнитьПараметрыПотока(МассивПорции, НомерПотока, ПараметрыПотоков)
КодПотока = "Поток " + НомерПотока;
ПараметрыПотока = Новый Структура;
ПараметрыПотока.Вставить("ОбъектыДляОбработки", МассивПорции);
ПараметрыВызоваСервера = Новый Массив;
ПараметрыВызоваСервера.Добавить(КодПотока);
ПараметрыВызоваСервера.Добавить(ПараметрыПотока);
ПараметрыПотоков.Вставить(КодПотока, ПараметрыВызоваСервера);
КонецПроцедуры
Обратите внимание на строку МассивПорции.Добавить(ОбщегоНазначения.СтрокаТаблицыЗначенийВСтруктуру(стрТЗ));
Она нужна, чтобы в обработчик потока получил эмуляцию строки выборки запроса (или таблицы значения). По-другому прокинуть не получится, будет ошибка передачи мутабельного значения.
- Процедура ожидания выполнения потоков ОбработатьДанныеТЗМногопоточно. Процедура универсальная, пишется один раз.
Процедура ОбработатьДанныеТЗМногопоточно(РезультатЗапроса, ИмяПроцедуры)
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
тз = РезультатЗапроса.Выгрузить();
КолОбъектов = ТЗ.Количество();
ВремяНачала = ТекущаяДата();
УникальныйИдентификатор = Новый УникальныйИдентификатор;
Задание = СоздадимПотокиНаСервереДляТЗ(УникальныйИдентификатор, ИмяПроцедуры, тз);
ИдентификаторЗадания = Задание.ИдентификаторЗадания;
инд = 0;
Попытка
//Пока Задание.Статус = "Выполняется" Цикл
Пока ДлительныеОперации.ОперацияВыполнена(ИдентификаторЗадания, Задание).Статус = "Выполняется" Цикл
// Ожидаем выполнения задания
инд = инд +1;
КонецЦикла;
Исключение
КонецПопытки;
ВремяВыполнения = Окр((ТекущаяДата()-ВремяНачала)/60,2);
ИмяСобытия = СтрШаблон("Начало процедуры %1_Поток", ИмяПроцедуры);
ЖурналРегистрации.ДобавитьСообщениеДляЖурналаРегистрации(ИмяСобытия,
УровеньЖурналаРегистрации.Информация,,, СтрШаблон("Процедура выполнена, обработано %1 за %2 мин", КолОбъектов, ВремяВыполнения));
КонецПроцедуры
- Выборка данных, пишется на основании оригинальной процедуры обработчика, в нашем случае ОбработчикиОбновленияУХКазначейство.ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета, оставляем только код выборки данных, сравните с Для каждого обработчика обновления.
Процедура ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СписаниеСРасчетногоСчета.Ссылка КАК Ссылка,
| СписаниеСРасчетногоСчета.УдалитьДокументПланирования КАК УдалитьДокументПланирования,
| СписаниеСРасчетногоСчета.ДокументПланирования КАК ДокументПланирования
|ИЗ
| Документ.СписаниеСРасчетногоСчета КАК СписаниеСРасчетногоСчета
|ГДЕ
| СписаниеСРасчетногоСчета.УдалитьДокументПланирования <> НЕОПРЕДЕЛЕНО
| И СписаниеСРасчетногоСчета.ДокументПланирования = НЕОПРЕДЕЛЕНО";
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат;
КонецЕсли;
// СА Многопоточная реализация в.2
ИмяПроцедуры = "УХ32Ускор_МногопоточноеПроведение.ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета_Поток";
ОбработатьДанныеТЗМногопоточно(РезультатЗапроса, ИмяПроцедуры);
КонецПроцедуры
- Обработчик потока, пишется на основании оригинальной процедуры обработчика, оставляем только код обработки данных.
Процедура ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета_Поток(Поток, Параметры) Экспорт
ИмяПроцедуры = "ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета_Поток";
ИмяСобытия = "Выполнение потока";
ЖурналРегистрации.ДобавитьСообщениеДляЖурналаРегистрации(ИмяСобытия,
УровеньЖурналаРегистрации.Информация,,,СтрШаблон("Начат %1 поток %2", ИмяПроцедуры, Поток));
Сч = 0;
Для каждого Выборка Из Параметры.ОбъектыДляОбработки Цикл
ДокументОбъект = Выборка.Ссылка.ПолучитьОбъект();
ДокументОбъект.ДокументПланирования = Выборка.УдалитьДокументПланирования;
ОбновлениеИнформационнойБазы.ЗаписатьОбъект(ДокументОбъект);
Сч=Сч+1;
КонецЦикла;
ЖурналРегистрации.ДобавитьСообщениеДляЖурналаРегистрации(ИмяСобытия,
УровеньЖурналаРегистрации.Информация,,, СтрШаблон("Завершен %1 поток %2 обработано %3", ИмяПроцедуры, Поток, Формат(Сч,"ЧГ=0")));
КонецПроцедуры
Многопоточная реализация готова.
- Теперь нужно выполнить этот обработчик до выполнения типового обновления. Для этого с помощью расширения доопределим процедуру СтандартныеПодсистемыКлиент.ПередНачаломРаботыСистемы() с вызовом &Перед, в обработчике события вызовем наш обработчик обновления УХ32Ускор_МногопоточноеПроведение.УХ32Ускор_ПередНачаломРаботыСистемы_НаСервере():
&Перед("ПередНачаломРаботыСистемы")
Процедура УХ32Ускор_ПередНачаломРаботыСистемы(Знач ОповещениеЗавершения)
УХ32Ускор_МногопоточноеПроведение.УХ32Ускор_ПередНачаломРаботыСистемы_НаСервере();
КонецПроцедуры
Далее код самого обработчика в модуле УХ32Ускор_МногопоточноеПроведение:
Процедура УХ32Ускор_ПередНачаломРаботыСистемы_НаСервере() Экспорт
УстановитьКоличествоПотоков(30); // При необходимости устанавливаем константу КоличествоПотоковДлительныхОпераций
ЗаполнитьДокументПланированияСписаниеСРасчетногоСчета();
КонецПроцедуры
Процедура УстановитьКоличествоПотоков(КолПотоков)
Константы.КоличествоПотоковДлительныхОпераций.Установить(КолПотоков);
КонецПроцедуры
Вы спросите, зачем понадобилось перехватывать вызов СтандартныеПодсистемыКлиент.ПередНачаломРаботыСистемы()? Отвечаю, чтобы выполнить наши обработчики до установки монопольного режима. В многопоточный механизм БСП не работает.
Также прошу обратить внимания на вызов УстановитьКоличествоПотоков(20). Он нужен для установки константы КоличествоПотоковДлительныхОпераций, которая как верхняя граница количества потоков в механизме длительных операций. Константу можно установить руками через интерфейс Администриование-Общие настройки-Настройка производительности, если вдруг было установлено мало потоков, а запас по ресурсам есть, то можно перед запуском обработчиков установить на ваш выбор.
В итоге что мы имеем:
- Процедуры, составляющие «ядро» многопоточной обработки.
- Пример перевода типового однопоточного обработчика обновления в многопоточный. При этом многопоточная обвязка унифицирована, вынесена в отдельные процедуры «ядра».
- Показано, как выполнить обработчик до выполнения типовых.
Но есть один нюанс… Дело в том, что мы грубо нарушаем последовательность выполнения обработчиков обновления, которые теоретически должны выполняться в строго определенной последовательности. Для себя нашел такой выход:
-
Запускаю обновление базы без многопоточного расширения, т.е. флаг Активно сброшен.
-
Когда я по ЖР вижу, что начал выполняться долгоиграющий обработчик, сбрасываю сеанс, активирую расширение.
-
Запускаю базу, теперь сначала запустятся наши многопоточные обработчики.
P.S. В статье описан только один обработчик, как пример. По аналогии можно в течении 15-30 минут переписать практически любой стандартный обработчик обновления. Для желающих, файл с готовым расширением для ускорения обновления с УХ 3.2.3.58 до 3.2.5.32 во вложении. Перед использованием прочтите абзац выше "Но есть один нюанс".
Если кому-то нужна консультация, как для другой конфигурации использовать эту методику, пишите, постараюсь помочь. Присылайте свои реализации, можно будет сделать библиотеку многопоточных обработчиков обновлений.
Проверено на следующих конфигурациях и релизах:
- 1С:Управление холдингом 1.3, релизы 10.3.73.3