пятница, 6 ноября 2015 г.

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

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

Согласно определению в статье «Особенности хранения значений в переменных модулей объектов и форм», с диска ИТС:
В платформе 1С:Предприятие 8 используется стратегия управления временем жизни объектов, основанная на подсчете ссылок на объекты - reference counting. Данная стратегия заключается в следующем. Каждый объект платформы содержит счетчик ссылок. При появлении ссылки на объект (объект присваивается какой-либо переменной) происходит увеличение счетчика на единицу, при уничтожении подобной ссылки - значение счетчика на единицу уменьшается. Объект автоматически уничтожается, и память, занимаемая им, освобождается в момент, когда счетчик ссылок объекта становится равен нулю.
Особенностью подсчета ссылок на объекты является возможность организации "циклической ссылки". Циклическая ссылка возникает, когда объекты начинают ссылаться друг на друга. Это приводит к ситуации, при которой ни один из объектов, участвующих в циклической ссылке, не будет уничтожен. В свою очередь это является причиной возникновения утечек памяти (memory leaks).
Необходимо, по возможности, избегать появления циклических ссылок. Соблюдение достаточно простых правил при разработке конфигураций позволит избежать проблем связанных с циклическими ссылками. В случае появления подобной ссылки необходимо определить момент для ее разрыва, чтобы объекты могли быть уничтожены нормальным образом.
Полный перечень случаев возникновения циклической ссылки привести невозможно, приведем лишь некоторые примеры:
  • хранение в переменной модуля объекта (набора записей, формы) ссылки на данный объект (набор записей, форму) - ситуация приводит к появлению циклической ссылки - объект (набор записей, форма) никогда не будет уничтожен;
  • хранение ссылки на владеемый объект в объекте-владельце и ссылки на объект-владелец во владеемом объекте - оба объекта никогда не будут уничтожены.
Следует обратить внимание, что, помимо переменных модулей, ссылки на объекты могут образовываться при передаче их в методы встроенного языка. Например, добавление в список значений нового значения - этого же списка значений, приведет к появлению циклической ссылки.

Циклическая ссылка возникает, когда объекты начинают ссылаться друг на друга. Это приводит к ситуации, при которой ни один из объектов, участвующих в циклической ссылке, не будет уничтожен. В свою очередь, это является причиной возникновения утечек памяти (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
Здесь Memory – объем памяти в байтах, занятой, но не освобожденной за серверный вызов. 
MemoryPeak – пиковое значение занятой за вызов памяти в байтах.
По журналу видно, что за один вызов длительностью 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  между тонким клиентом и сервером без режима медленной работы.
После того, как выявлена форма с циклическими ссылками, расследование места возникновения этой ссылки следует проводить по методу, описанному ранее в разделе «Рабочий процесс занял всю оперативную память (или достиг порога перезапуска)»

Комментариев нет:

Отправить комментарий