Однажды у нас начали происходить странные вещи: dev-сервер 1С намертво зависал после двух-трёх суток работы, после чего требовался внеплановый перезапуск системы. Первым «звоночком» всегда становилось резкое замедление работы.
При этом prod работал стабильно и не показывал каких-либо признаков проблем. Обычно всё происходит наоборот: dev работает, а на prod — проблемы, ошибки, тормоза и прочее. И в такой ситуации всё понятно: нагрузка больше, значит часть потенциальных проблем на dev'е мы просто не заметили. В нашем же случае получается, что dev отдыхает и виснет. Причем виснет ближе к утру, чаще даже до начала следующего рабочего дня, когда, понятное дело, нагрузки на него нет вообще никакой. Для справки: на тот момент стояла платформа 8.3.18, но думаю, что дело совсем не в этом.
Расследование. Начало
Первым делом мы обнаружили связь зависаний с одной конкретной базой на этом сервере — нашей общей тестовой базой, в которой аналитики проверяли доработки непосредственно перед их отправкой на prod. Как именно нашли, я уже вспомнить не смогу: наблюдали за работой клиентов, доверились интуиции... Проверили гипотезу достаточно просто: в течение некоторого времени отключали на ночь тестовую базу. Результатом стало отсутствие проблем с зависанием. Как только перестали это делать — всё вернулось назад.
Дальнейшие поиски возможных причин проблемы мы проводили уже на этой базе, хоть нам было и непонятно, чем она отличается от остальных. На сервере их более десятка, и все, включая тестовую, — клоны одной и той же рабочей базы.
Первое подозрение упало на нехватку выделенной оперативной памяти. Однако технологический журнал ничего особенного не показал: всё, что нужно было, каждый раз выделялось без проблем. Свободной памяти оставалось достаточно вплоть до момента полного зависания системы. И в этом была главная странность.
Также мы наблюдали за выделением памяти в мониторе ресурсов (в операционной системе), но ничего подозрительного не обнаружили: её было более чем достаточно, использовалась только треть... Но! Когда мы долго наблюдали за числами памяти по rphost`ам, заметили, что они скачут, а значит есть какая-то активность. Другим открытием стало следующее — часть выделенной памяти со временем не освобождается. Хотя и совсем маленькая часть, в пределах погрешности. Речь о килобайтах. Абсурдно мало, но проблема есть. И мы продолжили поиски.
Ищем активности
Итак, регламентные задания мы давно выключили. На всякий случай убрали обработчики ожидания. Не помогло.
Мы дошли до того, что выключили вообще все обработчики.
// "дошли до ручки"
Процедура ПередНачаломРаботыСистемы(Отказ)
Возврат;
КонецПроцедуры
Процедура ПриНачалеРаботыСистемы()
Возврат;
КонецПроцедуры
В итоге мы включаем замер производительности и убеждаемся, что вообще никакой код не выполняется. По сути просто конфигурация 1С с базой данных. Далее смотрим монитор ресурсов и видим: есть активность! Выделение памяти по-прежнему прыгает и немножко деградирует: килобайты со временем не освобождаются, хотя этого и недостаточно, чтобы забить всю доступную память.
У нас уже не оставалось никаких идей, как вдруг в почти пустом к этому времени журнале регистрации мы замечаем наличие запросов к базе по rest-интерфейсу каждые 3–5 минут. Начинаем проверять эту версию. В 1С никакой лог-информации по запросам нет — просто работают и всё, а что делают, никто не знает. У нас сразу появились подозрения: может быть эти rest-запросы выбирают слишком много данных и вешают сервер? Но нет! Разработчики интеграции с сайтом быстро сознались, что это их запросы: выборка минимальная, просто обращение к справочнику пользователей с отбором по наименованию.
Поле, естественно, индексированное, но всё же отключаем и их. Тут добавлю интригу: догадываясь, что происходит, я попросил коллег не выключать совсем, а увеличить между запросами интервал, например, до 30 минут. И это сработало! Сервер перестал зависать.
Было бы абсурдом утверждать, что запросы по интерфейсу rest нельзя делать чаще, чем раз в полчаса, да я и не собираюсь. Прежде чем объяснить ситуацию, я хочу дать небольшую справку о том, как работает «типовой» менеджер памяти. Хочу отметить, что мои коллеги меня не поняли или просто не поверили в мои выводы. Мы настроили принудительный перезапуск dev-сервера каждую ночь, хотя я просил лишь «не беспокоить с 3 до 4 утра». Такие вот пироги...
Модель работы менеджера памяти
Здесь маленькое отступление: я не буду приводить конкретных цифр и вдаваться в нюансы работы определенных механизмов. Это не имеет никакого смысла. В данном контексте достаточно оперировать понятиями «маленькая» (ничтожно) и «большая» (значимая). Поскольку работу конкретных механизмов программы знает лишь их автор, мы будем пользоваться понятной нам моделью — «черным ящиком».
Реальность может быть совсем не такой, как в модели, но это не меняет сути. Менеджер памяти, как обладатель ресурса (большого облака свободной памяти), должен выделять блоки памяти любого размера по запросу, и далее по запросу освобождать их. Для выделения нового блока ему необходимо знать, какое место в облаке уже занято, а какое — свободно. Для этого при каждом выделении памяти менеджер заполняет соответствующую табличку, где фиксирует предоставленные области и их размер. Сама эта табличка, к слову, тоже требует выделение места в памяти. Было бы просто, если бы все выделяемые блоки имели одинаковую длину, но они все разные. Из этого следует, что при выделении каждого нового блока менеджеру памяти нужно обращаться к своей табличке в поисках свободного места (пока незанятого адресного пространства). И чем больше выделено блоков, тем объемнее табличка. Это первый момент, влияющий на скорость работы с памятью, а фактически, на скорость работы всей программы и даже целого сервера.
Где находится менеджер памяти?
Ответ на этот вопрос: везде. Операционная система отдаёт память платформе 1С по запросу, однако, если полагаться только на ОС, скорость будет очень низкой, так как выделяется огромное количество блоков памяти. Я предполагаю (или даже верю в это), что в самой платформе 1С также есть свой менеджер памяти, который получает от ОС большой блок памяти, а затем распределяет его по собственному усмотрению на внутренние нужды. То есть мы имеем два менеджера памяти — на сервере и на клиенте. Возможно, даже у каждой формы свой, и так далее... Тем не менее не играет роли сколько их и где они располагаются. Важно, что цель одна и схожи принципы работы.
Перем Строка, Параметры; // выделение памяти под переменную
Строка = "Привет"; // выделение памяти для строки.
Строка = "Привет Паша!"; // а также выделение памяти либо изменение размера блока каждый раз при изменении.
Параметры = Новый Структура(); // выделение памяти под структуру
МояФорма = ОткрытьФорму("МояФорма"); // выделение памяти. и на сервере, и на клиенте.
Запрос = Новый Запрос(ТекстЗапроса); // выделение памяти для объекта
Результат = Запрос.Выполнить(); // выделени памяти под данные
ФоновыеЗадания.Выполнить("МоеФоновоеЗадание"); // выделение памяти под новый сеанс, а уже внутри него - все предыдущие случаи.
Освобождение памяти
Программируя на 1С, мы не задумываемся о том, когда нужно освобождать память. Эту функцию берёт на себя платформа. Об этом написано много статей. Вот несколько общих выводов о работе 1С и других платформ с памятью:
- Память освобождается не сразу после выполнения задачи, а позже, для некоторого пула блоков, так как это эффективнее. Например, после закрытия формы, завершения сеанса и фонового задания. Либо периодически запускается «Сборщик мусора». Хочу отметить, что здесь неважно, происходят действия на клиенте (формы) или сервере (фоновые задания и т.д.), поскольку процессы везде аналогичные.
- Платформа 1С (как и любая другая программа) ведёт учёт ссылок на каждый объект, чтобы понимать, что можно освобождать, а что — нет. Освобождение памяти, занимаемой объектом (структурой, массивом, запросом, формой, переменной, прочим), возможно лишь в случае, когда на него больше нет ссылок: удалены, ранее ссылавшиеся на объект переменные перезаписаны другими значениями и т.д.
3, 4 и 5-й выводы я опишу после примеров.
Перейдем к примерам:
// код на форме
Параметры = Новый Структура("Строка", "Паша"); // переменная формы.
ПараметрыПриложения = Параметры; // переменная модуля приложения.
В коде выше на объект «Структура» имеются две ссылки — в переменных «Параметры» и «ПараметрыПриложения». Соответственно, после закрытия формы (либо позже) при попытке освободить память «Структуры» и переменных освобождается память, занимаемая данными формы и переменной «Параметры». После закрытия формы они больше не требуются. Но на «Структуру» при этом остается ссылка в переменной «ПараметрыПриложения». Освободить занимаемую ею память можно будет позже, когда мы присвоим переменной другое значение или завершим сеанс программы целиком.
Рассмотрим еще один пример:
// Циклические ссылки
Листинг = Новый Массив();
Данные = Новый Структура();
Данные.Вставить("Листинг", Листинг);
Листинг.Добавить(Данные);
Здесь, на первый взгляд, ничего «криминального» нет. На переменные «Листинг» и «Данные» ссылаются по одному разу. Они также ссылаются друг на друга. Если обе переменные являются переменными формы, то, казалось бы, при закрытии формы могут быть удалены. Возможно, в продвинутых менеджерах памяти так и происходит, но в общем случае нельзя удалить переменную «Листинг», пока существует «Данные», как и нельзя удалить «Данные», пока на неё ссылается «Листинг». Мы наблюдаем замкнутый круг под названием «Циклические ссылки». Обе переменные в итоге не будут удалены до окончания работы сеанса программы, соответственно, не будет освобождена занимаемая ими память. На сервере, да и где бы то ни было, ситуация аналогичная: циклические ссылки на локальные переменные в любой процедуре не будут удалены после выхода из процедуры.
Циклические ссылки не обязательно парные. Например, можно присвоить «Структуру» самой себе:
Данные = Новый Структура();
Данные.Вставить("Корень", Данные);
С точки зрения выполнения кода никаких проблем нет, но освободить память из под такой структуры будет непросто.
Данные = Неопределено; // не получится, т.к. есть ещё ссылка.
К самой структуре уже доступа нет, а ссылка осталась.
Конечно, можно написать:
Данные.Удалить("Корень"); // удалится ссылка
Данные = Неопределено; // можно будет освободить память.
Но кто это будет делать? И кто вообще об этом вспомнит? Обычно, циклические ссылки незаметно появляются при работе с данными: в массивах, структурах, соответствиях и т.д. Их часто можно наблюдать в многоуровневых структурах, больших массивах. Например, это происходит, когда мы читаем пользователей из справочника и преобразовываем данные в структуру. Скажем, у нас есть «Руководитель» со списком подчинённых, который в структуре «замкнётся».
Один из самых некрасивых примеров циклической ссылки — присвоение переменной формы значение самой формы. А ещё хуже — передать её в другую форму.
ФормаОбработки = ЭтотОбъект;
Начиная с момента, описанного в примере выше, занимаемая формой память не может быть освобождена после закрытия формы, так как на неё ссылается переменная «ФормаОбработки».
Теперь вернёмся к выводам про освобождение памяти:
- Часть блоков, которые были выделены под одно общее дело (вызов процедуры, открытие формы и т.д.), не могут быть освобождены вместе с остальными. Но они могут быть освобождены позднее, например, после завершения сеанса.
- Запросы на выделение памяти происходят постоянно и сильно мешают процессу «сборки мусора», который не отличается быстротой. Отсюда следует пятый вывод.
- Менеджер памяти часто использует тактику отложенного анализа и освобождения блоков. Он выжидает, когда активность запросов на выделение новой памяти дойдет до нуля. В этот момент запустится анализ или «Сборщик мусора». Если в процессе появится запрос на выделение памяти, проведённая работа будет «выброшена», а менеджер станет ждать следующего подходящего случая.
Итоги. Что происходит на практике
Давайте подведем итоги. Мы выяснили, что из большого облака ресурсов постоянно выделяются маленькие блоки памяти. Часть из них освобождается, а часть — нет. Какое-то время они остаются «заблокированными». При этом что-то освобождается позже, а что-то — никогда. Проблема в том, что это мешает выделению новых блоков: каждый раз системе требуется анализировать таблицу занятых блоков и искать пространство, длина которого не меньше запрашиваемого. Когда такого «мусора» в памяти становится слишком много, поиск свободного подходящего места замедляет работу программы и сервера. При этом места заданной длины может просто не найтись, хоть и по общему показателю будет ещё очень много свободной памяти.
Здесь можно найти сходство с фрагментацией жесткого диска, но есть принципиальные отличия — файл на диске можно порезать на кусочки и положить в разные места, а с оперативной памятью так поступить не получится, поскольку нужен свободный кусок заданной длины.
Итак, пришло время раскрыть интригу
Я показал примеры кода из конфигурации, но то же самое справедливо и для самой платформы 1С. Поскольку система занимается обслуживанием запросов по rest-интерфейсу, часть выделенной ранее памяти не может быть освобождена в нужный момент и остаётся висеть посередине предоставленного ресурса.
После завершения сеансов менеджер памяти на платформе (а может и в операционной системе, что по большому счету неважно) делает паузу, выжидая снижения активности программы, а потом запускает длительный процесс — «сборщик мусора» для освобождения уже ненужных блоков памяти и реорганизации таблицы выделенных блоков памяти. Следующее соединение (rest-запрос) вынуждает отложить работу на потом. В нашем кейсе запросы приходили каждые 3–5 минут. В итоге «мусора» посреди большого ресурса становится всё больше, что и приводит к зависанию сервера.
Почему на prod такой проблемы нет? Там подобные запросы происходят не по расписанию, а по событию — определённым действиям на сайте. Интервал между событиями может быть как нулевой, то есть они идут друг за другом, так и больше, но «сборщик мусора», как правило, успевает нормально работать.
Все вышеизложенные выводы в статье сделаны на модели работы менеджера памяти в режиме «черного ящика».
Косвенным подтверждением модели может выступить обычное наблюдение за монитором ресурсов: с течением времени, после завершения работы программы или сеанса, показатели выделенной памяти у процессов rphost меняются. Медленно начинают освобождаться блоки, уходу которых мешали ссылки. Это может занимать как секунды, так и минуты. Затем процесс освобождения ускоряется, уходят большие блоки, иногда лавинообразно.
Раньше, лет 10–20 назад, может кто-то вспомнит, после закрытия программы некоторое время «шуршал» диск винчестера. Это был именно процесс освобождения памяти. У меня есть точное подтверждение этому, так как в то время я занимался разработкой на C++, и запуск программ под отладчиком четко показывал корреляцию между длительностью «шуршания» и количеством (не от объёма) выделенных блоков памяти.
Коллеги по работе в итоге мне не поверили (или не поняли). А вы верите? :)