При масштабировании систем на платформе «1С:Предприятие 8.3» часто применяется практика разделения ролей серверов в кластере. Например, один сервер выделяется исключительно для обслуживания сеансов пользователей, а второй — для выполнения регламентных и тяжелых фоновых заданий. Однако при такой конфигурации разработчики и администраторы часто сталкиваются со специфическими ошибками производительности, среди которых наиболее коварной является «Недостаточно памяти для выполнения операции на один вызов» или внезапное аварийное завершение рабочих процессов rphost.
В этой статье мы подробно проанализируем, почему возникает эта проблема при переносе фоновых заданий на выделенный сервер кластера, как на это влияют параметры сеанса и модули с повторным использованием возвращаемых значений, а также рассмотрим эффективные архитектурные и административные способы её решения.
Для того чтобы понять первопричину проблемы, разберем внутреннюю архитектуру взаимодействия рабочих процессов в многосерверном кластере 1С.
Когда вы настраиваете требования назначения функциональности (ТНФ), разделяя клиентские сеансы и фоновые задания по разным физическим серверам (например, 1S_1 для пользователей и 1S_2 для фоновых заданий), вы создаете распределенную среду. В этой среде физические процессы rphost выполняются на разных машинах, но работают в рамках единого логического кластера.
Ключевой конфликт кроется в механизме работы с Параметрами сеанса и Сервисом сеансовых данных:
1S_1).1S_2, при каждом обращении к параметру сеанса рабочий процесс rphost на 1S_2 вынужден делать сетевой запрос к серверу 1S_1, чтобы забрать данные.ХранилищеЗначения), то при каждом обращении происходит полная сетевая передача этого двоичного пакета с последующей десериализацией в оперативной памяти сервера 1S_2.rphost требуется выделить огромный непрерывный блок адресного пространства. Если такие обращения происходят параллельно из нескольких потоков фоновых заданий, виртуальная память процесса мгновенно фрагментируется, что приводит к ошибке выделения памяти «на один вызов», даже если суммарно физической памяти на сервере достаточно.Использование общего модуля с опцией «Повторное использование возвращаемых значений» (кэша) в данной архитектуре накладывает дополнительный негативный эффект:
rphost на сервере 1S_2. Однако кэш имеет свойство устаревать (очищается через 20 минут неактивности, через 6 минут после последнего использования или принудительно сбрасывается платформой при определенных транзакционных событиях). Как только кэш сбрасывается, следующий запрос снова инициирует тяжелую десериализацию. Если этот момент совпадает с пиком нагрузки, происходит моментальный вылет по лимиту памяти на один вызов.rphost мгновенно исчерпывает адресное пространство и аварийно падает (dump/crash).При работе на одном физическом сервере (1S_1) этих проблем не возникает, так как обращение к Сервису сеансовых данных происходит локально, минуя сетевой стек и тяжелую повторную сериализацию объектов платформы.
Ниже мы подробно рассмотрим пошаговые методы исправления этой ситуации: от изменения архитектуры кода до тонкой административной настройки кластера.
Использование параметров сеанса для передачи больших объемов данных в фоновые задания является грубой архитектурной ошибкой. Гораздо эффективнее и безопаснее передавать данные напрямую в качестве параметров при старте фонового задания, либо задействовать механизм Временного хранилища.
Рассмотрим по шагам, как переписать этот механизм правильно.
Шаг 1. Помещение данных во временное хранилище и запуск задания:
Вместо записи тяжелой структуры в параметр сеанса, мы один раз помещаем ее во временное хранилище родительского сеанса и передаем полученный адрес в качестве параметра запуска фонового задания.
// Код выполняется в родительском сеансе на сервере 1S_1
СтруктураДанных = Новый Структура;
СтруктураДанных.Вставить("Таблица20К", ПолучитьТаблицу20К());
СтруктураДанных.Вставить("Таблица110К", ПолучитьТаблицу110К());
// Помещаем во временное хранилище с привязкой к уникальному идентификатору
АдресХранилища = ПоместитьВоВременноеХранилище(СтруктураДанных, Новый УникальныйИдентификатор);
// Передаем адрес в параметрах запуска фонового задания
ПараметрыЗапуска = Новый Массив;
ПараметрыЗапуска.Добавить(АдресХранилища);
ФоновыеЗадания.Выполнить("МойОбщийМодуль.ВыполнитьСборДанных", ПараметрыЗапуска);
Шаг 2. Получение данных внутри фонового задания:
Внутри фонового задания на сервере 1S_2 мы один раз в самом начале процедуры извлекаем данные из хранилища и работаем с ними локально в оперативной памяти.
// Код общего модуля (сервер 1S_2)
Процедура ВыполнитьСборДанных(АдресХранилища) Экспорт
// Получаем структуру данных ОДИН раз
ДанныеДляРаботы = ПолучитьИзВременногоХранилища(АдресХранилища);
Если ДанныеДляРаботы = Неопределено Тогда
ВызватьИсключение "Не удалось получить исходные данные из временного хранилища!";
КонецЕсли;
Таблица110К = ДанныеДляРаботы.Таблица110К;
// Дальнейшая обработка данных происходит локально и очень быстро
Для Каждого СтрокаТаблицы Из Таблица110К Цикл
// Логика обработки строк
КонецЦикла;
КонецПроцедуры
Этот метод исключает повторные сетевые обращения к параметрам сеанса и полностью решает проблему фрагментации памяти при десериализации.
Если к таблице на 110 тысяч строк в процессе работы фонового задания требуется обращаться сотни раз для поиска информации, держать всю эту таблицу в оперативной памяти неэффективно. Лучшее решение — делегировать операции поиска и фильтрации самому серверу СУБД, который оптимизирован для работы с такими объемами.
Рассмотрим, как реализовать этот подход:
Справочник.БуферДанныхФоновыхЗаданий).Посмотрим на пример кода запроса внутри фонового задания:
// Пример быстрого точечного поиска данных через СУБД внутри фонового задания
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Буфер.Поле1,
| Буфер.Поле2,
| Буфер.Значение
|ИЗ
| Справочник.БуферДанныхФоновыхЗаданий КАК Буфер
|ГДЕ
| Буфер.ИдентификаторПакета = &ИдентификаторПакета
| И Буфер.КлючПоиска = &КлючПоиска";
Запрос.УстановитьПараметр("ИдентификаторПакета", УникальныйИдентификаторПакета);
Запрос.УстановитьПараметр("КлючПоиска", ИскомоеЗначение);
РезультатЗапроса = Запрос.Выполнить();
После завершения работы фонового задания родительский сеанс или регламентное задание очистки удаляет отработанные записи из справочника по идентификатору пакета.
Если фоновое задание и родительский сеанс могут работать в рамках одной транзакции (или одного контекста соединения), то можно передавать данные через менеджер временных таблиц СУБД.
Для этого таблица значений помещается во временную таблицу SQL, а сам МенеджерВременныхТаблиц передается в параметры фонового задания. Это позволит выполнять пакетные запросы к данным без необходимости сериализации таблиц в память сервера приложений.
Если бюджет проекта не позволяет прямо сейчас вносить изменения в код конфигурации, проблему можно временно локализовать на уровне настроек кластера серверов 1С.
Мы можем принудительно заставить платформу обрабатывать Сервис сеансовых данных непосредственно на сервере фоновых заданий 1S_2, чтобы исключить сетевой оверхед.
Пошаговая настройка требований назначения функциональности в консоли кластера:
1S_2 и откройте раздел «Требования назначения функциональности».1S_1 добавьте зеркальное правило с типом требования «Не назначать» для этого же сервиса (либо настройте меньший приоритет).Важное предостережение: Перенос Сервиса сеансовых данных на сервер 1S_2 уберет сетевую сериализацию для фоновых заданий, но теперь обычные пользователи, работающие на 1S_1, будут совершать сетевые вызовы к 1S_2 при обращении к своим параметрам сеанса. Этот способ подходит только в том случае, если интенсивность работы пользователей с параметрами сеанса значительно ниже, чем в фоновых заданиях.
Проводя аудит серверов 1С, обязательно обратите внимание на используемую лицензию и версию платформы: