Превышен максимальный расход памяти за один вызов: как мы победили падение rphost в 1С:ERP на лицензии ПРОФ

26.10.26

База данных - HighLoad оптимизация

Расчет сложного многопередельного заказа в 1С:ERP падает с ошибкой «Превышен максимальный расход памяти сервера за один вызов», процесс rphost на сервере Linux сжирает 8 ГБ за 7 минут, а лицензия на сервере - ПРОФ, где тонкие настройки памяти кластера игнорируются платформой. В статье делюсь практическим опытом: как без покупки дорогой лицензии 1С:КОРП и без наращивания планок памяти ускорить тяжелый расчет в 8 раз. Разбираем отказ от копирования таблиц остатков (наложение дельты), кэширование деревьев спецификаций и параллельный запуск через 4 фоновых задания. Примеры кода на встроенном языке 1С, замеры производительности и разбор граблей внутри.

Друзья, всем привет!

Хочу поделиться реальным производственным кейсом, на который ушло несколько дней и чашек кофе. Речь пойдет о борьбе с коварной и знакомой многим ошибкой:

«Превышен максимальный расход памяти сервера за один вызов»

Ситуация усугублялась классическими условиями большинства наших внедрений: сервер 1С работает на Linux, СУБД - PostgreSQL, а лицензия на сервер - обычный ПРОФ, а не КОРП.

Если вы хоть раз сталкивались с выжиранием памяти и аварийным завершением вызовов в тяжелых расчетных задачах, то знаете стандартный ответ: «Купите лицензию КОРП, добавьте оперативки на 128 ГБ или переделывайте бизнес-процесс».

Но мы решили задачу исключительно в коде конфигурации - на уровне правильных структур данных в памяти, оптимизации кэша и распараллеливания через фоновые задания. В итоге скорость роста памяти упала в 8 раз, падения прекратились. Те же 10 позиций, которые в одном потоке считались 36-51 минуту, проходят за 4,5 минуты. Список из 45 позиций на уже оптимизированном коде без параллели - 59 минут, в 4 потока - 18 минут.

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

Поехали!

В чем суть задачи?

В реальном многопередельном производстве (электротехника, приборостроение) типовой механизм разузловки 1С:ERP часто заходит в тупик.

У каждого изделия и полуфабриката есть десятки конструкторских ревизий спецификаций (_1, _2 … _11). На складе при этом лежит сборная солянка: готовые узлы старых ревизий от прошлых выпусков, разрешенные аналоги материалов, позиции в пути или уже заказанные у поставщиков.

Если запускать партию строго по основной спецификации (как делает типовая ERP), система упрется в отсутствие одной детали и покажет дефицит. А в это время на складе пылятся готовые полуфабрикаты прошлой ревизии, под которые всё есть! В итоге диспетчеры и конструкторы садились за Excel и  вручную подбирали ревизии по 40-50 заказам.

Мы написали специализированный инструмент - «Динамический запуск производства». Это интерактивный симулятор. Он идет сверху вниз по заказам клиентов (кто выше - у того приоритет на остатки). Сначала пытается закрыть потребность готовыми изделиями и открытыми этапами. Затем пробует собрать полные комплекты по ревизиям (от свежих к старым). А на оставшийся «хвост» подбирает до 150 комбинаций спецификаций (мы назвали их «ключами ревизий») с минимальным дефицитом по закупке, чтобы диспетчер на форме мог нажать кнопку «Сравнить варианты» и выбрать лучший.

Стена: падение по памяти на 45 позициях

На 5-10 позициях заказов механизм работал быстро. Но когда плановики загрузили реальный сменный пул на 45 сложных изделий, начались проблемы.

Ровно через 7 минут расчет вылетал с ошибкой превышения памяти вызова.

Мы сняли замеры параметров сеанса через rac и журнал регистрации. Время ожидания СУБД (duration-current-dbms) = 0 секунд. PostgreSQL вообще не был нагружен. Все 100% времени и памяти уходили внутри кода 1С в рабочем процессе rphost. Память одного вызова росла прямо на глазах: 30 сек - 0.6 ГБ, 90 сек - 1.6 ГБ, 7 минут - 8.0 ГБ (скорость прироста - больше 1 ГБ в минуту).

Попытка решения через настройки кластера серверов (и почему ПРОФ вас расстроит)

Первая мысль любого администратора: зайти через консоль кластера или rac и подкрутить параметры safe-call-memory-limit (безопасный расход памяти за один вызов), max-memory-size (лимит памяти рабочих процессов), lifetime-limit (интервал перезапуска процессов).

Мы настроили лимиты через rac, утилита бодро вернула rc=0 (успешно). Но платформа 1С на лицензии ПРОФ просто игнорирует эти параметры. На ПРОФ работает только один глобальный аварийный сброс - обрыв вызова при приближении процесса к ~80% оперативной памяти сервера. Тонко управлять вызовами по отдельности без КОРП нельзя.

Пришлось открывать замер производительности и разбираться, что именно съедает память в коде.

Что показал замер?

Мы расставили замеры через ТекущаяУниверсальнаяДатаВМиллисекундах() на каждую строку заказа и на внутренние шаги алгоритма.

Картина оказалась неожиданной. 9 строк из 10 считались за 0.1 секунды. Но одна сложная многопередельная позиция уходила в расчет на 140 секунд, порождая 96 вариантов комбинаций и 1250 записей в кэше. А внутри разузловки узла 57% времени уходило не на сам расчет остатков, а на сборку длинной текстовой строки-ключа для кэша поддерева.

Ниже - 5 шагов, которыми мы полностью устранили эту проблему.

Шаг 1. Перестаем клонировать склад: наложение дельты на базовые остатки

Было. Чтобы примерить каждый из 150 вариантов спецификаций хвоста, старый код вызывал СкопироватьКонтекст(). Функция делала глубокую копию всей таблицы остатков склада (Соответствие), пулов «в пути» и «в заказах». Тысячи полных копий тяжелых коллекций в памяти на один заказ.

Стало. Мы отказались от клонирования и разделили остатки на два слоя - базовый склад и дельту изменений. Основной склад (База) лежит в памяти в единственном экземпляре и никогда не копируется. Вариант для примерки создает маленькое пустое Соответствие (Дельта). При чтении остатка функция сначала проверяет Дельту. Если там ключа нет - читает Базу. А при тестовом списании запись идет только в Дельту.

// Чтение и запись остатка: если кэш - структура База+Дельта,
// меняется только дельта, общий склад не копируется.
Функция КоличествоВКэше(Кэш, Номенклатура)
	
	Если ТипЗнч(Кэш) = Тип("Структура") Тогда
		Тек = Кэш.Дельта.Получить(Номенклатура);
		Если Тек = Неопределено Тогда
			Тек = Кэш.База.Получить(Номенклатура);
		КонецЕсли;
		Возврат Тек;
	КонецЕсли;
	
	Возврат Кэш.Получить(Номенклатура);
	
КонецФункции

Процедура УстановитьКоличествоВКэше(Кэш, Номенклатура, Значение)
	
	Если ТипЗнч(Кэш) = Тип("Структура") Тогда
		Кэш.Дельта.Вставить(Номенклатура, Значение);
	Иначе
		Кэш.Вставить(Номенклатура, Значение);
	КонецЕсли;
	
КонецПроцедуры

В итоге память перестала раздуваться при переборе сотен вариантов.

Шаг 2. Кэш поддеревьев: короткие идентификаторы и снятие вредного лимита

Один и тот же покупной узел или плата входит во множество разных изделий. Чтобы не разузловывать его повторно, в алгоритме жил КэшПоддеревьев. Но там было три узких места. Ключ кэша строился рекурсивным обходом всего дерева спецификации вниз со склейкой строк (те самые 57% процессорного времени). При помещении в кэш и извлечении из него код делал полную копию структуры узла. Стояло ограничение «для экономии памяти»:

Если Кэш.Количество() > 5000 Тогда
	ОчиститьКэш();
КонецЕсли;

Структуру узла стали определять один раз: добавили поля ИдСтруктуры и Листья прямо в описание позиции при первой инициализации. Ключ кэша стал собираться мгновенно из коротких идентификаторов вместо полного обхода дерева. Убрали копирование узлов: сформированная ветка узла после расчета не меняется, поэтому варианты могут безопасно использовать ссылки на один и тот же объект в памяти.

Удалили лимит в 5000 записей. Замер показал парадокс: лимит в 5000 забивался уже к 14-й строке заказов. Кэш очищался, и оставшиеся 30 заказов (с теми же самыми узлами) считали одни и те же полуфабрикаты с нуля, раздувая память и вешая сервер. После того как узлы перестали копироваться, сам кэш стал легким, и снятие лимита дало огромный выигрыш в скорости.

Шаг 3. Отсечение заведомо пустых веток

Простая математическая логика. Если при примерке свежей ревизии полуфабриката на 1 штуку мы видим, что все 100% материалов ушли в дефицит (на складе нет ничего, в заказах поставщиков пусто), то более старые ревизии этого же полуфабриката при тех же остатках перебирать бессмысленно - они дадут ровно такой же дефицит.

Мы добавили проверку УровеньРевизииЦеликомВДефиците. Такие ветки отсекаются сразу, еще до перебора комбинаций.

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

Результат первых трех шагов. Скорость роста памяти упала с 25 МБ/с до 3 МБ/с (в 8 раз). Вызов на 75-й секунде стал занимать всего 330 МБ вместо прежних 2 ГБ.

Шаг 4. Постоянные номера ключей и пропуск пересчетов

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

Мы вынесли ключи в справочник «Ключи ревизий производства». Уникальность определяется контрольной суммой состава: головная спецификация плюс упорядоченный список пар «полуфабрикат / спецификация». Один и тот же набор всегда находит свой старый элемент, новый набор получает следующий свободный номер после точки.

А в регистр сведений стали писать выбор: ЗаказКлиента + Строка → ВыбранныйКлюч.

В чем плюс для скорости. Когда диспетчер уже выбрал и сохранил ключ по строке, при следующем общем пересчете отчёт вообще не запускает перебор 150 комбинаций для этой строки. Он мгновенно подтягивает утвержденный состав из регистра.

Шаг 5. Распараллеливание на 4 фоновых задания

Даже оптимизированный расчет на 45 позициях нагружает процессор. Хотелось параллельности. Но как распараллелить алгоритм, который списывает остатки строго последовательно сверху вниз по общему складу?

Мы использовали ключевую особенность логики. Склад списывается последовательно только для твердо закрытой части (готовые узлы и полные комплекты). А перебор 150 вариантов для незакрытого остатка - это моделирование на копии остатков, которое на баланс следующих строк никак не влияет.

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

Четыре исполнителя параллельно идут по тому же списку. Но каждый берет в перебор вариантов только «свои» номенклатуры (раздача идет равномерно по весу ревизий). У каждого фонового задания в кластере 1С - свой собственный независимый лимит памяти.

Сборка результата. Координатор дожидается завершения фоновых заданий (с понятным прогрессом в окне ожидания: «Ожидание: завершено 3 из 4»), объединяет таблицы вариантов и формирует результат на форме.

Функция ЗапуститьИсполнителейПеребора(ПараметрыОтчета)
	
	Исполнители = Новый Массив;
	ВсегоИсполнителей = 4;
	
	Для Номер = 0 По ВсегоИсполнителей - 1 Цикл
		
		ПараметрыИсполнителя = Новый Структура;
		ПараметрыИсполнителя.Вставить("НомерИсполнителя", Номер);
		ПараметрыИсполнителя.Вставить("ЧислоИсполнителей", ВсегоИсполнителей);
		
		ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияФункции(Новый УникальныйИдентификатор);
		ПараметрыВыполнения.ЗапуститьВФоне = Истина;
		ПараметрыВыполнения.ОжидатьЗавершение = 0;
		ПараметрыВыполнения.НаименованиеФоновогоЗадания = СтрШаблон(
			НСтр("ru = 'Перебор вариантов, исполнитель %1 из %2'"), Номер + 1, ВсегоИсполнителей);
		
		Исполнители.Добавить(ДлительныеОперации.ВыполнитьФункцию(ПараметрыВыполнения,
			"Отчеты.ДинамическийЗапуск.ПеребратьВариантыХвостов", ПараметрыИсполнителя));
		
	КонецЦикла;
	
	Возврат Исполнители;
	
КонецФункции

Контрольный прогон на тех же 10 позициях. Старт 15:36:31 по времени сервера. Одновременно жило максимум 3 задачи (не 4): к 15:37:07 ещё 3, к 15:38:07 уже 2, к 15:40:58 осталась 1 (скорее координатор), к ~15:41:15 сеансов нет. Итого 4 минуты 30-45 секунд. После расчёта load сервера 0.5 - машина свободна.

Для сравнения по той же десятке: один поток 36-51 мин, разрез на 4 части подряд ~12 мин, этот запуск ~4.5 мин.

Список из 45 позиций на том же оптимизированном коде, но без параллели (подряд в одном вызове) - 59 минут. Тот же список в 4 потока - 18 минут.

Итоги в цифрах

Скорость роста памяти rphost: было 18-25 МБ/сек (~1.2 ГБ/мин), стало 2-4 МБ/сек (в 6-8 раз меньше).

Память вызова на 75-й секунде: было ~1.8 ГБ, стало 0.33 ГБ.

Расчёт 10 позиций: один поток 36-51 мин, 4 части подряд ~12 мин, 4 фоновых задания - 4 мин 30-45 сек (макс. 3 живых сеанса).

Расчёт 45 позиций: раньше падение по лимиту памяти на 8 ГБ. На оптимизированном коде подряд - 59 мин, в 4 фоновых задания - 18 мин.

Повторный расчет строки с выбранным ключом: вместо полного цикла перебора комбинаций - мгновенно, по сохраненному ключу.

Главный вывод

Если ваш сервер 1С на лицензии ПРОФ падает с ошибкой превышения памяти вызова, не спешите просить бюджет на лицензии КОРП или новые сервера.

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

Надеюсь, статья окажется полезной коллегам, сопровождающим производственные базы на 1С:ERP. 

Вступайте в нашу телеграмм-группу Инфостарт

1C:ERP оптимизация памяти rphost Linux фоновые задания многопередельное производство БСП управление памятью

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

HighLoad оптимизация Разработчик 1С 8.3 1С:ERP Управление предприятием 2 Бесплатно (free)

Использование оператора «В» для полей или данных составного типа (например, Регистратор) может приводить к неочевидным проблемам.

10.11.2025    15919    ivanov660    48    

58

HighLoad оптимизация Разработчик 1С:Предприятие 8 1C:ERP Бесплатно (free)

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

18.02.2025    16501    ivanov660    39    

62

HighLoad оптимизация Разработчик Россия Бесплатно (free)

А вы знали, что сервер 1С при соединении с базой на сервере PostgreSQL самостоятельно устанавливает некоторые параметры? Это важно знать при настройке сервера и отладке долгих запросов. Предлагаю разобраться.

27.08.2024    10417    soulner    10    

41

HighLoad оптимизация Технологический журнал Системный администратор Разработчик Бесплатно (free)

Обсудим поиск и разбор причин длительных серверных вызовов CALL, SCALL.

24.06.2024    19154    ivanov660    13    

64

HighLoad оптимизация Разработчик 1С:Предприятие 8 Бесплатно (free)

Метод очень медленно работает, когда параметр приемник содержит намного меньше свойств, чем источник.

06.06.2024    24996    Evg-Lylyk    73    

46

HighLoad оптимизация Разработчик 1С:Предприятие 8 1C:Бухгалтерия Бесплатно (free)

Анализ простого плана запроса. Оптимизация нагрузки на ЦП сервера СУБД используя типовые индексы.

13.03.2024    14639    spyke    29    

54
Для отправки сообщения требуется регистрация/авторизация