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

Обновлено: 02.07.2024

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

Стек программы

Стек программы - это специальная области памяти, организованная по принципу очереди LIFO (Last in, first out - последним пришел, первым ушел). Название "стек" произошло из-за аналогии принципа его построения со стопкой (англ. stack) тарелок - можно класть тарелки друг на друга (метод добавления в стек, "заталкивание", "push"), а затем забирать их, начиная с верхней (метод получения значения из стека, "выталкивание", "pop"). Стек программы также называют стек вызовов, стек выполнения, машинным стеком (чтобы не путать его со "стеком" - абстрактной структурой данных).

Для чего нужен стек? Он позволяет удобно организовать вызов подпрограмм. При вызове функция получает некоторые аргументы; также она должна где-то хранить свои локальные переменные. Кроме того, надо учесть, что одна функция может вызвать другую функцию, которой тоже надо передавать параметры и хранить свои переменные. Используя стек, при передаче параметров нужно просто положить их в стек, тогда вызываемая функция сможет их оттуда "вытолкнуть" и использовать. Локальные переменные тоже можно хранить там же - в начале своего кода функция выделяет часть памяти стека, при возврате управления - очищает и освобождает. Программисты на высокоуровневых языках обычно не задумываются о таких вещах - весь необходимый рутинный код за них генерирует компилятор.

Последствия ошибки

Теперь мы подошли почти вплотную к проблеме. В абстрактном виде стек представляет собой бесконечное хранилище, в которое можно бесконечно добавлять новые элементы. К сожалению, в нашем мире все конечно - и память под стек не исключение. Что будет, если она закончится, когда в стек заталкиваются аргументы функции? Или функция выделяет память под свои переменные?

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

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

Причины ошибки

Что же может привести к такой неприятной ситуации? Исходя из описанного выше механизма, один из вариантов - слишком большое число вложенных вызовов функций. Особенно вероятен такой вариант развития событий при использовании рекурсии. Бесконечная рекурсия (при отсутствии механизма "ленивых" вычислений) прерывается именно таким образом, в отличие от бесконечного цикла, который иногда имеет полезное применение. Впрочем, при небольшом объеме памяти, отведенной под стек (что, например, характерно для микроконтроллеров), достаточно может быть и простой последовательности вызовов.

Другой вариант - локальные переменные, требующие большого количества памяти. Заводить локальный массив из миллиона элементов, или миллион локальных переменных (мало ли что бывает) - не самая лучшая идея. Даже один вызов такой "жадной" функции легко может вызвать переполнение стека. Для получения больших объемов данных лучше воспользоваться механизмами динамической памяти, которая позволит обработать ошибку её нехватки.

Однако динамическая память является довольно медленной в плане выделения и освобождения (поскольку этим занимается операционная система), кроме того, при прямом доступе приходится вручную выделять её и освобождать. Память же в стеке выделяется очень быстро (по сути, надо лишь изменить значение одного регистра), кроме того, у объектов, выделенных в стеке, автоматически вызываются деструкторы при возврате управления функцией и очистке стека. Разумеется, тут же возникает желание получить память из стека. Поэтому третий путь к переполнению - самостоятельное выделение в стеке памяти программистом. Специально для этой цели библиотека языка Си предоставляет функцию alloca. Интересно заметить, что если у функции для выделения динамической памяти malloc есть свой "близнец" для её освобождения free, то у функции alloca его нет - память освобождается автоматически после возврата управления функцией. Возможно, это только осложняет ситуацию - ведь до выхода из функции освободить память не получится. Даже несмотря на то, что согласно man-странице "функция alloca зависит от машины и компилятора; во многих системах ее реализация проблематична и содержит много ошибок; ее использование очень несерьезно и не одобряется" - она все равно используется.

Примеры

В качестве примера рассмотрим код для рекурсивного поиска файлов, расположенный на MSDN:

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

Как видно, в функции main выделяется память в стеке под массивы типов int и float по миллиону элементов каждый, что в сумме дает чуть менее 8 мегабайт. Если учесть, что по умолчанию Visual C++ резервирует под стек лишь 1 мегабайт, то ответ становится очевидным.

А вот пример, взятый из GitHub-репозитория проекта Flash-плеера Lightspark:

Можно надеятся, что h.getLength()-7 не будет слишком большим числом, чтобы на следующей строчке не произошло переполнения. Но стоит ли сэкономленное на выделении памяти время "потенциального" вылета программы?

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

Поиск циклических ссылок

Циклическая ссылка возникает, когда объекты начинают ссылаться друг на друга. Это приводит к ситуации, при которой ни один из объектов, участвующих в циклической ссылке, не будет уничтожен. В свою очередь, это является причиной возникновения утечек памяти (memory leaks).

Классический пример циклической ссылки:

Такая структура останется в оперативной памяти, пока не будет перезапущен процесс, в котором эта структура была создана.

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

Опасные циклические ссылки

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

  • Рабочий процесс занял всю оперативную память (или достиг порога перезапуска);
  • Бесконечная рекурсия в результате возникновения циклических ссылок;
  • Сеансовые данные заняли все место на диске, на котором расположено хранилище.

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

Рабочий процесс занял всю оперативную память (или достиг порога перезапуска)

Если в конфигурации существуют множество мест возникновения циклических ссылок в серверном коде, то память, занимаемая рабочим процессом, постоянно растет. Выглядит такой рост, на графике использования памяти процессом rphost (счетчик "\Process("rphost*")\Virtual Bytes"), как лестница со ступеньками. Информацию по настройке счетчика вы можете найти в статье.


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

Следует учесть, что данный журнал будет занимать значительный объем. Необходимо размещать его на диске, имеющем достаточно свободного места. Также, рекомендуется периодически архивировать старые файлы и переносить их в другое дисковое пространство. По окончанию расследования имеет смысл отключать журнал, чтобы минизировать влияние на производительность информационной системы. Для сокращения объема журнала возможна установка параметра history до нескольких часов (но, не меньше 2). При этом периодичность архивации должна соответствовать параметру history , т.е. не реже, чем один раз в history -1 часов за предыдущие часы.

Дальше, необходимо расследовать возникновение каждой «ступеньки» на графике. Для этого:

  1. Определяется точное время, когда был скачкообразный рост по данным Performance Monitor,
  2. Ищется событие CALL в технологическом журнале процессов rphost за тоже время со свойством Memory, соответствующим размеру роста памяти на графике. Событие CALL может быть зафиксировано немного позже, но вы должны быть уверены, что скачкообразный рост памяти процесса rphost пришел на время выполнения именно этого вызова.
  • Memory – объем памяти в байтах, занятой, но не освобожденной за серверный вызов.
  • MemoryPeak – пиковое значение занятой за вызов памяти в байтах.

По журналу видно, что за один вызов длительностью 2,7 секунды было выделено, примерно 160 МБ памяти. Согласно графику эта память далее не была освобождена, в чем мы убеждаемся по свойству Memory. Следом за событием CALL в нашем примере следует событие с тем же clientID=405:

Вызов затребовал выделения 160 МБ, а затем попал в подозрение на циклическую ссылку (событие LEAKS технологического журнала).

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

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

В качестве контрольных точек могут использоваться:

  • начало и конец исполнения встроенного языка на клиенте или на сервере;
  • вызов процедуры/функции встроенного языка и возврат из процедуры/функции;
  • начало выполнения одной строки кода встроенного языка и окончание выполнения другой строки кода встроенного языка.

Начальную и конечную контрольную точку определяет элемент <point>. При этом, вложение контрольных точек друг в друга допускается, но игнорируется – подсчет утечек ведется только по внешним контрольным точкам. Например, если в процессе исполнения кода конфигурации были пройдены контрольные точки Начальная1, Начальная2, Конечная1, Конечная2, то утечки будут отслеживаться между точками Начальная1 и Конечная2.

Элемент <point> может иметь один из следующих форматов:

Более подробно вы можете прочитать в документации.

Таким образом в результате анализа указанного журнала вы можете получить:

  • объем занятой и не освобожденной памяти определенным пользователем,
  • стек вызова на встроенном языке, в котором возникла проблема,
  • стек вызова на встроенном языке, в котором были созданы и не освобождены объекты.

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

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

Описанная ситуация плоха по следующим причинам:

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

Бесконечная рекурсия в результате возникновения циклических ссылок

Из-за циклических ссылок, можно вызвать аварийное завершение рабочего процесса.

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

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

Если в форме много реквизитов или коллекций, то не всегда очевидно, какая из них содержит циклическую ссылку. Расследовать причину можно по следующему алгоритму.

1. Собирается технологический журнал с событиями EXCP CALL SCALL с контекстами.

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

3. Ищем последнее событие с контекстом по данному clientID

4. Устанавливаем место в конфигурации, которое привело к ошибке.

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

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

Затем в конце каждой серверной процедуры или функции, формы элемента, вызвать процедуру ПроверитьЦиклическуюСсылку, передав в нее имя процедуры. Например:

Включить сбор технологического журнала с настройками:

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

Сеансовые данные заняли все место на диске, на котором расположено хранилище

Сеансовые данные хранятся на рабочем сервере с назначенным на него сервисом сеансовых данных в каталоге кластера …\reg_<ПОРТ>\snccntx…

В них хранится сеансовая информация, например, информация форм управляемого приложения. Также, в них расположено временное хранилище. Все вызовы ПоместитьВоВременноеХранилище, помещают указанные в параметре данные в каталог сеансовых данных.

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

Неактуальными данные становятся в зависимости от параметра «Адрес» функции ПоместитьВоВременноеХранилище:

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

В момент, когда размер актуальных данных составляет 25% от общего размера всех сеансовых данных, платформа запускает «сборку мусора». В этот момент на диске с сеансовыми данными должно быть свободного места, размером 25% от общего объема сеансовых данных. Если свободного места не хватит, то работа кластера становится, и он не сможет продолжить функционировать до того момента, пока не будут удалены старые сеансовые данные.

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

Для того, чтобы определить «зависают» ли данные формы, необходимо добавить в форму процедуру:

Затем, в процедуре ПриЗакрытии добавить последней строкой ее вызов.

Настроить технологический журнал на сбор информации:

Затем открыть форму, проделать в ней операции, характерные для работы пользователей и закрыть.

В случае успешного сброса сеансовых данных в технологическом журнале будет пара событий за один короткий промежуток времени, clientID= которых совпадает:

В случае «зависшей» формы будет только событие SDBL.

Однако, необходимо учитывать, что событие VRSREQUEST… ClearTempStorage может быть вызвано не сразу после закрытия формы. Это характерно для медленного соединения. Поэтому, поиск «зависших» форм необходимо проводить на тестовых серверах в монопольном режиме с соединением по TCP/IP между тонким клиентом и сервером без режима медленной работы.

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

если коменчу строку 1 - все нормально, если коменчу строку 2 - рхост глючит и в 1с получаю бред про ошибке выхода из сервеной процедуры. отлаживать не выходит - получаю "Переполнение стека встроенного языка на сервере". понимаю, что надо смотреть ПКО "ВозвратИзОтпускаПоУходуЗаРебенком", но кода там нет, конфа идентичная, все сопоставления "на автомате". Куда копать?

признак, что рхост валится - остается блокировка на файле, а в консоли сервера никаких соединений.

на худой конец остается точки останова ставить и по принципу "деления пополам" локализовывать место где 1С валится

+ а если отладка не пашет так ЗаписьЖурналаРегистрации можно сделать

тогда - хотя-бы можно будет видеть дошла программа до точки или нет

а может где-то в ПКО стоит флаг "не запоминать выгруженные объекты"?

+ цикличные ссылки, 1С "не понимает" уже выгрузила объект и делает это по миллион раз

+ правда в СП написано, что это для 7.7 только но чем-то таким "попахивает"

[но кода там нет, конфа идентичная] Одно правило цепляет другие и в них может быть код

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

может УниверсальнуюВыгрузкуЗагрузку как-то пильнуть, чтоб выловить повторные выгрузки объектов ?

прошел по всем ПКО - все проверил, вроде ничего не менял - но все заработало :( шайтан.

не нравятся мне ситуации, когда программа через раз работает

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

Это покажет рукводителю необходимость выделить денег на тестовый сервер. При включении ТЖ все начнут нафиг вываливаться минут через 10-30 в зависимости от железа сервера.

Может быть покажет, а может и нет. Это какое должно быть барахло, чтоб при включении ТЖ по этим событиям все начали вываливаться?

[Это какое должно быть барахло] А что? Если неграмотно настроить ТЖ, то система будет в него писать все события и в итоге этот ТЖ загадит аж терабайтовый диск за очень короткий промежуток времени - было у меня такое.

Запись указанных событий незначительно влияет на IOPS и файлы не занимают много места.

Читайте также: