Как мы анализируем потребление памяти приложением 1С... с трудом
Полевой разбор дампа rphost: как вытаскивать BSL-фрагменты, запросы, XML/base64 и грубую статистику по UID коллекций 1С без полноценного анализатора аллокаций.
В блоге 1С есть отличная статья «Как мы анализируем потребление памяти нативными приложениями». Там описан взрослый инженерный подход: дампы памяти, аллокации, типы объектов, связи между ними, Windows/Linux, heap, символы, графы и дальнейший анализ данных. То есть полноценный инструмент для понимания того, что именно лежит в памяти нативного приложения.
Есть и другой, более полевой жанр расследований: сначала кажется, что мы просто смотрим железо, виртуализацию, CPU, память, технологический журнал, Process Explorer и отладчик, а потом внезапно утыкаемся в memcpy, memmove или конкретную библиотеку. Иногда результат получается очень практичным, но путь к нему совсем не похож на аккуратную лабораторную работу.
Мы решили пойти куда более скромным путём. Не писать анализатор аллокаций. Не восстанавливать все типы объектов платформы. Не строить граф связей между структурами памяти. А просто ответить на приземлённый вопрос: если процесс rphost потребляет много памяти, можно ли из дампа вытащить хоть что-то понятное человеку?
Спойлер: можно. Но с трудом.
Откуда вообще брать данные
Когда хочется понять, на что тратится память сервера 1С, первый нормальный путь — это не дамп. Сначала нужно смотреть штатные источники: технологический журнал, события CALL, длительность вызовов, Memory, MemoryPeak, контекст выполнения, серверные вызовы, фоновые задания, запросы и потребление памяти по процессам.
Например, CALL в ТЖ можно агрегировать скриптом уровня `call_get_statistics.sh`: сгруппировать по Context, посчитать суммарную длительность, среднюю длительность, максимум и MemoryPeak. Это отвечает на вопрос: какой контекст больше всего потребляет память или дольше всего работает?
В приложенном варианте скрипт ожидает, что рядом с ним лежат каталоги вида `rphost_*`, а внутри — файлы технологического журнала. Он берет все строки `CALL`, где есть `Context=`, вытаскивает имя базы из `p:processName`, затем поля `Memory`, `MemoryPeak`, `InBytes`, `OutBytes` и `CpuTime`. После этого группирует данные по паре «база | контекст».
chmod +x call-get-statistics.sh
./call-get-statistics.shНа выходе появляются пять файлов: `GLOBAL_DB_Top_CpuTime.txt`, `GLOBAL_DB_Top_Memory.txt`, `GLOBAL_DB_Top_MemoryPeak.txt`, `GLOBAL_DB_Top_InBytes.txt` и `GLOBAL_DB_Top_OutBytes.txt`. В каждой строке формат одинаковый: сумма, среднее, количество вызовов, максимум и затем ключ группировки. Обычно первым делом я смотрю `MemoryPeak` и `CpuTime`: они помогают выбрать контекст, для которого уже имеет смысл открывать дамп.
Но иногда этого мало. Мы видим, что rphost вырос до десятков гигабайт. ТЖ показывает подозрительные вызовы. И всё равно хочется понять, что реально было внутри памяти. Тогда появляется соблазн взять дамп процесса.
procdump -ma rphost.exe rphost_full.dmpДамп — это не текстовый файл
Первая ошибка — открыть `.dmp` как текст и ждать, что там будет аккуратно написано `Процедура Команда1НаСервере()`. Не будет. Внутри будет смесь бинарных структур, UTF-16LE строк, UTF-8 строк, строк в неправильной кодировке, base64-вставок, XML, служебных структур платформы и фрагментов, которые похожи на код, но начинаются с середины.
Например, нормальное слово `Пока` после неправильного декодирования может выглядеть как `РџРѕРєР°`. А фрагмент UTF-16LE в сыром виде превращается в почти нечитаемый набор байтов. Значит, задача уже не «прочитать файл», а найти текстоподобные участки, попробовать несколько декодировок, оценить похожесть на 1С-код и сохранить максимум материала для повторного анализа.
Что мы хотели получить
Минимальная цель была такая: прочитать `rphost.exe_16460.dmp`, найти длинные текстоподобные участки, отдельно искать raw-строки, UTF-16LE, mojibake, base64, XML, BSL и запросы 1С. Важно было сохранить всё найденное, составить рейтинг интересных фрагментов и не потерять обрезанные куски без начала `Процедура`.
Последняя часть оказалась особенно важной. В дампе часто встречается не полный модуль, а фрагмент вроде `РезультатЗапроса = Запрос.Выполнить();`, затем `ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();`, затем цикл `Пока`, и только потом `КонецПроцедуры`. Если такой кусок выбросить, можно потерять самую интересную зацепку.
Первая и вторая попытка
Первая версия скрипта уже делала разумные вещи: искала raw byte runs, искала UTF-16LE runs, пыталась чинить cp1251 mojibake, считала score по ключевым словам 1С, сохраняла summary и отдельно base64 hits. Это работало лучше, чем обычный `strings`, но был риск выбрать «интересные» куски слишком рано.
Вторая версия пошла в другую сторону: сначала тащить почти всё. Она извлекала UTF-16 и raw-строки, считала score, сохраняла `all_strings.txt`, `top_strings.txt`, `code_blocks.txt`. Идея правильная: в дампе лучше сначала сохранить максимум, а потом фильтровать. Но понадобились offset-ы, manifest, base64-декодирование, сохранение обрезанных процедур и анализ плотностей.
Отдельная идея: искать плотности
Потом появилась ещё одна полезная мысль: смотреть не только строки, но и регионы дампа. Скрипт делит дамп на чанки по 1 МБ и считает плотность UTF-16-подобного текста, плотность ненулевых байтов, энтропию, самые интересные регионы и самые длинные строки внутри региона. Это разведка: где вообще лежит что-то похожее на данные, а где просто бинарная масса.
Итоговый подход
Нормальная схема получилась такой: не доверять одной кодировке, читать один и тот же фрагмент как utf-8, cp1251, latin1 и utf-16le, потом пробовать чинить mojibake. После этого считать похожесть на 1С по словам `Процедура`, `Функция`, `Запрос`, `ВЫБРАТЬ`, `Регистр`, `Документ`, `Справочник`, `ПараметрыСеанса`, `Соответствие`, `ТаблицаЗначений` и по доле русских букв.
Это не точный парсер языка 1С, а эвристика. Но для дампа эвристика — нормальный рабочий инструмент: вход всё равно повреждённый, фрагментарный и неоднородный. Главное правило — не выбрасывать данные только потому, что они сейчас выглядят некрасиво.
Что выдают скрипты
Основная папка результата — `decoded_1c_dump_full`. Внутри есть `00_manifest.jsonl` со всеми найденными фрагментами и offset-ами, `01_summary.txt` с краткой сводкой, `02_all.txt` для ручного поиска, `03_procedures.txt` с попыткой собрать процедуры и функции, а также `04_base64.txt` с декодированными base64-вставками.
Что реально удалось увидеть
Из тестового фрагмента удалось восстановить не абстрактный шум, а узнаваемый код 1С: выполнение запроса, выборку детальных записей, создание `Соответствие`, цикл по выборке, затем `ФиксированноеСоответствие` и запись в `ПараметрыСеанса.VeryBigTable`. Это не доказательство утечки, но уже хорошая зацепка.
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
т = Новый Соответствие;
сч = 1;
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
т.Вставить(сч, ВыборкаДетальныеЗаписи.Период);
сч = сч + 1;
КонецЦикла;
м = Новый ФиксированноеСоответствие(т);
ПараметрыСеанса.VeryBigTable = м;Попытка посчитать объекты по UID
Отдельно я добавил более низкоуровневый эксперимент: разобрал minidump, получил memory ranges и стал искать UID известных коллекций 1С в ASCII, UTF-16LE и binary little-endian представлении. В дампе оказалось около 1163.1 МБ захваченной памяти.
По выбранным типам найдено 2196 совпадений, из них 472 — binary little-endian UID. Именно они наиболее интересны для грубой привязки к объектам в памяти. Текстовые совпадения я оставил, но отношусь к ним осторожно: часть из них может быть строками, метаданными или повторяющимися идентификаторами.
| Тип 1С | Binary UID | Все совпадения | Оценка участка, МБ |
|---|---|---|---|
| Массив | 64 | 64 | 8.691 |
| ФиксированныйМассив | 61 | 61 | 3.850 |
| ТаблицаЗначений | 60 | 120 | 5.658 |
| Структура | 55 | 1548 | 0.776 |
| СписокЗначений | 55 | 203 | 8.092 |
| Соответствие | 50 | 50 | 5.507 |
| ФиксированнаяСтруктура | 43 | 44 | 3.943 |
| ФиксированноеСоответствие | 43 | 43 | 2.680 |
| ДеревоЗначений | 41 | 63 | 2.704 |
Здесь важно не обманывать себя: это не точные байты объектов. Чтобы сказать «память занята вот такими объектами на столько-то мегабайт», нужно знать границы аллокаций и внутренний формат объектов платформы. Моя колонка `Оценка участка` — эвристика: расстояние от найденного binary UID до следующего такого маркера в том же memory range, ограниченное одним мегабайтом на находку.
Чем это отличается от инструмента 1С
В статье 1С описан системный анализ памяти: адреса аллокаций, размеры, типы объектов, связи между ними, символы, heap, Windows/Linux, PostgreSQL и графы связей. Наш подход гораздо проще. Мы не знаем точный C++-тип объекта, размер конкретной аллокации, кто на кого ссылается и какой объект удерживает память.
Мы знаем другое: в дампе есть текстовые следы, часть этих следов можно восстановить до читаемого 1С-кода, а UID-маркеры помогают наметить области, где встречаются коллекции 1С. Это не замена анализатору памяти. Это инструмент разведки.
Зачем это вообще нужно
Практический сценарий такой: видим рост памяти rphost, смотрим ТЖ, находим подозрительные CALL с большим MemoryPeak, снимаем дамп, прогоняем скрипт, ищем `ПараметрыСеанса`, `ТаблицаЗначений`, `Соответствие`, `ФиксированноеСоответствие`, большие запросы, временные таблицы, регистры и конкретные объекты метаданных.
Рабочая цепочка получается такой: ТЖ → контекст → дамп → восстановленный фрагмент → гипотеза → проверка в конфигурации.
Ограничения
У такого подхода есть понятные ограничения. Скрипты могут не восстановить процедуру целиком, найти кусок без начала, принять мусор за полезный текст, продублировать один и тот же фрагмент в разных декодировках или не найти текст, если он лежит в сжатом, зашифрованном или нестандартном виде. Но это всё равно лучше, чем открыть дамп в редакторе и смотреть на `РџРѕРєР° Выборка...`.
Выводы
Дамп rphost можно использовать не только как объект для WinDbg, но и как источник текстовых следов прикладного кода. Для 1С особенно важны UTF-16LE и mojibake вида `РџРѕРєР°`. Нельзя обрабатывать дамп как файл в одной кодировке: это смесь бинарных структур, строк, XML, base64 и случайных фрагментов памяти.
Лучше сначала сохранить максимум, а потом фильтровать. Самые полезные выходные файлы — manifest, summary, all, procedures и base64. Такой скрипт не доказывает утечку памяти, но помогает найти направление расследования. В связке с ТЖ это уже рабочий инструмент: ТЖ показывает, где болит; дамп иногда показывает, что именно там лежало.