От экспертов «1С-Рарус»: Расследование и оптимизация расчета операции «Закрытие месяца» в 1С:ERP. Часть II
От экспертов «1С-Рарус»: Расследование и оптимизация расчета операции «Закрытие месяца» в 1С:ERP. Часть II

От экспертов «1С-Рарус»: Расследование и оптимизация расчета операции «Закрытие месяца» в 1С:ERP. Часть II

28.12.2021
49 мин
16286

Оглавление

  1. Падение процесса расчета «Закрытия месяца» через сутки после запуска и расчет длительностью в 25 часов
  2. Кейс 1: Оптимизируем «Закрытие месяца» в 1С:ERP для нефтяной компании
    1. Особенности учета на предприятии
    2. Параметры нагрузочного тестирования и выявление ошибки на этапе расчета себестоимости
    3. Характеристики тестового стенда
    4. Ищем причины медленного выполнения этапа «Настройка распределения расходов»
    5. Настраиваем выполнение создание служебных документов «Распределение расходов» в несколько потоков
    6. Результат оптимизации этапа «Настройка распределения расходов»
    7. Ищем ошибку, прерывающую выполнение этапа «Расчет себестоимости»
    8. Устраняем прерывание процесса «Расчета себестоимости» и настраиваем параллелизм
    9. Финальная оптимизация «Расчета себестоимости»
    10. Дополнительная оптимизация времени выполнения «Закрытия месяца» на этапе «Регламентированный учет — отражение документов»
  3. Кейс 2: Ускоряем расчет себестоимости и закрытие месяца в 1С:ERP
    1. Характеристики серверов
    2. Применение подходов к повышению производительности, описанных выше, на другом проекте
    3. Оптимизируем этап «РаспределениеПартийНДСФИФОСкользящая»
    4. Оптимизируем этап «ЗаписатьСформированныеДвижения»
  4. Послесловие

Коротко напомним, что было в первой части статьи «От экспертов 1С‑Рарус».
Выявляем причины долгого «Закрытия месяца» в 1С:ERP и ускоряем выполнение операции. Часть I»:

  • Мы разобрались с режимом отладки. Как его включить и что значит каждый настроечный параметр в этом режиме. Поговорили о том, почему и в каких случаях нужно обходиться с этими параметрами особенно осторожно.
  • Разобрали три практических случая, и то, как мы сумели одержать победу в этих битвах за производительность.
  • А также поделились советами, как можно смотреть на промежуточные результаты при отладке закрытия, когда речь об очень больших временных таблицах.

Прочитать первую часть статьи можно здесь.

Падение процесса расчета «Закрытия месяца» через сутки после запуска и расчет длительностью в 25 часов

В данной части статьи мы продолжим «медитировать» над практическими случаями, происходящими в процессе закрытия месяца.

А именно:

  • Что делать, если на предприятии не могли закрыть месяц. После запуска расчета падал с нехваткой места на диске.
  • На другом же предприятии закрытие завершалось успешно, но расчет шел 25 часов, что никак не было приемлемо для производственного цикла предприятия.

Итак, в путь!

Кейс 1: Оптимизируем «Закрытие месяца» в 1С:ERP для нефтяной компании

Вначале речь пойдет о крупном предприятии, входящем в список крупнейших российских нефтяных компаний. В составе предприятия развиваются нефтегазодобыча, нефтепереработка, нефтегазохимия, шинный комплекс, сеть АЗС, электроэнергетика, разработка и производство оборудования для нефтегазовой отрасли. В составе холдинга присутствует большое число сервисных структур и количество косвенных затрат для распределения не просто велико, а огромно.

Особенности учета на предприятии

Работа производится на 1С: ERP 2.5.5.82, то есть релиз достаточно свежий и много чего в нем оптимизировано. В ходе работ возникла задача присоединения большого количества обособленных подразделений к текущей системе, вследствие чего было принято решение смоделировать ситуацию для оценки влияния этого присоединения к процедуре закрытия месяца.

Внимание Проверка потенциального влияния в виде проведения нагрузочного тестирования на предприятиях такого масштаба должно быть обязательной практикой. И если вам задают вопрос вида «Сколько понадобится прибавить в серверной мощности?», то правильнее всего занимать позицию эксперта, который даст ответ на этот вопрос только после эксперимента.

Особенностью учета данного предприятия является то, что необходимо рассчитать себестоимость по каждому участку и виду деятельности, а себестоимость каждого процесса (вид деятельности, передел) в каждом участке состоит из определенного количества статей расходов. Поэтому аналитика участок * процесс выведена в справочник «Структура предприятия».

Параметры нагрузочного тестирования и выявление ошибки на этапе расчета себестоимости

В модельном примере ввели 1 000 участков и 4 технологических процесса, т. е. 4 000 подразделений (элементов справочника «Структура предприятия») и 30 статей расходов. После чего ввели первичные документы:

  • На каждое подразделение документ «Отражение расходов» для всех статей расходов.
  • Для каждого подразделения документ «Производство без заказа» (для подразделений промежуточных уровней направление списания — на расходы вышестоящего подразделения, для подразделений верхнего уровня направление выпуска — на склад).

Выбор документов обусловлен тем, что в ходе проекта для присоединяемых СП требовалось автоматизировать производственный процесс только в рамках регламентированного учета. Для простоты на первом этапе ограничились этими настройками и не усложняли их косвенными затратами, амортизацией, ОХР и т. д. Далее приступили к процедуре закрытия месяца.

Процесс работал более-менее в штатном режиме, пока не переходил к этапу «Настройка распределения расходов». На этом этапе производится распределение затрат на другие затраты и выпущенную продукцию, в ходе которого создаются служебные документы «Распределение прочих затрат» на некоторое сочетание аналитик Организация, Подразделение, Статья расходов, Аналитика расходов.

В нашем примере мы получали сочетание аналитик порядка 1000 * 4 * 30 = 120 000 документов. Быстрая оценка скорости текущего создания документов (2–3 документа в секунду) показала, что только этот этап закрытия месяца будет длиться более 13 часов.

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

Характеристики тестового стенда

  • СУБД: MS SQL;
  • Платформа 1С:Предприятие: 8.3.17;
  • 1C:ERP: 2.5.5.82.

Сервер приложений:

  • Оперативная память: 160 Гб;
  • Процессор: 3.0 ГГц, 20 ядер.

Ищем причины медленного выполнения этапа «Настройка распределения расходов»

Отметим, что данный этап выполняется до начала этапа «Расчет себестоимости», а значит, нет возможности использовать методы анализа (отчет «Протокол расчета себестоимости», промежуточный протокол), которые мы описывали в первой статье. Поэтому будем анализировать общими способами, используемыми для расследования произвольных проблем производительности.

Предварительно мы уже описали назначение этапа и ход его работы — создание служебных документов «Распределение расходов» по всевозможным сочетаниям неких аналитик. То есть время этапа напрямую зависит от:

  • длительности операции создания и записи одного документа;
  • количества создаваемых документов.

Проанализируем длительность записи одного документа. Самый простой способ сделать это — снять замер производительности операции. Для этого во время выполнения этапа заглянем в журнал регистрации, чтобы убедиться, что система занята именно созданием документов «Распределений расходов». После чего можно установкой отладчика в процедуре модуля документа «ПередЗаписью» начать и завершить замер на следующей итерации создания документа.

Журнал регистрации

Замер показал, на что тратится время (примерно по 0.5–0.6 секунд) создания одного документа. Львиную часть этого времени занимало выполнение запроса — небольшого и несложного, казалось бы.

Журнал регистрации

Для сравнения выполнили этот запрос на сервере с другими настройками MS SQL. Запрос выполнялся моментально, буквально сотые доли секунды. Такая разница была вызвана преимущественно настройкой включенного параллелизма SQL (параметр max degree of parallelism).

Ниже приведена разница в получаемых планах запросов.

Журнал регистрации

Итак, только настройкой maxdop мы значительно ускорили общее время выполнения этапа. Но, конечно, итоговое время операции все равно было слишком велико. При этом не было смысла рассматривать возможности улучшения именно этого запроса, ведь, как ни крути, даже если этот запрос стал бы выполняться 0 секунд, в пересчете на все количество создаваемых документов, время этапа было бы непозволительно большим.

Настраиваем выполнение создание служебных документов «Распределение расходов» в несколько потоков

Поэтому следующим шагом к ускорению этапа стало применение возможности выполнения операций создания документов несколькими фоновыми заданиями одновременно.

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

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

Для реализации вышенаписанного определим участок кода, в котором производится настройка распределения расходов и создание документов. Самый простой способ сделать это — понять, чем занимается система в момент выполнения этой операции. Способов для этого достаточно: заглянуть в журнал регистрации, подключить отладчик и выполнить его остановку, настроить сбор логов технологического журнала, включить трассировку на сервере СУБД… Выбор конкретного способа осуществляется исходя из возможностей — не всегда бывает включена серверная отладка, не всегда журнал регистрации покажет что-то существенное, не всегда есть доступ к серверу 1С (серверу СУБД).

Итак, запускаем процедуру закрытия месяца, дожидаемся начала выполнения нашей «долгой» операции, отслеживая ее начало по журналу регистрации. В нашем случае воспользуемся возможностью получения стека вызовов конфигуратором в момент остановки отладчика, из которого и определяем процедуру, которую будем модифицировать — процедура «СформироватьДокументы» модуля менеджера документа «РаспределениеПрочихЗатрат».

Настройка распределения расходов и создание документов

Алгоритм работает следующим образом:

  • Вначале, анализируя данные регистров «Прочие расходы», «Себестоимость товаров» и «Материалы и работы в производстве», для статей расходов с установленным вариантом распределения расходов (на производственные затраты) определяются движения статей расходов с указанными выше аналитиками (Подразделение, Аналитика расходов, Направления деятельности и др.). Учитываются ранее сформированные документы «Распределения расходов» и в обработку берутся только те, по которым еще не было распределения (есть остаток распределения). Подобранные аналитики образуют массив настроек распределения.
  • Далее производится обход массива циклом с вызовом функции «СформироватьДокумент» для каждого элемента массива.

Модифицируем процедуру следующим образом:

  • Вначале определим количество фоновых заданий, исходя из параметра закрытия «МаксимальноеКоличествоФЗЗаписи» и общего количества настроек (создаваемых документов). Привязка именно к этому параметру несет исключительно рекомендуемый характер. Можно привязываться к другому параметру, подходящему вам по смыслу, либо добавлять свой собственный.
  • Затем для каждого номера задания соберем те настройки, которые должно обрабатывать именно это задание и передадим их в виде параметров фонового задания. В теле процедуры самого фонового задания будет все та же типовая функция «СформироватьДокумент».

Ниже приведена возможная реализация распараллеливания операции создания документов «Распределение прочих затрат»:

Создание документа «Распределение прочих затрат»

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

  
    Функция МногопоточнаяОбработкаЗаданийРасчетаСС(Параметры)
    
    МассивРезультатов = Новый Массив;
    
    ПроцедураОбработки     = Параметры.Процедура;
    ПараметрыДанных     = Параметры.Данные;
    ПараметрыРасчета     = Параметры.ПараметрыРасчета;
    
    Потоки = Новый Массив;
    
    КоличествоПотоков          = ПараметрыРасчета.МаксимальноеКоличествоФЗЗаписи;
    КоличествоОбъектовДляОбработки     = ПараметрыДанных.ТаблицаДанных.Количество();
    ОсталосьОбработать           = КоличествоОбъектовДляОбработки;
    КоличествоПотоковКЗапуску      = Мин(КоличествоОбъектовДляОбработки, КоличествоПотоков);

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

Функция РазделитьТаблицуНаПорции(ТаблицаДанных, РазмерПорции, КоличествоТаблиц = Неопределено)

    МассивПорций = Новый Массив;
    
    КоличествоДанных = ТаблицаДанных.Количество();
    КоличествоТаблиц = ?(КоличествоТаблиц = Неопределено, Цел(КоличествоДанных / РазмерПорции), КоличествоТаблиц);
    
    ПоследнийЭлемент = -1;
    
    Для НомерТаблицы = 1 По КоличествоТаблиц Цикл
     
     ПервыйЭлемент = ПоследнийЭлемент + 1;
     // определим последний обрабатываемый элемент потока - в зависимости от размера порции,
     // либо последний элемент массива данных (если это последний поток)
     Если НомерТаблицы = КоличествоТаблиц Тогда
       ПоследнийЭлемент = КоличествоДанных - 1;
     Иначе
       ПоследнийЭлемент = НомерТаблицы*РазмерПорции - 1;    
     КонецЕсли;
     
     ДанныеПорции = ДанныеДляОбработки(ТаблицаДанных, ПервыйЭлемент, ПоследнийЭлемент);
     
     МассивПорций.Добавить(ДанныеПорции);
     
    КонецЦикла;    
    
    Возврат МассивПорций;

КонецФункции

Функция ДанныеДляОбработки(ТаблицаДанных, ПервыйЭлемент, ПоследнийЭлемент)
    
    ВозвращаемыеДанные = ТаблицаДанных.СкопироватьКолонки();
    
    Для Сч = ПервыйЭлемент ПО ПоследнийЭлемент Цикл
     НоваяСтр = ВозвращаемыеДанные.Добавить();
     ЗаполнитьЗначенияСвойств(НоваяСтр, ТаблицаДанных[Сч])
    КонецЦикла;
       
    Возврат ВозвращаемыеДанные;
    
КонецФункции

#Область РаботаСПотоками

Функция НовоеОписаниеПотока(ИмяМетода)
    
    Описание = Новый Структура;
    Описание.Вставить("ИдентификаторЗадания", Неопределено);
    Описание.Вставить("Процедура", ИмяМетода);
    Описание.Вставить("АдресРезультата", "");
    Описание.Вставить("НаименованиеЗадания", "");
    Описание.Вставить("ПараметрыПроцедуры", Новый Структура);
    Возврат Описание;
    
КонецФункции

Процедура ЗапуститьОбработкуВФоне(Поток)
    
    ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВФоне(Неопределено);
    ПараметрыВыполнения.НаименованиеФоновогоЗадания = Поток.НаименованиеЗадания;
    ПараметрыВыполнения.ОжидатьЗавершение = 0;
    ПараметрыВыполнения.АдресРезультата = ПоместитьВоВременноеХранилище(Неопределено, Новый УникальныйИдентификатор);
    
    РезультатЗапуска = ДлительныеОперации.ВыполнитьВФоне(Поток.Процедура, Поток.ПараметрыПроцедуры, ПараметрыВыполнения);
    
    Поток.АдресРезультата = РезультатЗапуска.АдресРезультата;
    Статус = РезультатЗапуска.Статус;
    
    Если Статус = "Выполняется" Тогда
     Поток.ИдентификаторЗадания = РезультатЗапуска.ИдентификаторЗадания;
    ИначеЕсли Статус <> "Выполняется" И Статус <> "Выполнено" Тогда
     ВызватьИсключение РезультатЗапуска.КраткоеПредставлениеОшибки;
    КонецЕсли;
    
КонецПроцедуры

Процедура ОжидатьЗавершениеВсехПотоков(Потоки, МассивРезультатов)
    Пока Потоки.Количество() > 0 Цикл
     Если НЕ ЗавершитьПотокиВыполнившиеФЗ(Потоки, МассивРезультатов) Тогда
       ОжидатьЗавершениеПотока(Потоки[0]);
     КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

Функция ЗавершитьПотокиВыполнившиеФЗ(Потоки, МассивРезультатов)
    
    ЕстьЗавершенныеПотоки = Ложь;
    
    Индекс = Потоки.Количество() - 1;
    Пока Индекс >= 0 Цикл
     Поток = Потоки[Индекс];
     ИдентификаторЗадания = Поток.ИдентификаторЗадания;
     
     Если ИдентификаторЗадания <> Неопределено Тогда
       ЗаданиеВыполнено = ДлительныеОперации.ЗаданиеВыполнено(ИдентификаторЗадания);
     КонецЕсли;
     
     Если ИдентификаторЗадания = Неопределено ИЛИ ЗаданиеВыполнено Тогда
       Если ЗначениеЗаполнено(Поток.АдресРезультата) Тогда
         Результат = ПолучитьИзВременногоХранилища(Поток.АдресРезультата);
         Если Результат <> Неопределено Тогда
           МассивРезультатов.Добавить(Результат);
         КонецЕсли;
         УдалитьИзВременногоХранилища(Поток.АдресРезультата);
       КонецЕсли;
       Потоки.Удалить(Индекс);
       ЕстьЗавершенныеПотоки = Истина;
     КонецЕсли;
     
     Индекс = Индекс - 1;
    КонецЦикла;
    
    Возврат ЕстьЗавершенныеПотоки;
    
КонецФункции

Функция ОжидатьЗавершениеПотока(Поток, Длительность = 1)
    
    Если Поток <> Неопределено И Поток.ИдентификаторЗадания <> Неопределено Тогда
     Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Поток.ИдентификаторЗадания);
     
     Если Задание <> Неопределено Тогда
       Попытка
         Задание.ОжидатьЗавершенияВыполнения(Длительность);
         Возврат Истина;
       Исключение
         Возврат Ложь;
       КонецПопытки;
     КонецЕсли;
    КонецЕсли;
    
    Возврат Истина;
    
КонецФункции

#КонецОбласти

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

Механизм работы с потоками, перемещение результатов во временное хранилище

Количество фоновых заданий, создаваемых в процедуре, устанавливается из значения параметра операции закрытия месяца «Максимальное количество одновременно выполняемых заданий записи». В нашем случае это 10 потоков.

Результат оптимизации этапа «Настройка распределения расходов»

Общее время выполнения этапа после включения maxdop и параллельной записи документов — 1–1.5 часов.

Более того, сами документы можно создавать предварительно, не в процедуре закрытия месяца, а например отдельным регламентным заданием, выполняемым в течении месяца, что также позволяет выиграть время.

Ищем ошибку, прерывающую выполнение этапа «Расчет себестоимости»

Следующим долгим, и пожалуй самым важным этапом операции закрытия, стал этап расчета себестоимости. Напомним, что изначально этот этап вообще не завершался, после долгого времени ожидания процесс падал с ошибкой нехватки места на диске, где располагалась база tempDB.

Событие

Углубимся немного в теорию: одно из назначений служебной базы tempDB — хранение временных таблиц, создаваемых в запросах. Не каждая временная таблица будет создаваться в tempDB, в рядовых случаях они создаются в оперативной памяти сервера. Но в некоторых случаях, например, если получаемая таблица слишком велика, временная таблица может создаться в tempDB.

Ошибка, которая привела к прерыванию процесса расчета, наталкивает на мысль о том, что в данном этапе выполнялось большое количество тяжелых запросов с использованием большого количества временных таблиц. К тому же ситуацию может осложнить индексирование этих временных таблиц, ведь индексы тоже будут создаваться в tempDB, а также использование оператора SORT, что мы и видим в описании ошибки.

Теперь нам необходимо понять на каком шаге расчета себестоимости происходит эта ошибка и каким участком кода она вызвана.

Для того, чтобы узнать на каком шаге произошла ошибка, можно обратиться к журналу регистрации. В нем фиксируются все ключевые вехи этапов — начало, характеристики, инициализации и т. д. Найдем ближайшее событие расчета, возникшее перед ошибкой. В нем и содержится имя шага этапа.

Журнал регистрации

В нашем случае ошибка возникла при прохождении этапа «РаспределитьДолиПроизводственныхРасходов».

Итак, мы знаем на каком шаге этапа есть проблема, найдем теперь процедуру, обрабатывающую данный шаг. Получить ее можно разными способами. Один из них — осуществить глобальный поиск по конфигурации по ключевому имени этапа — «РаспределитьДолиПроизводственныхРасходов». Так мы выйдем на процедуру

«РасчетСебестоимостиПостатейныеЗатраты.РаспределитьДолиПроизводственныхРасходов».

Теперь запустим расчет снова и в режиме отладки попробуем понять почему на нее уходит столько времени и в итоге процесс падает с ошибкой. Для вспомогательного анализа будем следить за текущей активностью MS SQL, также настроим сбор счетчиков монитора производительности MS SQL — Perfomance Monitor.

Алгоритм данного этапа программно формирует множество текстов запросов для дальнейшего пакетного выполнения в объекте конфигурации «СхемаЗапроса». При этом внутри этой схемы может быть очень большое число пакетов запросов, например, в нашем случае их около 8 000. Поэтому ее выполнение может быть очень длительным по времени в зависимости от ряда причин. Одна из них — неоптимальный код. В релизе 2.5.5.82 мы заметили, что происходит циклическое и избыточное заполнение служебных описаний групп при составлении отборов запроса:

Результат запроса

Внутри цикла, выполняемого более 173 000 раз, выполняется функция «ОпределитьГруппу», внутри которой также происходит циклический обход таблицы значений более 6 000 раз. Далее по полученным описаниям групп составляется текст мегазапроса на выполнение. Результаты запросов схемы помещаются во временные таблицы для дальнейшей обработки, следующие элементы схемы используют результаты предыдущих элементов, в результате чего на одном из запросов выделенное место для tempDB заканчивается и процесс прерывается.

Наблюдая за счетчиком «Free Space in tempdb (KB)» («Свободное пространство в tempdb»), можно увидеть монотонное уменьшение показателя счетчика на протяжении нашей операции:

Счетчик «Free Space in tempdb (KB)»

Интенсивность процесса записи в базу можно увидеть с помощью счетчика «Page writes/sec» («Операций записей страниц в секунду»):

Счетчик «Page writes/sec»

А выполняемые при этом планы показывали, что идет активная работа с временными таблицами и операциями над ними.

Результат запроса

Теперь взглянем на счетчик «Page Life Expectancy» («Примерный срок хранения страницы»). Данный счетчик показывает сколько в среднем секунд страница хранится в буфере.

Счетчик «Page Life Expectancy»

Видим, что наблюдаются просадки счетчика. Это говорит о том, что в эти моменты выполнялись запросы, читающие большое количество страниц с диска, которые замещали ранее прочитанные страницы в буфере. И скорее всего, запросы выполняли сканирование без использования поиска по индексу, так как местами значение счетчика близко к нулю.

Устраняем прерывание процесса «Расчета себестоимости» и настраиваем параллелизм

В нашем случае исправление этой ошибки было выполнено силами разработчиков 1С:ERP. Исправления значительно уменьшили объем читаемых и обрабатываемых данных, вследствие чего более не было нагрузки на tempDB.

Теперь рассмотрим более подробно влияние настроек СУБД на нашу операцию.

В предыдущей части статьи мы отметили, что процессам «Закрытия месяца» может помочь включение настройки параллелизма на сервере SQL. Мы проверили это влияние, установили следующие настройки MS SQL:

  • max degree of parallelism = 4;
  • cost threshold for parallelism = 100;
  • legacy cardinality estimation = on.

Данные настройки были подобраны для нашего сервера индивидуально. Отметим, что нет универсальной «хорошей» настройки параллелизма, первичное назначение характеристик этих опций может осуществляться исходя из физических параметров сервера СУБД (количества ядер процессоров, поддержка Hyper-Threading и др.), а далее их следует подбирать, исходя из анализа и наблюдениями за ростом ожиданий, нагрузкой CPU и, конечно же, общей производительности работы пользователей.

Внимание Примечание:
В общем случае могут быть различные типы ожиданий, например такие как CXPACKET (ожидание завершения отстающих потоков главным потоком) , ожидания ввода-вывода (PageIOLatch_XX) и даже взаимоблокировки. Разговор об этом выходит за рамки данной статьи. Один из случаев страничных блокировок разобран в одной из наших предыдущих статей «Страничные блокировки в MS SQL Server при проведении документов».

Но заметим, что можно рассмотреть вариант установки данных параметров только на время процедуры закрытия месяца, если включение параллелизма для штатных операций вызывает замедления. В любом случае, не стоит пренебрегать данными возможностями SQL, когда они есть, зачастую они несут положительный эффект для различных операций.

После устранения ошибки неоптимального кода и установки настроек MS SQL этап наконец выполнился и время его работы составило 5.5 часа. Хотя время значительно улучшилось, оно не достигло целевых значений. Поэтому продолжаем расследование дальше.

Финальная оптимизация «Расчета себестоимости»

Для определения следующего узкого места обратимся к протоколу расчета. Это специального вида отчет, позволяющий увидеть ключевые характеристики прохождения конкретного расчета себестоимости. Подробно о том как его получить и читать мы рассматривали в первой части публикации «Выявляем причины долгого «Закрытия месяца» в 1С:ERP и ускоряем выполнение операции. Часть I».

Нас интересует топ самых длительных операций этапа «Расчет себестоимости».

Этапа «Расчет себестоимости»

Видим, что этапы №90 и №91 суммарно занимают более 4 часов, т. е. значительное время всей операции расчета себестоимости, а значит, именно ими следует заняться.

Для начала рассмотрим этап №90 — ЗаписатьСформированныеДвижения. Здесь записываются наборы записей регистров по ранее полученным таблицам движений. Типовой код записи предусматривает возможность параллельной записи несколькими фоновыми заданиями. Проанализируем процедуру записи движений «НачалоЗаписиДвижений» общего модуля «РасчетСебестоимостиПрикладныеАлгоритмы».

Процедура записи движений «НачалоЗаписиДвижений» общего модуля «РасчетСебестоимостиПрикладныеАлгоритмы»

Общее количество движений документов делится на порции с учетом определенных условий (например, движения одного документа попадают строго в одну порцию). Далее, в проходе по количеству порций, отбираются движения только текущей порции и запускается фоновое задание записи движений:

Результат запроса

Видим, что алгоритм производит разбиение на порции в зависимости от настроек блока «Управление многопоточностью» операции «Распределение затрат и расчет себестоимости». В зависимости от оборудования сервера, а также его конфигурации, эти параметры можно редактировать.

В нашем случае мы установили следующие значения параметров:

Параметры операций закрытия месяца

Теперь рассмотрим этап №91 — ЗарегистрироватьКОтражениюВРегламентированномУчете.

Найдем процедуру, содержащую алгоритм этапа способом, описанным выше — это процедура «ВернутьДокументыКОтражению» общего модуля «РеглУчетПроведениеСервер». Анализируя алгоритм, видим, что в процедуре определяются документы к отражению (с изменившимися движениями), затем происходит запись документов в служебный регистр сведений «ОтражениеДокументовВРеглУчете». В нашем случае документов к отражению — более 100 000.

Будем записывать этот регистр параллельно. Регистр подчинен регистратору, документы-регистраторы сгруппированы, а значит пересечений в измерениях не будет наблюдаться. Разбиение на порции осуществим аналогично приведенному выше примеру (как это сделано в типовой процедуре «НачалоЗаписиДвижений» общего модуля «РасчетСебестоимостиПрикладныеАлгоритмы»:

Результат запроса

Определив максимальный номер порции (количество фоновых заданий), запускаем на выполнение задание со своим набором данных для обработки (код примера прохода цикла показан выше).

Для определения количества заданий в нашем случае был добавлен отдельный параметр закрытия — «КоличествоДвиженийВФЗЗаписиЭтапаОтраженияВРегУчете», но можно привязаться и к существующим, если они подходят под конкретные условия.

В результате ускорения этих этапов время выполнения расчета себестоимости сократилось до 3.5 часов, что полностью устроило заказчика.

Протокол расчета

Итак, мы рассмотрели способы расследования проблемы долгого расчета себестоимости как с помощью анализа протокола расчета (итогового, промежуточного), так и стандартными способами расследования проблем производительности, в случае, когда данных протокола недостаточно, либо их вообще нет.

Дополнительная оптимизация времени выполнения «Закрытия месяца» на этапе «Регламентированный учет — отражение документов»

Несмотря на то, что время расчета себестоимости достигло целевого результата, общее время закрытия все равно не устраивало Заказчика, а именно длительность одного из финальных этапов — отражения документов в регламентированном учете. Его время составляло в среднем 2–3 часа. Необходимо было ускорить этот этап хотя бы в два раза.

Сама процедура весьма проста — определяется набор документов, для которых необходимо произвести запись движений в различные регистры, и, собственно, производится запись. В самом алгоритме улучшить нечего. Долгая длительность этапа связана с большим количеством обрабатываемых документов. Поэтому попробуем решить задачу все тем же способом — разбиения документов на порции и обработкой этих порций отдельными фоновыми заданиями.

Итак, обратимся к процедуре «ОтразитьВсе» общего модуля «РеглУчетПроведениеСервер». Для каждого типа документов, для которого будет производиться запись в регистры рег. учета, вызывается процедура «ОтразитьДокументыПакетно». Доработаем ее для возможности разбиения на отдельные фоновые задания.

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

Результат запроса

Логику работы перенесем в процедуру «СформироватьТаблицыОтбораДанныхФЗ» — аналог типовой процедуры «СформироватьТаблицыОтбораДанных» с разбиением данных для выполнения несколькими потоками.

Само разбиение снова выполним, назначая строкам временной таблицы данных разделитель (номер порции):

Результат запроса

Модифицированный алгоритм показал отличные результаты — время этапа сократилось до 30–40 минут.

Итак, общее время закрытия (всех этапов) составило 4 с небольшим часа. По сравнению с начальным временем закрытия (напомним, 24 часа до момента падения процесса) — шестикратная разница.

Кейс 2: Ускоряем расчет себестоимости и закрытие месяца в 1С:ERP

Далее приведем пример оптимизации закрытия месяца на другом предприятии, которое только начинало свою работу в 1С:ERP, перейдя на него с другой системы учета. Особенностью являлось то, что проблемным оказалось первое закрытие в истории компании на ERP – только этап расчета себестоимости длился 29 часов.

Характеристики серверов

  • СУБД: MS SQL;
  • Платформа 1С:Предприятие: 8.3.17;
  • 1C:ERP: 2.5.5.52.

Сервер приложений (3 рабочих сервера, 2 центральных):

  • Оперативная память: 288 Гб;
  • Процессор: 3.0 ГГц, 44 ядра.

Применение подходов к повышению производительности, описанных выше, на другом проекте

Ранее в статье мы рассмотрели оптимизацию нескольких этапов закрытия месяца. Применение подобных подходов возможно и на других медленных этапах, если их логика позволяет.

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

Протокол расчета

Оптимизируем этап «РаспределениеПартийНДСФИФОСкользящая»

Видим, что в топе операций — этап «РаспределениеПартийНДСФИФОСкользящая». Простой глобальный поиск по конфигурации по имени этапа приводит нас к месту действия события — процедура «РасчетСебестоимостиНДС.ИнициализироватьТаблицыДляДокументовВводаОстатков».

Результат запроса

Ее анализ показывает, что запросом выбираются документы Ввода начальных остатков за период расчета, затем последовательно в цикле происходит обработка — выполнение нового запроса по конкретному документу с последующим помещением данных во временную таблицу. Конечно, если в месяце закрытия нет документов Ввода остатков, то и не будет возникать этот этап.

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

И, так как после получения результатов закрытия постоянно вносились правки в документы, процедура проводилась снова и снова, поэтому не было никакой возможности смириться с длительностью этапа.

Снова понимаем, что для решения проблемы здесь отлично поможет метод разбиения множества документов на порции с последующей обработкой их разными сеансами. Реализация алгоритма схожа с реализацией из п. «Настройка распределения расходов» и использует тот же механизм работы с потоками. Поэтому приведем только кусочек кода доработанной процедуры «ИнициализироватьТаблицыДляДокументовВводаОстатков»:

Результат запроса

Внутри процедуры, выполняемой каждым фоновым заданием, находится типовой код, который раньше выполнялся в цикле основной процедуры. Ранее, как было отмечено выше, на каждый документ собиралась временная таблица с данными, наша процедура делает то же самое. Но необходимо передать результаты этой процедуры в родительский сеанс — сделать это можно через временное хранилище (либо через запись в базу данных, если первый вариант по каким-то причинам не подходит). Отметим, что в данном случае возникает необходимость конвертации данных временной таблицы в иной формат (например, таблицу значений), так как менеджер временных таблиц не сериализуется.

Получив результаты всех дочерних сеансов, производится их объединение в единую временную таблицу. В зависимости от количества запускаемых потоков, можно добиться улучшения времени этапа в несколько раз, что и было сделано.

Оптимизируем этап «ЗаписатьСформированныеДвижения»

Второе место в топе операций занимает этап №89 — «ЗаписатьСформированныеДвижения», его оптимизация рассматривалась выше (в релизе 2.5.5 он шел под номером 90).

Улучшение производительности двух этапов расчета себестоимости позволило закрыть месяц за 5.5–6 часов, что значительно лучше исходных показателей.

Послесловие

Давайте подводить итоги нашей дилогии, которая дальше может вырасти и в трилогии и даже в некую сагу.

Во-первых, мы надеемся, что сумели показать, что расследования в области оптимизации закрытия месяца в 1С:ERP выходят за рамки штатной экспертизы по производительности, так как требуют от вас понимания архитектуры решения, математических принципов функционирования, методологии отладки закрытия.

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

В-третьих, для случаев, когда протокол расчета получен, надо научаться читать этот протокол. И хорошо ориентироваться в опциях настройки отладки закрытия. Потому что часть из них нельзя включать без нужды из-за риска ещё большего замедления процесса «Закрытия месяца».

Четвертый вывод состоит в том, что если закрытия не удалось дождаться, то не следует опускать руки. В этом случае в полной мере понадобятся ваши навыки эксперта по производительности.

Пятое — в ряде случаев мы способны оптимизировать код самостоятельно, а в некоторых разумнее открыть проект ЦКПТ и взаимодействовать с коллегами из 1С. Мы показали это в первом кейсе второй части статьи.

Надеемся, что данный цикл публикаций про оптимизацию производительности закрытия в ЕРП будет полезным для вас и до встречи в новом 2022 году.

Вы читаете статью из рубрики:
От экспертов «1С-Рарус»

Есть вопросы по статье? Задайте их нам!

Рассылка «Новости компании»: узнавайте о новых продуктах, услугах и спецпредложениях

Посмотреть все рассылки «1С‑Рарус»

Поле не должно быть пустым
Электронная почта указывается только латиницей, обязательно должен присутствовать знак @, доменное имя не может быть короче двух символов

Посмотреть все рассылки «1С-Рарус»

Иконка «Предупреждение» Отправляя эту форму, Вы соглашаетесь с Политикой конфидециальности и даете согласие на обработку персональных данных компанией «1С-Рарус»

Заинтересованы в сотрудничестве?
Нужна консультация?
Свяжитесь с нами!