Обмен 1С через FTP рвётся на большом пакете: почему он не докачивается и как сделать, чтобы докачивался
Иван Недомолков · Жарияланды:
Типичная картина: у филиала или склада плохой интернет, обмен с центральной базой идёт через FTP, пакет РИБ или обмена БСП весит несколько сотен мегабайт. Связь моргает, и следующая попытка снова везёт пакет с самого начала. Если обрывы случаются чаще, чем пакет успевает доехать, обмен не проходит вообще. Бывает и хуже: пакет доехал обрезанным, и это всплывает только на загрузке, ошибкой в журнале регистрации, которую никто не связывает с транспортом.
Обе беды растут из одного места. Штатный транспорт отправляет файл одним вызовом и сразу под финальным именем, платформа не умеет дописывать файл на FTP со смещения, а приёмная сторона проверяет только, что файл не пустой. Ниже разбор по порядку: как убедиться, что у вас именно это, какие есть выходы и как устроена доставка частями, которую мы сделали и проверили с настоящими обрывами.
Стенд для всех замеров: платформа 8.3.27.1606, УТ 11.5 с БСП 3.1.11, локальный FTP-сервер pyftpdlib. Мегабайт здесь двоичный, 1 048 576 байт.
Признаки, что виноват транспорт
- Обмен с одним узлом стабильно не доходит, а с соседями по хорошему каналу идёт.
- В журнале отправки ошибки вида “Ошибка подключения к FTP-серверу”, и каждая следующая попытка длится столько же, сколько первая: прогресса между попытками нет.
- На FTP в каталоге обмена лежит файл с именем пакета, но меньше, чем у отправителя.
- Загрузка падает на разборе XML или на распаковке архива, хотя выгрузка прошла чисто.
- В своём коде обмена вы ставили FTP-соединению таймаут “чтобы не висело”, и большие пакеты перестали уходить.
Последний пункт стоит разобрать отдельно, потому что он не про обрывы.
Таймаут FTPСоединения считается на всю операцию
Параметр Таймаут в конструкторе FTPСоединение ограничивает операцию целиком, а не время
простоя. Проверка: таймаут 3 секунды, файл, который на ограниченном канале идёт около
десяти секунд. Соединение рвётся ровно на третьей секунде с “Превышен таймаут”, хотя
данные всё это время шли. В штатном транспорте БСП на передачу стоит 12 часов. Если
в самописном обмене вы поставили минуту, крупный пакет на медленном канале не пройдёт
никогда, и выглядеть это будет как обрыв связи.
Что делает штатный FTP-транспорт БСП
Код обработки ТранспортСообщенийОбменаFTP и общего модуля ТранспортСообщенийОбмена
открыт (БСП 3.1.11, лицензия CC BY 4.0). Отправка пакета там такая:
// БСП 3.1.11, Обработка.ТранспортСообщенийОбменаFTP, ВыполнитьКопированиеФайлаНаFTPСервер
Попытка
FTPСоединение.Записать(ИмяФайлаИсточника, КаталогНаСервере + ИмяФайлаПриемника);
Исключение
СообщениеОбОшибке = НСтр("ru = 'Ошибка подключения к FTP-серверу, ...'");
// ...
Возврат Ложь;
КонецПопытки;
Из этого фрагмента следует всё остальное:
| Свойство транспорта | Следствие |
|---|---|
один вызов Записать на весь файл | после ошибки следующая попытка повторяет то же самое с нуля |
| запись сразу в финальное имя | после обрыва на сервере остаётся обрезанный файл под именем настоящего пакета |
| приём берёт самый свежий файл по шаблону имени и пропускает только пустые | огрызок с ненулевым размером и свежей датой уходит в загрузку как целый |
| контрольной суммы нет | несжатый обрезанный XML не отличить от целого до загрузки; сжатый не распакуется, и это случайная защита |
Реквизит МаксимальныйДопустимыйРазмерСообщения пакет на части не режет, на него иногда
надеются зря. Это ограничение: файл крупнее отправка отвергает с текстом “Превышен
допустимый размер сообщения обмена”.
На приёме проверка одна:
// там же, ПолучитьСообщение
ИначеЕсли (ТекущийФайл.Размер() = 0) Тогда
Продолжить;
Почему не дописать файл с места обрыва
Справка не показывает у Записать и Получить параметра смещения. Мы проверили это
вызовом, а не чтением справки:
Попытка
Выполнить("С.Записать(Ф, ""/proba2.txt"", 100)");
Исключение
// "Слишком много фактических параметров"
КонецПопытки;
На 8.3.27 оба метода отвечают “Слишком много фактических параметров”. Третьего параметра
нет, дописать существующий файл на FTP средствами платформы нечем. Следом мы отправили
пакет одним Записать, как это делает БСП, и посреди передачи убили процесс FTP-сервера.
Платформа выдала исключение “Failed sending data to the peer”, а на сервере под именем
пакета остался файл на 46 % от исходного.
Какие есть выходы
| Вариант | Что даёт | Чего не даёт |
|---|---|---|
| Включить сжатие в настройке транспорта | пакет меньше, обрезанный архив не распакуется | после обрыва всё равно с нуля, огрызок лежит под настоящим именем |
| Уменьшить пакет (чаще выгружать, дробить по времени) | меньше потеря на обрыв | ничего не меняет в механике, большой первичный пакет всё равно не пройдёт |
| Внешняя утилита с докачкой | докачка с байта | это уже не 1С: установка на каждом сервере, свой мониторинг |
| Свой транспорт в конфигурации | полный контроль | в БСП 3.1.11 список транспортов зашит в ТранспортСообщенийОбмена и переопределяемую процедуру, нужна правка конфигурации |
| Доставка частями внешней обработкой между каталогами | докачка с первой недоехавшей части, проверка SHA-256 до загрузки, конфигурация не меняется | обрыв стоит одной части, а не нуля байт |
Мы выбрали последний вариант. Дальше о том, как он устроен.
Доставка частями: требования
Раз писать со смещения нельзя, единицей докачки становится файл. Пакет режется на части, каждая едет отдельным файлом и либо дошла целиком, либо повторяется. Чтобы это работало надёжно, нужно закрыть пять вопросов.
1. Как понять, что часть доехала. Размер файла на сервере не годится: у огрызка размер
тоже есть. Поэтому часть пишется во временное имя и переименовывается в финальное только
после того, как Записать вернулась без ошибки:
ИмяВременного = ПолучитьИмяВременногоФайла("part");
Данные.Записать(ИмяВременного);
ПутьЧасти = ПутьНаСервере(Пакет.УдалённыйКаталог, ИмяЧасти(Номер));
Попытка
Соединение.Записать(ИмяВременного, ПутьЧасти + ".tmp");
Исключение
УдалитьФайлы(ИмяВременного);
ВызватьИсключение;
КонецПопытки;
УдалитьФайлы(ИмяВременного);
Соединение.Переместить(ПутьЧасти + ".tmp", ПутьЧасти);
Связь упала между записью и переименованием - на сервере остался .tmp, часть уйдёт
заново. Потеря та же, одна часть.
2. Как резать, не удваивая пакет на диске. У платформы есть РазделитьФайл, она быстрая,
но кладёт рядом вторую копию пакета. Отправителю эта копия не нужна, часть читается
из исходного файла на лету:
Поток = ФайловыеПотоки.ОткрытьДляЧтения(ПолноеИмя);
Поток.Перейти(Смещение, ПозицияВПотоке.Начало);
Буфер = Новый БуферДвоичныхДанных(Длина);
Прочитано = Поток.Прочитать(Буфер, 0, Длина);
Поток.Закрыть();
Данные = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буфер);
3. Как приёмной стороне понять, что пакет готов. У каждого пакета на FTP своя папка:
/obmen/Message_A_B.xml.parts/opis.txt размер, SHA-256 пакета и каждой части (уходит первой)
/obmen/Message_A_B.xml.parts/0001.part части; пишутся в NNNN.part.tmp, потом переименовываются
/obmen/Message_A_B.xml.parts/gotovo.txt SHA-256 пакета (уходит последним)
Пока нет gotovo.txt, получатель пакет не трогает и сообщает, сколько частей из скольких
уже на сервере. После обрыва отправитель читает опись, убеждается по SHA-256, что она от того
же пакета, и шлёт только недостающие части. Если там опись другого пакета, значит обмен
успел выгрузить новый, и старые части удаляются.
Хеши пакета и частей считаются за один проход объектом ХешированиеДанных. SHA-256 для
600 МБ через ДобавитьФайл занимает около пяти секунд, результат совпал до символа
с независимым расчётом в Python. CRC32 втрое быстрее, но 32 бита на файл в сотни
мегабайт мы сочли слабой защитой.
4. Как не отдать загрузке битый пакет. Получатель качает части в рабочую папку и у каждой сверяет SHA-256. Не совпало - качает повторно. Совпало со второго раза, значит сбоила сеть. Не совпало дважды, значит часть испорчена на сервере, и её надо перепослать:
Совпал = Ложь;
Для Заход = 1 По 2 Цикл
УдалитьФайлы(ИмяВременного);
Соединение.Получить(ПутьЧасти, ИмяВременного);
Если ХешФайла(ИмяВременного) = Ожидаемый Тогда
Совпал = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если Не Совпал Тогда
// битую часть убираем с сервера вместе с отметкой готовности:
// следующая отправка дошлёт только её
УдалитьНаСервере(Соединение, ПутьЧасти);
УдалитьНаСервере(Соединение, ПутьНаСервере(Пакет.УдалённыйКаталог, "gotovo.txt"));
КонецЕсли;
Собранный через ОбъединитьФайлы пакет проверяется ещё раз целиком и только при совпадении
переносится в каталог обмена. Рабочая папка лежит внутри этого каталога, а штатный транспорт
“каталог” ищет файлы без подпапок (НайтиФайлы(КаталогОбменаИнформацией, ШаблонИмениСообщения, Ложь)),
поэтому части и полусобранный файл загрузка не видит. Перенос готового пакета это
переименование в пределах одного диска. Места на время сборки нужно около двух размеров пакета.
5. Как не запустить два раза. Отправка стоит по расписанию, администратор нажимает кнопку руками, потому что “что-то долго”. Чтобы две копии не слали одни и те же части, в рабочей папке лежит файл-замок, отдельный для отправки и приёма, и каждый шаг обновляет его время. Вторая копия видит свежий замок и уходит, ничего не трогая. Замок старше 15 минут считается брошенным (например, форму закрыли посреди передачи), следующий запуск его перехватывает и продолжает с первой недоехавшей части. 15 минут взяты с запасом больше таймаута одной FTP-операции.
Какого размера часть
Каждая часть стоит нескольких обменов командами с сервером: подключение, вход, пассивный канал, переименование. На петле 127.0.0.1 это около 25 мс на часть независимо от размера, нижняя граница. На живом канале с пингом 150 мс шесть-семь таких обменов дадут около секунды на часть; это оценка, на живом канале мы её не мерили.
Мелкие части проигрывают на накладных, крупные дорого терять: в среднем обрыв съедает половину части. Минимум суммы двух потерь даёт формулу
оптимальный размер части = корень(2 x размер пакета x накладные на часть x скорость канала / число обрывов)
Для пакета в гигабайт, секунды накладных и пяти обрывов это около 7 МБ на канале 1 Мбит/с и около 23 МБ на 10 Мбит/с. Плохой канал обычно и медленный, поэтому взяли 8 МБ: на 1 Мбит/с такая часть идёт минуту с небольшим, это худшее, что теряется при обрыве. Размер записывается в опись, получатель берёт его оттуда. Граница у такого выбора есть: таймаут операции 10 минут, значит на канале медленнее 14 КБ/с часть в 8 МБ не пройдёт.
Как это встаёт в обмен
Свой транспорт в БСП подключается только правкой конфигурации, поэтому обработка работает мостом между двумя каталогами. В узле обмена ставится штатный транспорт “Локальный или сетевой каталог”. У отправителя обработка забирает пакет из этого каталога и везёт на FTP, у получателя забирает с FTP и кладёт в такой же каталог, откуда пакет берёт штатная загрузка. От версии БСП это почти не зависит.
Входящий пакет от исходящего в одном каталоге отличается по имени. БСП называет пакет
Message_<отправитель>_<получатель>: если мы узел А, наш исходящий называется Message_А_Б,
входящий Message_Б_А (перед кодом иногда стоит ещё префикс базы). Код своего узла обработка
берёт из ЭтотУзел() планов обмена, в форме его можно перебить.
Для расписания есть серверная команда дополнительной обработки БСП “Отправить и получить пакеты обмена через FTP”: сначала отправка, потом приём. Вызовом из кода мы прогнали её в обе стороны, через справочник дополнительных обработок с расписанием пока нет.
Старые конфигурации
FTP-обменом чаще пользуются базы, которые годами не обновлялись: платформа свежая, а режим
совместимости 8.2.16. В этом режиме закрыто всё, что появилось в 8.3.6: СтрНайти
с направлением поиска, СтрРазделить, СтрСоединить, СтрЗаканчиваетсяНа,
НаправлениеПоиска. Первая сборка обработки в такой базе даже не создавалась:
Ошибка инициализации модуля: ВнешняяОбработка.FTPДокачкаПакетовОбмена.МодульОбъекта
{...МодульОбъекта(213,34)}: Переменная не определена (НаправлениеПоиска)
Конфигуратор в той же базе на /CheckModules отвечал “Синтаксических ошибок не обнаружено”,
ошибка ловится только созданием обработки. Функции заменены своими на Найти, Лев и Сред:
Функция ПозицияПоследнего(Знач Строка1, Знач Подстрока)
Итог = 0;
Позиция = Найти(Строка1, Подстрока);
Сдвиг = 0;
Пока Позиция > 0 Цикл
Итог = Сдвиг + Позиция;
Сдвиг = Итог + СтрДлина(Подстрока) - 1;
Позиция = Найти(Сред(Строка1, Сдвиг + 1), Подстрока);
КонецЦикла;
Возврат Итог;
КонецФункции
После правки обработка создаётся, полная отправка и приём проходят, хеш доставленного пакета совпадает во временных базах с режимами 8.2.16, 8.3.5 и 8.3.10. Сборка до правки при 8.2.16 и 8.3.5 падала. Платформа нужна от 8.3.20.
Три прогона с настоящим обрывом
Пакет настоящий: сообщение обмена, выгруженное ЗаписьСообщенияОбмена по служебному плану
обмена БСП, около 60 тысяч элементов “Номенклатуры”, 632 988 913 байт без сжатия, то есть
603,7 МБ и 76 частей (75 по 8 МБ и одна на 3,7 МБ). Обрыв делали убийством процесса
FTP-сервера, канал ограничивали до 4 МБ/с, чтобы попадать в середину части.
| Прогон | Что сделали | Что получилось |
|---|---|---|
| Обрыв отправки | сервер убит на 16-й части из 76 | на сервере 15 готовых частей (120 МБ за 32 секунды) и недописанный 0016.part.tmp. Вторая попытка прочитала опись и довезла 61 часть, 483,7 МБ за 2 минуты 7 секунд. Повторно не ушло ничего: 15 + 61 = 76 |
| Обрыв приёма | сервер убит на 20-й части приёма | у получателя 19 проверенных частей, в каталоге обмена пакета нет. Вторая попытка скачала оставшиеся 57 |
| Порча части | в части 0040 на сервере заменили 16 байт, размер тот же | на 40-й части хеш не совпал дважды, итог “Пакет НЕ положен в каталог обмена: … часть 40 из 76 битая на сервере”. Следующая отправка дослала одну часть, 8 МБ за доли секунды, второй приём скачал части с 40-й по 76-ю и положил пакет с верным хешем |
| Чистая передача и загрузка | без обрывов и без ограничения канала | ушёл за 7 секунд, пришёл за 12 (петля). Загружен в узле-получателе через ЧтениеСообщенияОбмена и ПрочитатьXML с ОбменДанными.Загрузка = Истина: все объекты записаны, ошибок ноль, номер принятого сообщения равен номеру пакета |
Без докачки вторая попытка в первом прогоне повезла бы все 603,7 МБ. Целый пакет на этом
канале идёт около двух с половиной минут, и при обрывах каждые полминуты он не дошёл бы
никогда. SHA-256 сверяли независимо от 1С, через crypto в Node.js: у отправителя,
в склейке частей на FTP и в каталоге получателя.
Полноценный узел синхронизации БСП между двумя базами в этих прогонах не настраивался. Утверждаем ровно то, что проверили: доставленный пакет платформа прочитала как сообщение обмена и записала целиком. FTPS тоже не заявляем: на стенде с TLS передача зависала на листинге каталога.
Обработка
Всё описанное собрано в обработку “FTP с докачкой для пакетов обмена”, на Инфостарте она стоит 10 SM, код открыт, конфигурацию не меняет. Сверху строка итога по цвету: идёт, доставлено с совпавшим хешем, обрыв с номером части, с которой продолжим, или отказ класть пакет в каталог с причиной. Журнал хранит все попытки по каждому пакету и отдельной строкой число частей, ушедших повторно; выгрузка в Markdown собирает журнал и диагноз обрывов для администратора канала, без логина и пароля FTP. Подробнее на странице инструмента.
Если пакет доходит, но объекты в нём не те или их нет, транспорт ни при чём, смотрите регистрацию изменений в плане обмена. Обмен с внешним сервисом, который отваливается по времени, разобран отдельно: таймаут обмена 1С с внешним сервисом.
Настроить обмен между центральной базой и филиалами на слабых каналах, от плана обмена до доставки и контроля, мы можем под ключ: обмен данными 1С.