Как ускорить выгрузку большого количества мелких файлов через FTP в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При интеграции 1С с внешними системами (например, сайтами или сторонними базами данных) разработчики часто сталкиваются с необходимостью выгрузки больших массивов информации. Представьте ситуацию: у вас есть стек из 60 000 документов, каждый из которых весит всего от 1 до 6 килобайт. Однако при попытке отправить их через стандартный встроенный механизм FTPСоединение вы обнаруживаете, что один файл загружается в среднем от 1.5 до 2 секунд. В итоге простая задача растягивается на 25–30 часов непрерывной работы процессора!

В этой статье мы подробно проанализируем, почему встроенный FTP-клиент 1С работает медленно при отправке множества мелких файлов, и разберем по шагам несколько эффективных способов кардинального ускорения этого процесса: от многопоточности до использования внешних утилит и изменения архитектуры обмена.

Выясняем причину: почему стандартное FTPСоединение работает медленно?

По замеру производительности в 1С основное время (до 99%) уходит на выполнение метода Записать() объекта FTPСоединение. Передача файла размером в пару килобайт длится целую секунду или больше по следующим причинам:

  1. Постоянное переподключение (Handshake Overhead): При вызове метода Записать() для каждого нового файла платформа на уровне сокетов инициирует новое подключение к каналу данных (особенно в пассивном режиме ПассивныйРежим = Истина). Для файлов размером 1–6 КБ время, затрачиваемое на установку TCP-соединения, согласование портов и авторизацию, в сотни раз превышает время передачи самого тела файла.
  2. Исчерпание портов (TCP Port Exhaustion) и состояние TIME_WAIT: При попытке передать десятки тысяч файлов последовательно операционная система (как на стороне сервера 1С, так и на FTP-сервере) быстро расходует доступный пул динамических портов. Закрытые сокеты переходят в состояние TIME_WAIT (обычно на 2–4 минуты). Из-за этого ОС начинает искусственно задерживать открытие новых соединений, что и приводит к задержкам на ровном месте.
  3. Влияние сетевого пинга (Latency): Протокол FTP требует обилия служебных команд (проверить каталог, создать, войти, открыть канал данных, записать, закрыть). Если пинг до FTP-сервера составляет хотя бы 50–100 мс, то суммарное время ожидания ответов от сервера на один файл легко вырастает до 1.5–2 секунд.

Рассмотрим подробнее четыре практических решения, которые помогут решить эту проблему.

Решение 1. Реализация многопоточности в 1С

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

Вместо того чтобы последовательно отправлять 60 000 файлов в одном сеансе, мы можем разбить этот массив данных на порции (например, по 5 000 или 6 000 файлов) и запустить их обработку параллельно в 10 или более потоков. Автор темы на практике подтвердил, что запуск выгрузки в 10+ потоков полностью решает проблему скорости.

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


Процедура РаспределитьВыгрузкуПоПотокам(ТаблицаВыгрузки) Экспорт
    
    КоличествоПотоков = 10;
    ВсегоЗаписей = ТаблицаВыгрузки.Количество();
    РазмерПорции = Цел(ВсегоЗаписей / КоличествоПотоков) + 1;
    
    МассивЗаданий = Новый Массив;
    
    Для НомерПотока = 1 По КоличествоПотоков Цикл
        
        НачальныйИндекс = (НомерПотока - 1) * РазмерПорции;
        Если НачальныйИндекс >= ВсегоЗаписей Тогда
            Прервать;
        КонецЕсли;
        
        КонечныйИндекс = Мин(НачальныйИндекс + РазмерПорции - 1, ВсегоЗаписей - 1);
        
        // Формируем порцию данных для текущего потока
        ПорцияДанных = ТаблицаВыгрузки.СкопироватьКолонки();
        Для Индекс = НачальныйИндекс По КонечныйИндекс Цикл
            ЗаполнитьЗначенияСвойств(ПорцияДанных.Добавить(), ТаблицаВыгрузки[Индекс]);
        КонецЦикла;
        
        ПараметрыЗапуска = Новый Массив;
        ПараметрыЗапуска.Добавить(ПорцияДанных);
        
        // Запуск фонового задания
        ИмяПотока = "Выгрузка_Файлов_Поток_" + НомерПотока;
        ФоновоеЗадание = ФоновыеЗадания.Выполнить("МодульОбменаFTP.ВыгрузитьПорциюНаФТП", ПараметрыЗапуска, , ИмяПотока);
        МассивЗаданий.Добавить(ФоновоеЗадание);
        
    КонецЦикла;
    
    // Ожидаем завершения работы всех запущенных потоков
    Попытка
        ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий);
        ЗаписьЖурналаРегистрации("FTP_Обмен", УровеньЖурналаРегистрации.Информация, , , "Все потоки успешно завершили выгрузку.");
    Исключение
        ЗаписьЖурналаРегистрации("FTP_Обмен", УровеньЖурналаРегистрации.Ошибка, , , "Ошибка выполнения потоков: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

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


Процедура ВыгрузитьПорциюНаФТП(ПорцияДанных) Экспорт
    
    // Инициализируем FTP-соединение внутри потока
    ФТП = Новый FTPСоединение("х.х.х.х", 21, "логин", "пароль", , Истина);
    ПредыдущийКаталог = "";
    
    Для Каждого Строка Из ПорцияДанных Цикл
        
        // Оптимизируем смену каталога, чтобы не выполнять лишние запросы к FTP
        Если Строка.ТекущийКаталогФТП <> ПредыдущийКаталог Тогда
            м = ФТП.НайтиФайлы(Строка.ТекущийКаталогФТП);
            Если м.Количество() = 0 Тогда
                ФТП.СоздатьКаталог(Строка.ТекущийКаталогФТП);    
            КонецЕсли;
            ФТП.УстановитьТекущийКаталог(Строка.ТекущийКаталогФТП);
        КонецЕсли;
        
        Попытка
            ФТП.Записать(Строка.ПутьКФайлу + "\" + Строка.ИмяФайла, Строка.ИмяФайла);
        Исключение
            ЗаписьЖурналаРегистрации("FTP_Обмен_Поток", УровеньЖурналаРегистрации.Предупреждение, , , 
                "Не удалось выгрузить файл " + Строка.ИмяФайла + ": " + ОписаниеОшибки());
        КонецПопытки;
        
        ПредыдущийКаталог = Строка.ТекущийКаталогФТП;
        
    КонецЦикла; 
    
    ФТП = Неопределено;
    
КонецПроцедуры

Решение 2. Оптимизация архитектуры обмена (Архивация данных)

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

Передача одного архива размером 10 МБ займет долю секунды, тогда как последовательная передача 10 000 файлов по 1 КБ может занять несколько часов. На стороне получателя архив распаковывается автоматическим скриптом или принимающим веб-сервером.

Разберем пример создания и отправки ZIP-архива средствами 1С:


Процедура СформироватьИОтправитьАрхив(ТаблицаВыгрузки, ИмяФайлаАрхива) Экспорт
    
    ПутьКАрхиву = КаталогВременныхФайлов() + ИмяФайлаАрхива;
    Архив = Новый ЗаписьZipФайла(ПутьКАрхиву, , , МетодСжатияZIP.Сжатие, УровеньСжатияZIP.Оптимальный);
    
    Для Каждого Строка Из ТаблицаВыгрузки Цикл
        ПолныйПутьКФайлу = Строка.ПутьКФайлу + "\" + Строка.ИмяФайла;
        ФайлНаДиске = Новый Файл(ПолныйПутьКФайлу);
        Если ФайлНаДиске.Существует() Тогда
            // Сохраняем файлы в архиве, сохраняя при необходимости структуру путей
            Архив.Добавить(ПолныйПутьКФайлу, РежимСохраненияПутейZIP.СохранятьОтносительныеПути);
        КонецЕсли;
    КонецЦикла;
    
    Архив.Записать();
    
    // Отправляем один ZIP-файл вместо тысяч мелких файлов
    ФТП = Новый FTPСоединение("х.х.х.х", 21, "логин", "пароль", , Истина);
    ФТП.Записать(ПутьКАрхиву, ИмяФайлаАрхива);
    ФТП = Неопределено;
    
    // Удаляем временный файл архива
    Попытка
        УдалитьФайлы(ПутьКАрхиву);
    Исключение
        // Логируем возможные проблемы очистки временной папки
    КонецПопытки;
    
КонецПроцедуры

Решение 3. Делегирование транспорта внешним утилитам (cURL, WinSCP)

Если многопоточность внутри 1С по каким-то причинам нежелательна или перегружает рабочий процесс сервера, лучшим решением становится использование специализированного стороннего софта, оптимизированного для скоростной передачи данных.

Использование утилиты cURL:

Консольный клиент curl.exe умеет держать TCP-соединение открытым в рамках одной сессии и передавать файлы пачкой без постоянной авторизации. Вы можете программно сформировать список файлов и запустить выполнение curl одной командой операционной системы через метод ЗапуститьПриложение().

Использование библиотеки WinSCP через COM:

WinSCP предоставляет мощный программный интерфейс для работы с файловыми серверами. Подключив .NET сборку WinSCP как COMОбъект("WinSCP.Session"), мы получаем возможность использовать высокооптимизированные методы, например, PutFiles или SynchronizeDirectories. WinSCP автоматически обрабатывает сетевые сбои, поддерживает режим дозаписи и работает в разы быстрее штатного механизма 1С.

Рассмотрим пример синхронизации каталога через WinSCP:


Процедура СинхронизироватьПапкуЧерезWinSCP(ЛокальныйКаталог, УдаленныйКаталог) Экспорт
    
    Попытка
        // Настройка параметров подключения
        ПараметрыСессии = Новый COMОбъект("WinSCP.SessionOptions");
        ПараметрыСессии.Protocol = 1; // Указываем протокол FTP
        ПараметрыСессии.HostName = "х.х.х.х";
        ПараметрыСессии.UserName = "логин";
        ПараметрыСессии.Password = "пароль";
        ПараметрыСессии.PortNumber = 21;
        
        Сессия = Новый COMОбъект("WinSCP.Session");
        Сессия.Open(ПараметрыСессии);
        
        ПараметрыПередачи = Новый COMОбъект("WinSCP.TransferOptions");
        ПараметрыПередачи.TransferMode = 0; // Двоичный режим передачи (Binary)
        
        // Передаем все новые и изменившиеся файлы из локальной папки в удаленную
        РезультатПередачи = Сессия.PutFiles(ЛокальныйКаталог + "\*", УдаленныйКаталог, Ложь, ПараметрыПередачи);
        
        // Проверяем наличие ошибок в процессе передачи
        РезультатПередачи.Check();
        
        // Закрываем и освобождаем сессию
        Сессия.Dispose();
        
    Исключение
        ЗаписьЖурналаРегистрации("WinSCP_Обмен", УровеньЖурналаРегистрации.Ошибка, , , 
            "Ошибка обмена через WinSCP: " + ОписаниеОшибки());
    КонецПопытки;
    
КонецПроцедуры

Решение 4. Переход на протокол SFTP или HTTP REST API

Сетевые архитекторы рекомендуют уходить от классического протокола FTP при передаче больших массивов данных в силу его технической устарелости.

Резюме и практические рекомендации

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

  1. Если у вас нет возможности изменить принимающую сторону и файлы должны лежать на FTP "как есть" — используйте многопоточность в 1С (Решение 1). Распределение задач по 10 фоновым заданиям ускорит передачу примерно в 10 раз.
  2. Если принимающая сторона может распаковать архив — обязательно используйте пакетную отправку через ZIP-архивацию (Решение 2). Это самый надежный метод, который снизит время обмена с нескольких часов до пары секунд и сбережет ресурсы сети.
  3. Если стандартное FTPСоединение в 1С ведет себя нестабильно при пиковых нагрузках — делегируйте эту задачу специализированной утилите WinSCP (Решение 3).
← На главную