Технологические вопросы крупных внедрений
13.05.2016

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

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

Циклические ссылки

Согласно определению в статье «Особенности хранения значений в переменных модулей объектов и форм»:

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

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

Копировать в буфер обмена
Данные = Новый Структура;
Данные.Вставить("Ключ", Данные);

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

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

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

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

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

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

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

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

Копировать в буфер обмена
<config xmlns="http://v8.1c.ru/v8/tech-log">
	<log location="c:\log\log_memory_leaks" history="12">
		<event>
			<eq property="Name" value="CALL"/>
		</event>
		<event>
			<eq property="name" value="LEAKS"/>
		</event>
		<property name="all"/>
	</log>
	<leaks collect = "1">
		<point call = "server"/>
	</leaks>
</config>

 

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

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

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

Например:

Копировать в буфер обмена
09:49.242027-2703000,CALL,1,process=rphost,p:processName=DB,t:clientID=405,t:applicationName=WebServerExtension,t:computerName=SERVER,t:connectID=372,Usr=WS,SessionID=4013,
Interface=bc15bd01-10bf-413c-a856-ddc907fcd123,Method=0,CallID=282742730,Memory=165568867,MemoryPeak=167010093,InBytes=91259002,OutBytes=79986501

Здесь:

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

Копировать в буфер обмена
09:49.242028-0,LEAKS,1,process=rphost,t:clientID=405,t:applicationName=WebServerExtension,t:computerName=SERVER,t:connectID=372,Usr=WS,Descr='CatalogManager.Контрагенты…

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

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

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

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

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

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

Копировать в буфер обмена
<point call="client"/>, <point call="server"/>

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

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

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

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

Пример:

Копировать в буфер обмена
&НаСервере

Перем ФормаНаСервере;

&НаСервере

Перем СтруктураНаСервере;

&НаСервере

Перем КонтрагентНаСервере;

&НаСервере

Процедура LeaksНаСервере()

  КонтрагентНаСервере = Справочники.Контрагенты.СоздатьЭлемент(); // Событие LEAKS, т.к. при выходе их процедуры переменная не сброситься. Но циклической ссылки нет.

  ФормаНаСервере = ЭтаФорма; // Циклическая ссылка

  СтруктураНаСервере = Новый Структура("ДанныеФормы", ФормаНаСервере);  // Циклическая ссылка

КонецПроцедуры

&НаКлиенте

Процедура Leaks (Команда)

  LeaksНаСервере();

КонецПроцедуры

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

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

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

Пример:

Копировать в буфер обмена
&НаСервере

Функция ВызватьАварийноеЗавершениеСервер()


  СтруктураСервер = Новый Структура();

  СтруктураСервер.Вставить("Свойство1", Неопределено);

  СтруктураСервер.Вставить("Свойство2", СтруктураСервер); // Циклическая ссылка структуры саму на себя


  Возврат СтруктураСервер;


КонецФункции // В момент возврата с сервера на клиент, рабочий процесс упадет с дампом


&НаКлиенте

Процедура ВызватьАварийноеЗавершение(Команда)

  СтруктураСервер = ВызватьАварийноеЗавершениеСервер();

КонецПроцедуры

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

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

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

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

Копировать в буфер обмена
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
	<log location="C:\LOGS\All" history="12">
		<event>
			<eq property="Name" value="EXCP"/>
		</event>
		<event>
			<eq property="Name" value="CALL"/>
		</event>
		<event>
			<eq property="Name" value="SCALL"/>
		</event>
		<property name="all"/>
	</log>
</config>

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

Копировать в буфер обмена
17:22.5180-0,EXCP,2,process=rphost,p:processName=DB,t:clientID=173,t:applicationName=1CV8, t:computerName=SERVER,t:connectID=1577,SessionID=1622,Usr=Иванов,DumpFile=…

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

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

Копировать в буфер обмена
17:13.1738-2,CALL,1
17:13.1742-0,Context,1,Context='Справочник.Контрагенты.Форма.ФормаЭлемента.Форма : 777 : ОбновитьСписокДоговоров();'

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

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

Копировать в буфер обмена
&НаСервере

Процедура ПроверитьЦиклическуюСсылку(ИмяПроцедуры)

  РеквизитыФормы = ПолучитьРеквизиты();

  Для Каждого СвойствоФормы Из РеквизитыФормы Цикл

    Попытка

      Запрос = Новый Запрос("ВЫБРАТЬ ""ЛовушкаЦиклическихСсылок"", """ + ИмяПроцедуры + """ КАК ИмяПроцедуры, """ + СвойствоФормы.Имя + """ КАК СвойствоФормы");

      Запрос.Выполнить();

    Исключение

      Продолжить;

    КонецПопытки;

    Попытка

      ЗначениеДляСериализации = ЭтаФорма[СвойствоФормы.Имя];

      Сериализатор = Новый СериализаторXDTO(ФабрикаXDTO);

      ОбъектXDTO = Сериализатор.ЗаписатьXDTO(ЗначениеДляСериализации); // Здесь может возникнуть ошибка

      ОбъектXDTO.Проверить();

    Исключение

    КонецПопытки;

  КонецЦикла;

КонецПроцедуры

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

Копировать в буфер обмена
&НаСервере

Процедура ОбновитьСписокДоговоров().


  ПроверитьЦиклическуюСсылку("ОбновитьСписокДоговоров");

КонецПроцедуры

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

Копировать в буфер обмена
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
	<log location="c:\log\log_dumps" history="24">
		<event>
			<eq property="Name" value="SDBL"/>
			<like property="Sdbl" value="%ЛовушкаЦиклическихСсылок%"/>
			<eq property="Usr" value="Иванов "/>
		</event>
		<property Name="all"/>
	</log>
</config>

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

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

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

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

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

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

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

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

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

Копировать в буфер обмена
&НаСервереБезКонтекста

Процедура ПроверитьСбросВременногоХранилища(ИмяФормы)


  Запрос = Новый Запрос("

  |ВЫБРАТЬ

  | ""CloseFormCheckTempStorage_" + ИмяФормы+ """

  |");

  Запрос.Выполнить();


КонецПроцедуры

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

Копировать в буфер обмена
&НаКлиенте

Процедура ПриЗакрытии()


  ПроверитьСбросВременногоХранилища(ЭтаФорма.ИмяФормы);


КонецПроцедуры

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

Копировать в буфер обмена
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://v8.1c.ru/v8/tech-log">
	<log location="c:\log\log_session" history="24">
		<event>
			<eq property="name" value="SDBL" />
			<like property="Sdbl" value="%CloseFormCheckTempStorage%" />
		</event>
		<event>
			<eq property="name" value="VRSREQUEST" />
			<like property="URI" value="%ClearTempStorage%" />
		</event>
		<property name="all"> </property>
	</log>
</config>

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

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

Копировать в буфер обмена
24:14.294050-7,SDBL,4,process=rphost,p:processName=DB,t:clientID=190,t:applicationName=1CV8C,t:computerName=Computer,t:connectID=14,
SessionID=30,Usr=User,AppID=1CV8C,Trans=0,Sdbl='SELECT"CloseFormCheckTempStorage_Справочник.Контрагенты.Форма.ФормаЭлемента"',Rows=1,
Context='Форма.Вызов : Справочник.Контрагенты.Форма.ФормаЭлемента.Модуль.ПроверитьСбросВременногоХранилищаСправочник.Контрагенты.Форма.ФормаЭлемента.Форма
: 777 : Запрос.Выполнить();'
25:16.780017-0,VRSREQUEST,3,process=rphost,p:processName= DB,t:clientID=190,t:applicationName=1CV8C,t:computerName=Computer,t:connectID=14,Method=POST,
URI='/e1cib/files?cmd=ClearTempStorage',Headers='1C-ConnectString:

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

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

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