При свертке информационных баз или регламентной очистке исторических данных разработчики часто сталкиваются с ситуацией, когда специализированная обработка аварийно завершает работу с системной ошибкой «Недостаточно памяти». Особенно часто эта проблема проявляется на конфигурациях предыдущих поколений, таких как «1С:Управление торговлей 10.3», работающих в режиме обычного приложения под управлением платформы 1С:Предприятие 8.3.
Проанализируем механику возникновения этой ошибки, разберем причины переполнения оперативной памяти и рассмотрим эффективные программные и инфраструктурные способы обхода проблемы.
Выясним причину, по которой непрерывный цикл по документам приводит к аварийному падению сеанса. Проблема складывается из нескольких факторов платформы:
ДокументОбъект, выборки, движения и кэши контекстов удерживаются в области видимости процедуры и не освобождаются оперативно.Разберем базовые системные меры, которые позволяют снизить остроту проблемы без изменения бизнес-логики конфигурации:
УстановитьИспользованиеИтогов(Ложь) у соответствующих менеджеров регистров, а после завершения очистки пересчитать итоги заново.Когда требуется просто пометить объекты на удаление или отменить их проведение в рамках свертки, выполнение тяжелых алгоритмов проведения (контроль остатков, перезапись связанных регистров, проверки заполнения) является избыточным. Для отключения проверок используем специальное свойство объекта ОбменДанными.Загрузка.
Посмотрим на пример правильной пометки на удаление:
ДокОбъект = ВыбраннаяСсылка.ПолучитьОбъект();
Если ДокОбъект <> Неопределено Тогда
ДокОбъект.ОбменДанными.Загрузка = Истина;
ДокОбъект.УстановитьПометкуУдаления(Истина);
КонецЕсли;
Флаг ОбменДанными.Загрузка = Истина сообщает платформе и конфигурации, что данные загружаются в служебном режиме. В этом случае платформенные обработчики ПередЗаписью, ПриЗаписи и подписки на события либо пропускаются полностью, либо выполняют минимальные проверки, не создавая утечек памяти.
Отмена проведения через метод Записать(РежимЗаписиДокумента.ОтменаПроведения) требует считывания в память всего документа целиком и вызова его модуля. Если требуется очистить движения, гораздо эффективнее перезаписывать наборы записей регистров пустыми значениями с отбором по регистратору.
Рассмотрим пример прямой очистки движений документа по регистру накопления:
Набор = РегистрыНакопления.Продажи.СоздатьНаборЗаписей();
Набор.Отбор.Регистратор.Установить(СсылкаНаДокумент);
Набор.ОбменДанными.Загрузка = Истина;
// Записываем пустой набор записей, удаляя старые движения
Набор.Записать();
При таком подходе сам документ не считывается в оперативную память, что радикально уменьшает расход ресурсов процесса.
Чтобы внутренний сборщик мусора платформы мог своевременно освобождать память, необходимо избегать выполнения всей логики внутри одной большой процедуры. Вынесем обработку единичного объекта или небольшой пачки в отдельную процедуру и ограничим объем явных транзакций.
Разберем по шагам реализацию порционной обработки:
Процедура ОбработатьПачкуДокументов(МассивСсылок)
НачатьТранзакцию();
Попытка
Для Каждого Ссылка Из МассивСсылок Цикл
ОбработатьОдинДокумент(Ссылка);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Процедура ОбработатьОдинДокумент(Ссылка)
ДокОбъект = Ссылка.ПолучитьОбъект();
Если ДокОбъект <> Неопределено Тогда
ДокОбъект.ОбменДанными.Загрузка = Истина;
ДокОбъект.УстановитьПометкуУдаления(Истина);
// Принудительно обнуляем ссылку на объект перед выходом из процедуры
ДокОбъект = Неопределено;
КонецЕсли;
КонецПроцедуры
Поясним особенности этого механизма: при выходе из процедуры ОбработатьОдинДокумент переменная ДокОбъект гарантированно покидает контекст исполнения. Фиксация транзакции пачками по 100–500 документов сбрасывает накопленный журнал транзакций СУБД и очищает внутренние буферы платформы.
Если выборка документов содержит десятки тысяч записей, выгрузка запроса целиком через метод Выгрузить() приведет к созданию громоздкой таблицы значений в памяти. Вместо этого используем порционную выборку с ключевым словом ПЕРВЫЕ.
Посмотрим на пример построения порционного запроса:
ПоследняяСсылка = Документы.РеализацияТоваровУслуг.ПустаяСсылка();
Пока Истина Цикл
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 500
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Док
|ГДЕ
| Док.Дата < &ДатаСвертки
| И Док.Ссылка > &ПоследняяСсылка
|УПОРЯДОЧИТЬ ПО
| Док.Ссылка";
Запрос.УстановитьПараметр("ДатаСвертки", ГраницаОчистки);
Запрос.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Количество() = 0 Тогда
Прервать;
КонецЕсли;
МассивДляОбработки = Новый Массив;
Пока Выборка.Следующий() Цикл
МассивДляОбработки.Добавить(Выборка.Ссылка);
ПоследняяСсылка = Выборка.Ссылка;
КонецЦикла;
ОбработатьПачкуДокументов(МассивДляОбработки);
КонецЦикла;
Такой цикл не накапливает результаты запроса в памяти и позволяет обработать любой объем документов без риска падения программы по переполнению ОЗУ.
Если конфигурация сильно модифицирована и перегружена сложными внутренними взаимосвязями, физическое удаление или каскадная отмена проведения миллионов старых записей часто оказываются нецелесообразными. В подобных ситуациях более надежным решением является классическая свертка базы:
Данный подход исключает необходимость массового удаления миллионов записей из СУБД, устраняет дефрагментацию таблиц и обеспечивает максимальную производительность обновленной системы.