При выгрузке больших объемов данных из среды 1С в сторонние системы (например, через SFTP-серверы или веб-сервисы) разработчики часто сталкиваются с жесткими ограничениями на размер принимаемого файла. Распространенный лимит — 4 МБ на один файл. Если объем выгрузки составляет сотни тысяч строк, перед нами встает задача: как разделить выгрузку на части, не превышая лимит, и при этом не потерять в производительности?
В этой статье мы подробно разберем, почему классические методы измерения размера XML-файла могут приводить к аварийному завершению работы системы, и рассмотрим современные, высокотехнологичные способы решения этой задачи в платформе «1С:Предприятие».
На первый взгляд, самое простое решение задачи выглядит так, как предложено в одном из сообщений форума:
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.УстановитьСтроку();
ФабрикаXDTO.ЗаписатьXML(ЗаписьXML, ОбъектXDTO);
СтрокаXML = ЗаписьXML.Закрыть();
РазмерВБайтах = СтрДлина(СтрокаXML); // Примерный расчет
Давайте проанализируем, почему этот подход абсолютно неприменим на реальных рабочих базах при больших объемах данных (например, при выгрузке 400 000 строк товаров). Метод ЗаписьXML.УстановитьСтроку() заставляет платформу формировать все XML-представление непосредственно в оперативной памяти компьютера.
Платформа 1С выделяет память под текстовую строку непрерывным блоком. При постоянном росте и перевыделении памяти под гигантскую XML-строку происходит сильная фрагментация оперативной памяти. В результате рабочий процесс сервера (или клиентское приложение) очень быстро исчерпывает доступный адресный пул и аварийно завершается с ошибкой «Недостаточно памяти» (Out of memory / bad allocation).
Кроме того, если мы будем постоянно перезаписывать и проверять такую строку в цикле, производительность выгрузки упадет в десятки раз.
Наиболее современным и технологичным решением для динамического контроля размера данных является использование механизмов потоков. В платформе «1С:Предприятие», начиная с версии 8.3.9, появились объекты ПотокВПамяти и ФайловыеПотоки. Они позволяют писать данные порциями и проверять размер «на лету», не закрывая сам объект записи.
Рассмотрим, как это реализовать на практике по шагам:
ЗаписьXML через этот поток с помощью метода ОткрытьПоток().Поток.Размер или Поток.Позиция.Посмотрим на пример кода, реализующий этот алгоритм:
Поток = Новый ПотокВПамяти();
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.ОткрытьПоток(Поток, "UTF-8");
// Начинаем запись XML-документа
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("ДанныеВыгрузки");
// Предположим, у нас есть массив ОбъектовXDTO (микро-объектов)
Для Каждого ТекущийОбъектXDTO Из МассивОбъектовXDTO Цикл
// Записываем очередной объект в поток
ФабрикаXDTO.ЗаписатьXML(ЗаписьXML, ТекущийОбъектXDTO);
// Сбрасываем буфер записи в поток, чтобы получить точный размер
ЗаписьXML.Адаптировать();
// Проверяем текущий размер потока в байтах (4 МБ = 4 194 304 байт)
Если Поток.Размер > 4000000 Тогда // Оставляем небольшой запас
// Закрываем текущую XML-структуру
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.Закрыть();
// Получаем двоичные данные из потока для отправки
ДвоичныеДанныеФайла = Поток.ЗакрытьИПолучитьДвоичныеДанные();
ОтправитьФайлНаSFTP(ДвоичныеДанныеФайла);
// Инициализируем новый поток для следующей порции данных
Поток = Новый ПотокВПамяти();
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.ОткрытьПоток(Поток, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("ДанныеВыгрузки");
КонецЕсли;
КонецЦикла;
// Закрываем оставшиеся теги и отправляем последний файл
Если ЗаписьXML.ТекущееСостояние() <> СостояниеЗаписиXML.Закрыт Тогда
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.Закрыть();
ДвоичныеДанныеФайла = Поток.ЗакрытьИПолучитьДвоичныеДанные();
ОтправитьФайлНаSFTP(ДвоичныеДанныеФайла);
КонецЕсли;
Создавать в оперативной памяти один монолитный ОбъектXDTO, содержащий сотни тысяч вложенных строк, — это грубая архитектурная ошибка. Объекты XDTO очень «тяжелые» для RAM, так как хранят множество метаданных о своей структуре и типах.
Правильный архитектурный подход заключается в совместном использовании низкоуровневой быстрой последовательной записи через ЗаписьXML и небольших, быстро уничтожаемых «микро-объектов» XDTO. Выгрузка при этом производится напрямую в файл на диске, размер которого мы можем мгновенно контролировать средствами операционной системы.
Выясним, как работает этот алгоритм:
ИмяФайла = ПолучитьИмяВременногоФайла("xml");
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.ОткрытьФайл(ИмяФайла, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("RootElement");
ВыборкаДанных = СформироватьЗапросДляВыгрузки();
Пока ВыборкаДанных.Следующий() Цикл
// Создаем микро-объект XDTO только для ОДНОЙ текущей строки данных
МикроОбъектXDTO = СоздатьМикроОбъектXDTO(ВыборкаДанных);
// Записываем его в файл
ФабрикаXDTO.ЗаписатьXML(ЗаписьXML, МикроОбъектXDTO);
// Принудительно выталкиваем данные из буфера 1С на диск
ЗаписьXML.Адаптировать();
// Проверяем физический размер файла на диске
ФайлНаДиске = Новый Файл(ИмяФайла);
РазмерФайла = ФайлНаДиске.Размер(); // Работает мгновенно, кэшируется ОС
Если РазмерФайла > 3900000 Тогда // Лимит 3.9 МБ с запасом
// Корректно закрываем XML-структуру текущего файла
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.Закрыть();
// Отправляем готовый файл на SFTP
ПередатьФайлНаСервер(ИмяФайла);
// Открываем новый файл для следующей порции
ИмяФайла = ПолучитьИмяВременногоФайла("xml");
ЗаписьXML = Новый ЗаписьXML();
ЗаписьXML.ОткрытьФайл(ИмяФайла, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("RootElement");
КонецЕсли;
КонецЦикла;
// Закрываем финальный файл, если в нем есть данные
Если ЗаписьXML.ТекущееСостояние() <> СостояниеЗаписиXML.Закрыт Тогда
ЗаписьXML.ЗаписатьКонецЭлемента();
ЗаписьXML.Закрыть();
ПередатьФайлНаСервер(ИмяФайла);
КонецЕсли;
Почему это работает быстро? Операционная система оптимизирует дисковые операции ввода-вывода (кэширует запись). Метод ЗаписьXML.Адаптировать() гарантирует, что данные будут сброшены из буфера платформы в буфер ОС, что позволит объекту Файл вернуть актуальный размер без закрытия дескриптора файла.
Если структура выгружаемого XML-документа строго регламентирована и известна заранее (жестко заданы имена тегов и атрибутов), мы можем использовать метод математического прогнозирования размера. Этот способ работает быстрее всех остальных, так как полностью исключает промежуточную сериализацию в память или на диск до момента принятия решения о записи.
При расчете мы должны учитывать особенности кодировки UTF-8, которая чаще всего используется по умолчанию в XML:
Разберем математический алгоритм расчета размера одной строки выгрузки:
Функция ВычислитьПримерныйРазмерСтрокиВБайтах(ДанныеСтроки) Экспорт
// 1. Длина постоянных XML-тегов структуры (константа для нашей схемы)
// Например, для структуры "<Row ID="" Name=""><Param/></Row>"
КонстантныйРазмерТегов = 25;
// 2. Вычисляем длину динамических данных с учетом кодировки UTF-8
ДлинаID = СтрДлина(Строка(ДанныеСтроки.Идентификатор)); // Латиница/цифры = 1 байт
// Кириллица занимает 2 байта, поэтому умножаем длину на 2
ДлинаНаименования = СтрДлина(ДанныеСтроки.Наименование) * 2;
РазмерСтроки = КонстантныйРазмерТегов + ДлинаID + ДлинаНаименования;
Возврат РазмерСтроки;
КонецФункции
В основном цикле обхода выборки мы просто накапливаем виртуальный счетчик размера. Как только расчетный размер приближается к 3.5 МБ, мы останавливаем выборку, физически формируем файл XML из накопленного пакета данных и переходим к следующему файлу. Погрешность данного метода на практике составляет менее 1-2%, что позволяет гарантированно укладываться в лимиты без лишних накладных расходов.
Для решения задачи контроля размера XML-файла мы рекомендуем отказаться от устаревших подходов с выгрузкой в длинную строку. Вместо этого используйте:
ПотокВПамяти) — если необходимо формировать данные динамически и выгружать их без промежуточного сохранения на жесткий диск сервера.