Каждый разработчик 1С рано или поздно сталкивается с задачей массового изменения данных: обновлением реквизитов контрагентов, перезаписью контактной информации или актуализацией цен. Сценарий часто выглядит одинаково: пишется простая обработка, которая выбирает 100 000+ объектов, в цикле модифицирует их и записывает. Однако через 10–15 минут работы процесс прерывается аварийным сообщением: «Неисправимая ошибка. Приложение будет закрыто», а в технологическом журнале фиксируется лишь лаконичное Outgoing connection closed.
В этой статье мы подробно разберем, почему возникает эта ошибка, как архитектура сервера приложений влияет на стабильность выполнения кода, и какие практические приемы программирования помогут обработать любые объемы данных без риска уронить рабочий процесс сервера.
Проанализируем механику возникновения сбоя. Когда рабочие процессы сервера приложений 1С работают в 32-разрядном режиме, они подчиняются жестким архитектурным ограничениям операционной системы. Максимальный объем виртуального адресного пространства, доступный одному 32-битному процессу rphost.exe, составляет 2 ГБ (в редких случаях, при использовании специальных ключей операционной системы — до 3 ГБ).
Когда в процессе выполнения серверного вызова объем выделенной памяти приближается к этому критическому порогу, операционная система отказывает процессу в очередном выделении блока памяти из кучи (heap). Происходит критическая ошибка нехватки памяти (Out of Memory). Платформа 1С не может обработать это исключение в штатном режиме, из-за чего рабочий процесс rphost.exe мгновенно прекращает свою работу («падает»). Сетевое соединение между клиентом и сервером рвется, что регистрируется в технологическом журнале как закрытие исходящего соединения, а пользователь видит окно с сообщением о неисправимой ошибке.
Переход на 64-разрядную версию сервера 1С полностью решает проблему жесткого лимита в 3 ГБ. В 64-битной архитектуре адресное пространство процесса практически неограниченно, и rphost.exe может задействовать всю оперативную память, доступную на физическом сервере.
Однако рассмотрим ситуацию глубже. Если алгоритм обработки написан неоптимально и допускает утечку памяти, то на 64-битном сервере процесс не упадет на 3 ГБ. Он продолжит расти, потребляя 8, 16 или даже 32 ГБ памяти. Это приведет к следующим негативным последствиям:
Таким образом, использование 64-разрядного сервера обязательно для современных систем, но оно не отменяет необходимости оптимизации кода.
Выясним, из-за чего растет потребление оперативной памяти сервером приложений во время длительного цикла. Выделим основные факторы:
ПолучитьОбъект() платформа считывает данные из базы и кэширует структуру объекта в памяти текущего сеанса. При обработке огромного массива данных этот кэш разрастается до гигантских размеров.Разберем детально, как правильно переписать алгоритм обработки данных, чтобы он работал быстро и не приводил к перерасходу ресурсов сервера.
Никогда не выбирайте все 100 000 объектов в один массив или выборку запроса. Данные необходимо обрабатывать небольшими порциями (пакетами) по 500–1000 штук. Для этого в запросе используется конструкция ВЫБРАТЬ ПЕРВЫЕ.
Посмотрим на пример формирования запроса для получения очередной порции необработанных объектов:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Контрагенты.Ссылка КАК Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| НЕ Контрагенты.ПометкаУдаления
| И Контрагенты.ДополнительныйРеквизит = &ПустоеЗначение
|УПОРЯДОЧИТЬ ПО
| Ссылка";
Запрос.УстановитьПараметр("ПустоеЗначение", "");
Для того чтобы сборщик мусора платформы 1С мог своевременно и в полном объеме очищать оперативную память, каждую порцию данных нужно обрабатывать в рамках отдельного серверного вызова. Организуем цикл на стороне клиента, который будет последовательно вызывать серверный метод.
Рассмотрим структуру клиент-серверного взаимодействия:
&НаКлиенте
Процедура ЗапуститьОбработкуДанных()
КоличествоОбработанных = 0;
ПараметрыЦикла = Новый Структура("ШагСекунд, МаксимумПорции", 5, 1000);
Пока Истина Цикл
// Каждый вызов этой функции — отдельный контекстный или бесконтекстный вызов сервера
ОбработаноЗаИтерацию = ОбработатьПорциюНаСервере(ПараметрыЦикла.МаксимумПорции);
Если ОбработаноЗаИтерацию = 0 Тогда
Прервать; // Все объекты успешно обработаны
КонецЕсли;
КоличествоОбработанных = КоличествоОбработанных + ОбработаноЗаИтерацию;
Состояние("Обработано объектов: " + КоличествоОбработанных);
КонецЦикла;
ПоказатьПредупреждение(, "Обработка успешно завершена!");
КонецПроцедуры
Теперь спроектируем серверную часть, которая будет выполнять непосредственную модификацию объектов. Обратите внимание на явное использование транзакций для каждой порции:
&НаСервереБезКонтекста
Функция ОбработатьПорциюНаСервере(Знач РазмерПорции)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ " + Формат(РазмерПорции, "ЧГ=0") + "
| Контрагенты.Ссылка КАК Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.ДополнительныйРеквизит = &ПустоеЗначение
|УПОРЯДОЧИТЬ ПО
| Ссылка";
Запрос.УстановитьПараметр("ПустоеЗначение", "");
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Возврат 0;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Обработано = 0;
// Оборачиваем обработку одной порции в транзакцию
НачатьТранзакцию();
Попытка
Пока Выборка.Следующий() Цикл
ОбъектКонтрагент = Выборка.Ссылка.ПолучитьОбъект();
// Отключаем обработчики бизнес-логики БСП для ускорения и экономии памяти
ОбъектКонтрагент.ОбменДанными.Загрузка = Истина;
// Модифицируем данные
ОбъектКонтрагент.ДополнительныйРеквизит = "Обработано";
ОбъектКонтрагент.Записать();
Обработано = Обработано + 1;
// Явно освобождаем ссылку на тяжелый объект
ОбъектКонтрагент = Неопределено;
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменятьТранзакцию = Истина;
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("МассоваяОбработка", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение "Ошибка при записи объекта в транзакции: " + ОписаниеОшибки();
КонецПопытки;
Возврат Обработано;
КонецФункции
Использование свойства ОбменДанными.Загрузка = Истина перед вызовом метода Записать() является критически важным при массовых операциях. Это свойство сообщает платформе и механизмам конфигурации, что объект записывается в режиме обмена (интеграции).
При этом автоматически отключаются:
Это не только снижает нагрузку на оперативную память rphost, но и ускоряет запись каждого объекта в 5–10 раз.
Внутри итератора цикла обязательно обнуляйте переменные, содержащие тяжелые ссылочные объекты платформы. Присвоение значения Неопределено переменной ОбъектКонтрагент сразу после выполнения метода Записать() явно указывает платформе, что объект больше не используется в коде, позволяя сборщику мусора своевременно очистить оперативную память, занятую этим объектом в куче процесса.
Если цель вашей обработки заключается в обновлении контактной информации контрагентов (адресов, телефонов, email), получение тяжелого объекта СправочникОбъект через метод ПолучитьОбъект() является избыточным действием.
В современных конфигурациях вся контактная информация хранится во в табличных частях или в специализированных регистрах сведений (например, в регистре СоставТочекКонтакта). Рассмотрите возможность записи данных напрямую в соответствующий регистр сведений через создание набора записей РегистрСведенийНаборЗаписей. Работа с наборами записей регистров напрямую минует логику справочников и потребляет в десятки раз меньше оперативной памяти.
Для предотвращения аварийных ситуаций на сервере приложений при запуске масштабных обработок администраторам рекомендуется выполнить следующие настройки в консоли администрирования кластера серверов 1С:
rphost), а старый процесс завершит текущие вызовы и безопасно перезапустится, освободив всю занятую память.Используя приведенные архитектурные подходы и правила написания кода, мы сможем гарантировать стабильную работу сервера 1С без утечек памяти и досадных аварийных завершений системы даже при обработке многомиллионных массивов данных.