Дисклеймер
Сразу хочу оговориться, что ниже написанное не претендует на лучшее решение (возможно, код где-то не оптимален), это просто как один из вариантов решения возникшей проблемы.
Вместо введения
Собственно, что мы имеем - имеем две конфигурации, одна написанная с нуля для работы кассиров в магазинах, так называемый FrontOffice, вторая Управление торговлей, редакция 11.2 (11.2.3.108). В обе конфигурации встроена БСП версии 2.3.2.50. Между конфигурациями существует обмен данными через Web-сервисы. Сам обмен и транспорт обмена полностью типовой, добавлен только свой план обмена. Обмен между базами происходит раз в 5 минут и в среднем очень маленький, в пределах 100 - 150 объектов за одну итерацию. Ввиду того что обмен работает через Web-сервис а магазины (FrontOffice) географически находятся в разных местах мы имеем несколько ограничений. Первое ограничение связано с тем что с разными магазинами (FrontOffice) разные каналы связи, которые соответственно имеют разную пропускную способность (от 2 Мбит до 50 Мбит), что в свою очередь накладывает серьезные ограничения на передаваемый объем данных. Второе ограничения связано с передачей данных на стороне веб сервера. Как пишут в статьях и на форумах максимальный размер пакета, который стабильно может передаваться через веба сервер варьируется от 16 Мб до 30 Мб (при настройках по умолчанию в зависимости от Web-сервера). По факту, ради эксперимента, я передавал файл через Web-сервис размером примерно 130 Мб в локальной сети, и он проходил, без каких-либо доработок, но не понятно, как это сказывалось на работоспособности веб сервера (так как не рекомендуется передавать данные больше нескольких Мб) и как бы это все происходило при пропускной способности канала 2 Мбита.
О проблеме
Суть проблемы заключается в том, что в определённый момент времени нужно передать порядка> 60000 объектов + примерно 40000 объектов должны были выгрузиться по ссылкам. При попытке выгрузить такое количество данных на магазин с пропускной способностью канала в 50 Мбит происходило следующее. План обмена на стороне инициатора обмена показывал, что все данные успешно выгружены, не сжатый файл выгрузки при этом "весил" примерно 800 Мб в сжатом виде примерно 13 Мб. А на принимающей стороне отображалось информация о том, что ничего не происходит и весь журнал регистрации заполнялся ошибками, т.к. обмен пытался каждые 5 минут запуститься заново, но не запускался из-за ошибки, что уже идет обмен данными. Так могло продолжаться пол дня и сутки и ничего в итоге не загружалось, обмен просто подвисал. По идее, при передачи данных через web-сервис (если верить исходному коду и описанию) происходит разбивка выгружаемых данные на файлы примерно по 1 Мб их передача, а затем склеивание и загрузка. Происходит это в общем модуле ОбменДаннымиСервер:
// Функция передает указанный файл в сервис передачи файлов.
//
// Параметры:
// ИмяФайла - Строка - путь к передаваемому файлу.
// ПараметрыДоступаКСервису - Структура: АдресСервиса, ИмяПользователя, ПарольПользователя.
// РазмерЧасти - Число - размер части в килобайтах. Если значение равно 0,
// то разбивка на части не производится.
// Возвращаемое значение:
// УникальныйИдентификатор - идентификатор файла в сервисе передачи файлов.
//
Функция ПоместитьФайлВХранилищеВСервисе(Знач ИмяФайла, Знач УзелИнформационнойБазы, Знач РазмерЧасти = 1024, Знач ПараметрыАутентификации = Неопределено)
// Возвращаемое значение функции.
ИдентификаторФайла = Неопределено;
Прокси = ПолучитьWSПроксиДляУзлаИнформационнойБазы(УзелИнформационнойБазы,, ПараметрыАутентификации);
ОбменВыполняетсяВОднойСети = ОбменДаннымиПовтИсп.ОбменВыполняетсяВОднойЛокальнойСети(УзелИнформационнойБазы, ПараметрыАутентификации);
Если ОбменВыполняетсяВОднойСети Тогда
ИмяФайлаВХранилище = ОбщегоНазначенияКлиентСервер.ПолучитьПолноеИмяФайла(КаталогВременногоХранилищаФайлов(), УникальноеИмяФайлаСообщенияОбмена());
ПереместитьФайл(ИмяФайла, ИмяФайлаВХранилище);
Прокси.PutFileIntoStorage(ИмяФайлаВХранилище, ИдентификаторФайла);
Иначе
КаталогФайлов = ПолучитьИмяВременногоФайла();
СоздатьКаталог(КаталогФайлов);
// Архивирование файла
ИмяНеразделенногоФайла = ОбщегоНазначенияКлиентСервер.ПолучитьПолноеИмяФайла(КаталогФайлов, "data.zip");
Архиватор = Новый ЗаписьZipФайла(ИмяНеразделенногоФайла,,,, УровеньСжатияZIP.Максимальный);
Архиватор.Добавить(ИмяФайла);
Архиватор.Записать();
// Разделение файла на части
ИдентификаторСессии = Новый УникальныйИдентификатор;
КоличествоЧастей = 1;
Если ЗначениеЗаполнено(РазмерЧасти) Тогда
ИменаФайлов = РазделитьФайл(ИмяНеразделенногоФайла, РазмерЧасти * 1024);
КоличествоЧастей = ИменаФайлов.Количество();
Для НомерЧасти = 1 По КоличествоЧастей Цикл
ИмяФайлаЧасти = ИменаФайлов[НомерЧасти - 1];
ДанныеФайла = Новый ДвоичныеДанные(ИмяФайлаЧасти);
Прокси.PutFilePart(ИдентификаторСессии, НомерЧасти, ДанныеФайла);
КонецЦикла;
Иначе
ДанныеФайла = Новый ДвоичныеДанные(ИмяНеразделенногоФайла);
Прокси.PutFilePart(ИдентификаторСессии, 1, ДанныеФайла);
КонецЕсли;
Попытка
УдалитьФайлы(КаталогФайлов);
Исключение
ЗаписьЖурналаРегистрации(СобытиеЖурналаРегистрацииУдалениеВременногоФайла(),
УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Прокси.SaveFileFromParts(ИдентификаторСессии, КоличествоЧастей, ИдентификаторФайла);
КонецЕсли;
Возврат ИдентификаторФайла;
КонецФункции
Да и действительно файл разбивается на части, в случае если он больше 1 Мб и передается, но в нашем случае данные метод не помогает, обмен все равно зависает, где то при попытке все эти данные передать и загрузить. Тогда в общем то и родилась идея передавать данные частями, а точнее выгружать определённое количество объектов за одну итерацию обмена данными. К примеру, если зарегистрировано 10000 изменений по справочникам мы их выгружаем по 1000 за одну итерацию обмена, пока не выгрузим все 10000 объектов. Тем самым мы уменьшаем нагрузку на канал, уменьшаем время выгрузки и загрузки за одну итерацию и в общем если канал связи совсем слабый, то гарантированно передаем необходимое количество данных за раз. Идея реализации была в том, чтобы выгрузка продолжала работать через типовые средства при помощи БСП, но в случае необходимости, делала это частями.
Реализация
Собственно, сам алгоритм получился примерно следующий:
- Сначала проверяем количество зарегистрированных объектов (наборов записей) на целевом плане обмена и соответственно узле. Если количество объектов меньше чем установлено в ограничении, то никаких дополнительных ограничений не накладываем, просто выгружаем все как есть типовыми механизмами.
- Если количество зарегистрированных изменений по справочникам больше чем установлено в ограничении, то сначала порционно выгружаются данные из справочников. Справочники выгружаются в первую очередь, чтобы между выгрузкой справочников и регистров в регистрах не было «Объект не найден»
- Если количество зарегистрированных объектов больше чем установлено в ограничении и есть зарегистрированные изменения по регистрам, то порционно выгружаются регистры (сведений и накоплений).
Может возникнуть вопрос, почему не выгружать одновременно справочники и регистры, если количество зарегистрированных изменений не превышает ограничение. Ответ кроется в функции ВыбратьИзменения у менеджера плана обмена. Что она собственно делает? Описание из синтаксис помощника 1С:Предприятие:
Синтаксис:
ВыбратьИзменения(<Узел>, <НомерСообщения>, <ФильтрВыборки>)
«Формирует выборку измененные данные для передачи их в тот или иной узел плана обмена. При этом в процессе выборки изменений в записи регистрации изменений проставляется номер сообщения обмена данными, в котором должны передаваться изменения. Номер сообщения в записи регистрации проставляется для того, чтобы при подтверждении приема сообщения, в котором передавались изменения соответствующие записи регистрации изменений были удалены и в дальнейшем изменения больше не передавались.»
В чем ее смысл – она выбирает измененные данные (зарегистрированные изменения) и проставляет в записи регистрации изменений номер сообщения обмена данными. Нам же нужно ограничить выборку определенным количеством объектов. В описании к данной функции есть важное замечание к 3 параметру «ФильтрВыборки»:
Неопределено - фильтр пуст, выбираются все изменения по узлу;
ОбъектМетаданных - выбираются изменения в основной таблице, связанной с данным объектом метаданных;
СсылкаНаОбъект - фактически, может быть выбрана только одна запись об изменении данного объекта, либо ни одной, если объект не менялся;
НаборЗаписей - набор записей регистра, может быть не выбран, для фильтрации изменений используется лишь отбор набора записей;
Массив - все элементы массива имеют один из перечисленных выше типов, кроме Неопределено. Условия фильтрации соединяются по ИЛИ.
Значение по умолчанию: Неопределено.
В замечании сказано, что если мы передаем массив каких-либо элементов для фильтрации, то все элементы должны быть одного типа, например, СправочникСсылка, или НаборЗаписей и т.д. Именно из-за этого ограничения пришлось разграничить выгрузку на справочники и регистры.
Все изменения добавляются в обработку «КонвертацияОбъектовИнформационныхБаз» в модуль объекта в процедуру «ВыполнитьВыгрузкуЗарегистрированныхДанных» после строки:
«НачальнаяВыгрузкаДанных = ОбменДаннымиСервер.УстановленПризнакНачальнойВыгрузкиДанных(ЗаписьСообщения.Получатель);»
До строки кода, которую тоже необходимо заменить:
ВыборкаИзменений = ОбменДаннымиСервер.ВыбратьИзменения(ЗаписьСообщения.Получатель, ЗаписьСообщения.НомерСообщения, МассивВыгружаемыхМетаданных);
В планы обмена необходимо добавить реквизит "КоличествоОбъектовВыгрузки" с типом число и добавить его на форму, в тем планы обмена, где ограничение актуально.
Еще один важный и приятный нюанс, это то что код написан таким образом, что при изменении состава необходимого плана обмена, нет необходимости переписывать ниже приведенный код, т.к. в нем определяется входит ли в состав плана обмена измененный объект. Т.е. в случае добавления нового объекта в состав плана обмена (или исключение), он будет автоматически добавляться (не добавляться, в случае исключения) в динамически формируемые запросы.
Собственно, сам код с комментариями.
//Проверка на ограничение выгрузки данных за одну итерацию обмена данных
//=======================================================================
//получаем значение установленного ограничения
КоличествоОбъектовКВыгрузке = ЗаписьСообщения.Получатель.КоличествоОбъектовВыгрузки;
//если ограничение установлено, то начинаем собирать запрос по объектам участвующим в обмене
Если КоличествоОбъектовКВыгрузке <> 0 Тогда
МассивДанныхДляОтбора = Новый Массив;
ТекущийПланОбмена = Метаданные.ПланыОбмена[ЗаписьСообщения.Получатель.Метаданные().Имя];
ТекстЗапросаОбщий = "";
Для каждого СтрокаМетаданных Из ТаблицаПравилВыгрузкиИспользуемые Цикл
СтрокаДляЗапроса = "";
//для справочников формируем запрос с учетом того что там всегда есть Ссылка
Если ЗначениеЗаполнено(СтрокаМетаданных.ИмяОбъектаДляЗапроса) И ТипЗнч(ТекущийПланОбмена.Состав.Найти(СтрокаМетаданных.ОбъектВыборкиМетаданные)) = Тип("ЭлементСоставаПланаОбмена") Тогда
СтрокаДляЗапроса = СтрокаМетаданных.ИмяОбъектаДляЗапроса;
//если это первый проход то указываем что это создание пакета
Если ТекстЗапросаОбщий = "" Тогда
ТекстЗапросаОбщий = ТекстЗапросаОбщий + "Выбрать ВД.Ссылка" + Символы.ПС + "ПОМЕСТИТЬ ВсеДанные" + Символы.ПС + " из " + СтрокаДляЗапроса + ".Изменения" + " КАК ВД ГДЕ ВД.Узел = &Узел" + Символы.ПС;
Иначе
ТекстЗапросаОбщий = ТекстЗапросаОбщий + "ОБЪЕДИНИТЬ ВСЕ" + Символы.ПС + "Выбрать ВД.Ссылка из " + СтрокаДляЗапроса + ".Изменения" + " КАК ВД ГДЕ ВД.Узел = &Узел" + Символы.ПС;
КонецЕсли;
//для регистров формируем запрос с учетом того что там разный набор измерений, но нам достаточно бырать Узел, чтобы подсчитать Количество объектов
Если ЗначениеЗаполнено(СтрокаМетаданных.ИмяОбъектаДляЗапросаРегистра) И ТипЗнч(ТекущийПланОбмена.Состав.Найти(СтрокаМетаданных.ОбъектВыборкиМетаданные)) = Тип("ЭлементСоставаПланаОбмена") Тогда
СтрокаДляЗапроса = СтрокаМетаданных.ИмяОбъектаДляЗапросаРегистра;
//если это первый проход то указываем что это создание пакета
Если ТекстЗапросаОбщий = "" Тогда
ТекстЗапросаОбщий = ТекстЗапросаОбщий + "ВЫБРАТЬ ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") + " Узел ПОМЕСТИТЬ ВсеДанные ИЗ " + СтрокаДляЗапроса + ".Изменения ГДЕ Узел = &Узел";
Иначе
ТекстЗапросаОбщий = ТекстЗапросаОбщий + "ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") + " Узел ИЗ " + СтрокаДляЗапроса + ".Изменения ГДЕ Узел = &Узел";
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
//добавляем выборку данных
ТекстЗапросаОбщий = ТекстЗапросаОбщий + Символы.ПС + "; ВЫБРАТЬ ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") +
" ВсеДанные.Ссылка КАК ИзмененныйОбъект ИЗ ВсеДанные КАК ВсеДанные";
ЗапросПоИзмененнымОбъектам = Новый Запрос(ТекстЗапросаОбщий);
ЗапросПоИзмененнымОбъектам.УстановитьПараметр("Узел", ЗаписьСообщения.Получатель);
ВыборкаПоИзмененнымОбъектам = ЗапросПоИзмененнымОбъектам.Выполнить().Выбрать();
//получаем количество измененных объектов, которое в любом случае не будет больше ограничения, тем самым ускоряя выборку и проверку
КоличествоЗарегистрированныхИзменений = ВыборкаПоИзмененнымОбъектам.Количество();
//если количество зарегистрированных объектов = количеству объектов ограничения то нуобходимо ограничить выгрузку
//проверка на "=" потому что все выборки ограничиваются количеством указанным в ограничении
//или есди по справочникам не зарегистрировано изменений, то выгружаем РС если по ним есть что выгрузить
Если КоличествоЗарегистрированныхИзменений = КоличествоОбъектовКВыгрузке ИЛИ КоличествоЗарегистрированныхИзменений = 0 Тогда
//собираем запрос по изменениям из справочников
ТекстЗапросаПоСправочникам = "";
Для каждого СтрокаМетаданных Из ТаблицаПравилВыгрузкиИспользуемые Цикл
СтрокаДляЗапроса = "";
Если ЗначениеЗаполнено(СтрокаМетаданных.ИмяОбъектаДляЗапроса) И ТипЗнч(ТекущийПланОбмена.Состав.Найти(СтрокаМетаданных.ОбъектВыборкиМетаданные)) = Тип("ЭлементСоставаПланаОбмена") Тогда
СтрокаДляЗапроса = СтрокаМетаданных.ИмяОбъектаДляЗапроса;
Если ТекстЗапросаПоСправочникам = "" Тогда
ТекстЗапросаПоСправочникам = ТекстЗапросаПоСправочникам + "Выбрать ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") + " ВД.Ссылка" + Символы.ПС + "ПОМЕСТИТЬ ВсеДанные" + Символы.ПС + " из " + СтрокаДляЗапроса + ".Изменения" + " КАК ВД ГДЕ ВД.Узел = &Узел" + Символы.ПС;
Иначе
ТекстЗапросаПоСправочникам = ТекстЗапросаПоСправочникам + "ОБЪЕДИНИТЬ ВСЕ" + Символы.ПС + "Выбрать ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") + " ВД.Ссылка из " + СтрокаДляЗапроса + ".Изменения" + " КАК ВД ГДЕ ВД.Узел = &Узел" + Символы.ПС;
КонецЕсли;
КонецЕсли;
КонецЦикла;
//добавляем выборку данных
ТекстЗапросаПоСправочникам = ТекстЗапросаПоСправочникам + "; ВЫБРАТЬ ПЕРВЫЕ " + Формат(КоличествоОбъектовКВыгрузке, "ЧГ=0") + " ВД.Ссылка ИЗ ВсеДанные КАК ВД";
ЗапросПоИзмененнымДанным = Новый Запрос(ТекстЗапросаПоСправочникам);
ЗапросПоИзмененнымДанным.УстановитьПараметр("Узел", ЗаписьСообщения.Получатель);
//получаем данные с учетом ограничения которые будем выгружать
МассивДанныхДляОтбора = ЗапросПоИзмененнымДанным.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
//запоминаем сколько объектов всего выгрузили
ВсегоВыгружено = МассивДанныхДляОтбора.Количество();
ТекстЗапросаПоРегистрам = "";
//если не было выгрузки справочников то в этой итерации можно выгрузить регистры
Если ВсегоВыгружено = 0 Тогда
Для каждого СтрокаМетаданных Из ТаблицаПравилВыгрузкиИспользуемые Цикл
//собираем запрос по изменениям из регистров
СтрокаДляЗапроса = "";
Если КоличествоОбъектовКВыгрузке - ВсегоВыгружено = 0 Тогда
Прервать;
КонецЕсли;
Если ЗначениеЗаполнено(СтрокаМетаданных.ИмяОбъектаДляЗапросаРегистра) И ТипЗнч(ТекущийПланОбмена.Состав.Найти(СтрокаМетаданных.ОбъектВыборкиМетаданные)) = Тип("ЭлементСоставаПланаОбмена") Тогда
СтрокаДляЗапроса = СтрокаМетаданных.ИмяОбъектаДляЗапросаРегистра;
ТекстЗапросаПоРегистрам = "ВЫБРАТЬ ПЕРВЫЕ " + Формат((КоличествоОбъектовКВыгрузке - ВсегоВыгружено), "ЧГ=0") + " * ИЗ " + СтрокаДляЗапроса + ".Изменения ГДЕ Узел = &Узел";
Запрос = Новый Запрос(ТекстЗапросаПоРегистрам);
Запрос.УстановитьПараметр("Узел", ЗаписьСообщения.Получатель);
ИзмененныеДанные = Запрос.Выполнить().Выбрать();
Если ИзмененныеДанные.Количество() > 0 Тогда
Пока ИзмененныеДанные.Следующий() Цикл
Если ВсегоВыгружено = КоличествоОбъектовКВыгрузке Тогда
Прервать;
КонецЕсли;
//формируем пустые наборы записей, т.к. из них нужны отборы для корректной фильтрации данных
Если Найти(СтрокаДляЗапроса, "РегистрСведений") <> 0 Тогда
Набор = РегистрыСведений[СтрЗаменить(СтрокаДляЗапроса, "РегистрСведений.", "")].СоздатьНаборЗаписей();
ИначеЕсли Найти(СтрокаДляЗапроса, "РегистрНакопления") <> 0 Тогда
Набор = РегистрыНакопления[СтрЗаменить(СтрокаДляЗапроса, "РегистрНакопления.", "")].СоздатьНаборЗаписей();
КонецЕсли;
//заполняем отборы данных в зависимости от измерения конкретного регистра
Для каждого СтрокаОтбора Из Набор.Отбор Цикл
//Добавляем попытку, т.к. в выборке из запроса могут быть не все поля, которое есть в отборе набора
Попытка
СтрокаОтбора.Установить(ИзмененныеДанные[СтрокаОтбора.ПутьКДанным]);
Исключение
КонецПопытки;
КонецЦикла;
МассивДанныхДляОтбора.Добавить(Набор);
Инкремент(ВсегоВыгружено);
КонецЦикла;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецЕсли;
//=======================================================================
// ВЫБОРКА ИЗМЕНЕНИЙ
ВыборкаИзменений = ОбменДаннымиСервер.ВыбратьИзменения(ЗаписьСообщения.Получатель, ЗаписьСообщения.НомерСообщения, МассивДанныхДляОтбора);
Иначе
// если количество объектов справочников к выгрузке меньше ограничения, то выгружаем только справочники, т.к. по регистрам
// может быть Количество объктов к выгрузке большем чем ограничение
МассивМетаданныхСправочников = ТаблицаПравилВыгрузкиИспользуемые.НайтиСтроки(Новый Структура("ИмяОбъектаДляЗапросаРегистра", Неопределено));
МассивВыгружаемыхМетаданных = Новый Массив;
Для каждого СтрокаМассиваМетаданныхСправочников Из МассивМетаданныхСправочников Цикл
МассивВыгружаемыхМетаданных.Добавить(СтрокаМассиваМетаданныхСправочников.ОбъектВыборкиМетаданные);
КонецЦикла;
ВыборкаИзменений = ОбменДаннымиСервер.ВыбратьИзменения(ЗаписьСообщения.Получатель, ЗаписьСообщения.НомерСообщения, МассивВыгружаемыхМетаданных);
КонецЕсли;
Иначе
ВыборкаИзменений = ОбменДаннымиСервер.ВыбратьИзменения(ЗаписьСообщения.Получатель, ЗаписьСообщения.НомерСообщения, МассивВыгружаемыхМетаданных);
КонецЕсли;
Про быстродействие
В целом, добавление данного кода не должно вызывать какие то-провалы в производительности выборки данных для выгрузки. Все выборки из запросов ограничиваются количеством, заданным в ограничении, что позволяет не выбирать "лишние" данные, что существенно уменьшает объем выбираемых данных и увеличивает быстродействие запросов. Да и к тому же в случае ограничения выборки данных, функция "ВыбратьИзменения" с наложенным фильтром отрабатывает гораздо быстрее, чем выборка всех полностью изменений.
Ограничения
- Данный код проверялся только на Управление торговлей, редакция 11.2 (11.2.3.108) и БСП 2.3.2.50 и только в режиме передачи данных через Web - сервисы, по правилам, написанным в конвертации данных 2.1. Возможно в других конфигурация потребуется какая-то доработка кода, так же и в случае использования универсально формата обмена данными.
- Данный код рассчитан на то, что в составе плана обмена участвуют только справочники, регистры сведений и регистры накопления. Если в плане обмена участвуют другие объекты, то код потребует доработки.
Вместо заключения
Мы были готовы потерять немного во времени выполнения обмена данными, т.к. понимали, что он стал выполняться дольше, но в итоге после данной модификации мы получили стабильно работающий обмен данными, не зависимо от количества передаваемых объектов (и соответственно объема данных). Проблемы была решена, что собственно и требовалось.
Надеюсь данное решение поможет кому то сэкономить время в случае подобной проблемы.