Файл CommerceML передан целиком, 1С показывает зелёную галочку, а в каталоге сайта по-прежнему вчерашние остатки. Кто виноват? Бухгалтер скажет, что выгрузка прошла. Администратор сайта скажет, что товаров нет. Оба правы, и в этом вся суть: «передача файла» и «обновление каталога» — два разных события, между которыми лежит отдельная работа. Если строить собственный узел обмена, то именно на этом шве он и держится.

Эта статья про то, как разложить обмен на этапы, которые нельзя склеивать в одну операцию, и что из этого стоит держать на своём VPS. Мы пройдём путь одного файла от авторизации до записей в каталоге, а потом посмотрим, что делать с повторами, зависшей обработкой и данными, которые меняются прямо во время обмена.

Три предмета обмена и три владельца

Прежде чем говорить о протоколе, полезно ответить на скучный вопрос: чьи это данные. Обмен «1С и магазин» на самом деле состоит из нескольких потоков с разным направлением и разным хозяином.

Кто владеет данными в типичной схеме (авторское разграничение, его можно сдвинуть под ваш бизнес)
ДанныеИсточник истиныКуда движутсяЧто случается при расхождении
Каталог: номенклатура, цены, остатки1С1С → сайтПравка на сайте будет затёрта следующей выгрузкой или, хуже, останется и разойдётся с учётом
ЗаказыСайт в момент создания, затем учётная системасайт → 1СЗаказ задвоится либо потеряется, если нет признака «уже выгружен»
Статусы заказов1С: оплата, сборка, отгрузка1С → сайтКлиент видит «в обработке», а товар уже в пути

Заметьте, что заказ меняет хозяина по ходу жизни: родился на сайте, потом им распоряжается учёт. Это нормальная ситуация, но она требует явного правила: с какого момента правки заказа на сайте перестают считаться авторитетными. Без такого правила интеграция превращается в тихую войну двух систем, где побеждает тот, кто писал последним.

Практический вывод для архитектуры: у каждого потока должен быть свой журнал состояния. «Каталог выгружен», «заказ №123 передан в 1С», «статус заказа №123 применён на сайте» — три разных вида фактов. Если сложить их в одну колонку «синхронизировано: да/нет», вы потеряете именно ту точность, ради которой затевался собственный сервис.

Протокол как последовательность, а не как «отправить файл»

Обмен через CommerceML 2 описан в документации 1С по протоколу обмена с сайтом. По ней файлы CommerceML 2 передаются на сайт запросами POST, целиком или частями. Важно, что это один из вариантов обмена, а не единственный способ связать любую конфигурацию 1С с любым магазином. Если ваша конфигурация или модуль работают иначе, пользуйтесь тем, что поддерживают они; ниже речь о логике именно этого протокола.

Последовательность выглядит так. Названия режимов и параметров, кроме перечисленных в документации, зависят от вашей конфигурации и магазина, поэтому здесь они даны условно, как роли шагов, а не как готовый API.

  1. Аутентификация. 1С обращается к сайту, тот проверяет учётные данные и возвращает cookie. Именно cookie затем сопровождает все последующие запросы сессии.
  2. Согласование возможностей. Сайт сообщает, умеет ли он принимать ZIP, и называет file_limit — максимальный размер одной порции файла.
  3. Передача файла. Файл уходит целиком, если укладывается в лимит, иначе частями — несколькими POST-запросами.
  4. Запуск обработки. Отдельным обращением сайту предлагают разобрать переданный файл.
  5. Повторы до завершения. Пока обработка не закончена, сайт отвечает, что запрос нужно повторить.
  6. Завершение по этому файлу. Сайт сообщает об окончании обработки.

Схематично это можно записать так (псевдозапись роли шагов, не конкретные адреса вашего модуля):

1. вход        -> ответ с cookie
2. сведения    -> поддержка ZIP, file_limit
3. файл, часть 1 (cookie)
   файл, часть 2 (cookie)
   ...
4. импорт файла (cookie) -> progress
   импорт файла (cookie) -> progress
   импорт файла (cookie) -> success

Обратите внимание, что передача и импорт здесь разнесены по разным пунктам. Это не прихоть формата. Принять байты и превратить их в товары — разные виды работы с разной ценой ошибки.

Cookie: маленькая деталь, из-за которой ломаются большие обмены

Раз сессию держит cookie, то все запросы одного обмена должны нести именно её. Если между шагами сессия потеряна, часть запросов окажется «чужой». Типичные причины в собственной инфраструктуре: балансировщик, который разводит запросы одной сессии на разные экземпляры приложения; прокси, срезающий заголовки; клиент, который не сохраняет cookie между вызовами. Для узла на одном VPS эта проблема проще, чем для кластера: один экземпляр приложения, одно место хранения сессий. Но если вы поставите перед обработчиками несколько воркеров, вопрос «где живёт сессия» придётся решить явно.

Про безопасность канала скажем отдельно. В исторических примерах протокола встречается обычный HTTP, но из этого не следует, что так можно делать сейчас: логин, пароль и cookie без TLS передаются открыто. Для своего узла нужен HTTPS с нормальным сертификатом, и это не пожелание, а условие того, что сессионный cookie не станет чужим ключом.

Файл частями и file_limit

Лимит порции задаёт сайт, а не 1С. Это логично: ограничения зависят от настроек веб-сервера, прокси и памяти обработчика на принимающей стороне. Если ваш узел стоит на VPS, значения лимита вы выбираете сами, и выбор имеет последствия:

  • слишком большой лимит упирается в таймауты и лимиты размера тела запроса у веб-сервера или обратного прокси: порция пройдёт на тестовом файле и упадёт на реальном;
  • слишком маленький даёт сотни запросов на крупный каталог, каждый со своими накладными расходами и шансом на сетевой сбой;
  • при передаче частями нужно отслеживать порядок и целостность: «часть 7 из 12 получена» — это состояние, которое надо где-то хранить.

Поддержка ZIP, о которой сайт сообщает на втором шаге, — вопрос трафика и времени: архив уменьшает объём передачи, но перед разбором его нужно распаковать, то есть потратить CPU и место на диске. Для узла на VPS это значит: свободное место под распакованное содержимое нужно считать с запасом, а не по размеру архива. Картинки товаров и XML сжимаются по-разному, поэтому точное соотношение зависит от вашего каталога, а не от общих цифр.

Два ответа, которые нельзя путать: progress и success

Теперь о самом важном. В пошаговом импорте ответ progress означает «запрос нужно повторить»: обработка продолжается и ещё не закончена. Ответ success сообщает, что обработка именно этого файла завершена. Документация при этом разделяет успешную запись переданного файла и успешный импорт: это разные этапы, и первый не гарантирует второй.

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

Из этого вытекают три правила для вашего сервиса.

  • Статус привязан к файлу. Не «обмен завершён», а «файл import.xml: передан полностью; импорт идёт; импорт завершён». Выгрузка обычно состоит из нескольких файлов (например, отдельно структура каталога и отдельно предложения с ценами и остатками), и у каждого свой путь.
  • Progress — это не сбой. Мониторинг, который тревожится при каждом повторе, будет звонить впустую. Тревожить должно другое: слишком долгое отсутствие продвижения.
  • Success — это точка фиксации. Только после неё имеет смысл говорить «этот файл применён» и записывать это в журнал.

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

Файл принят — значит ли это, что товары появились

Вернёмся к сцене из начала. Файл передан целиком. Что именно проверено на этот момент? Лишь то, что сайт получил байты и записал их. Записей в каталоге это не гарантирует: разбор может ещё не начаться, идти, упасть на одной неправильной позиции или застрять.

Поэтому приёмку обмена стоит строить на сопоставлении, а не на ответе протокола. Идея простая: из самого файла вы знаете, что в нём заявлено, а в базе магазина можете посмотреть, что появилось. Разница между этими числами и есть диагностика.

Условный пример сопоставления (числа выдуманы для иллюстрации)
ПоказательВ файлеНа сайтеЧто это говорит
Товаров в каталоге12001200Состав совпал
Позиций с ценой12001150Файл с предложениями обработан не полностью или не тот файл
Остатки по складу12000 обновленоИмпорт предложений не запускался или завершился ошибкой

Такой отчёт не требует догадок. Он прямо говорит, на каком этапе остановились: структура каталога применена, а цены и остатки нет. А это уже совсем другое сообщение, чем «не синхронизируется».

Для сопоставления нужны стабильные ключи. В обмене участвуют идентификаторы, которые 1С присваивает номенклатуре, и ваш сервис должен хранить соответствие «идентификатор в 1С — запись на сайте». Если такого соответствия нет, вы не отличите «товар обновился» от «создался дубль». Конкретные поля зависят от вашего магазина, поэтому придумывать их мы не будем: сверяйтесь со схемой данных вашей платформы.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Что на самом деле работает на VPS

Собственный узел обмена — это не один скрипт, а набор небольших частей с разными задачами. Вот разумный минимум для такой роли.

  • Приёмный HTTP-слой. Обрабатывает вход, выдаёт cookie, принимает части файлов. За ним стоит обратный прокси с TLS. Его нагрузка — короткие запросы и приём тела, поэтому ему важны лимиты размера и таймауты, а не гигагерцы.
  • Хранилище файлов. Полученные части, собранные файлы, распакованное содержимое. Здесь важен объём и скорость диска: распаковка и разбор XML упираются в ввод-вывод. На VPS с NVMe это неплохо ложится, но объём всё равно нужно считать под самый крупный реальный каталог плюс запас на распаковку.
  • Очередь задач разбора. Принять запрос и разобрать файл — разные нагрузки, поэтому их стоит развести. Очередь позволяет ответить 1С быстро (progress), а работу делать воркером.
  • База состояния обмена. Таблицы файлов, их этапов, соответствий идентификаторов, журнала попыток. Это память узла.
  • Воркеры разбора. Читают XML, пишут в базу магазина или вызывают его API.

По ресурсам разбор крупного XML обычно требует больше памяти и диска, чем приём запросов, особенно если парсер читает документ целиком. Потоковое чтение экономит память, но усложняет код. Если ваши выгрузки небольшие, подойдёт скромная конфигурация вроде Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe); если каталог крупный, а разбор идёт параллельно с приёмом заказов, логично смотреть на Uab (2 vCPU, 4 ГБ, 80 ГБ) или Heka (4 vCPU, 8 ГБ, 160 ГБ). Это отправная прикидка, а не обещание ёмкости: реальную нагрузку покажет замер на ваших данных.

У VPS MAATRIX доступен root, а значит, вы сами ставите обратный прокси, базу и воркеры и настраиваете лимиты под file_limit вашего магазина. Площадки перечислены в публичном предложении: UK, US, RU и FR; какую выбрать, решайте по задержке до 1С и до магазина, а не по общим соображениям. Параметры тарифов и наличие образов уточняйте на странице аренды VPS и в форме заказа. Если ваша 1С тоже живёт на облачном сервере, об этом отдельная статья: пошаговая настройка VPS под 1С в облаке с нуля.

Повтор этапа: безопасно ли спрашивать дважды

Сеть ненадёжна. Запрос ушёл, ответ не дошёл, 1С повторяет. Теперь вопрос: что произошло на стороне сайта в первый раз? Возможны варианты: запрос не дошёл вовсе; дошёл и выполнился; дошёл и выполняется до сих пор. Со стороны 1С они неразличимы, а последствия разные.

Отсюда разбор по этапам, насколько повтор безопасен. Выводы здесь авторские, и они зависят от того, как вы напишете обработчик.

  • Вход. Повтор безвреден: вы просто получите новую сессию.
  • Часть файла. Повтор безопасен, если запись идемпотентна по смыслу: часть №7 того же файла перезаписывает часть №7, а не дописывается второй раз. Если обработчик просто дописывает байты в конец, повтор испортит файл. Значит, нужен номер или смещение части.
  • Запуск разбора. Повтор безопасен только если вы различаете «ещё идёт» и «ещё не начинался». Иначе второй запуск запустит параллельный разбор того же файла.
  • Запись товаров. Безопасна, если запись выполняется как «создать или обновить по идентификатору», а не как «создать». Тогда повтор лишь перезапишет те же значения.

Здесь легко поддаться соблазну: придумать новый идентификатор попытки и считать, что он сам всё решает. Нет, он только позволяет отличать попытки друг от друга. Идемпотентность появляется не от идентификатора, а от логики: что обработчик делает, увидев знакомую попытку или знакомый товар.

Транзакции помогают, но у них есть граница

Разбор файла хочется обернуть в транзакцию: либо весь каталог применился, либо ничего. Для операций внутри базы данных это работает. Как описано в документации PostgreSQL по транзакциям, транзакция объединяет шаги в операцию «всё или ничего»: COMMIT фиксирует изменения, ROLLBACK отменяет изменения незавершённой транзакции. Там же важная оговорка по смыслу: эти гарантии относятся к операциям самой базы, а не к произвольному внешнему HTTP API.

Для интеграции это ключевая граница. Допустим, ваш воркер в одной транзакции пишет в свою базу состояния и вызывает HTTP-метод магазина. Откат транзакции отменит запись в вашей базе, но уже отправленный запрос в магазин назад не вернёт. Единой транзакции «своя база плюс внешний HTTP» не существует, если нет отдельного подтверждённого механизма, и обещать её нельзя.

Что делать практически. Транзакцию применяйте там, где она честно работает: записи в вашей базе (и в базе магазина, если вы пишете в неё напрямую и она это поддерживает) группируйте по порциям, а переходы состояний файла фиксируйте отдельно и после факта. А для внешних вызовов строите схему из двух частей: сначала фиксируете в базе намерение («отправляем заказ №123»), затем вызываете API, затем фиксируете результат. Если процесс упал между вторым и третьим, при перезапуске вы видите «намерение есть, результата нет» и решаете, что делать: проверить на стороне получателя или повторить, если вызов идемпотентен.

Зависшая обработка: как отличить долгую работу от мёртвой

Progress может приходить долго, и это нормально. А может приходить бесконечно, и это уже авария: воркер умер, а состояние осталось «разбирается». Снаружи эти два случая похожи, поэтому нужен признак жизни.

Практичная схема: воркер при обработке порции обновляет в базе состояния отметку времени и счётчик обработанных позиций. Тогда «зависло» определяется не по общему времени, а по отсутствию продвижения: счётчик не растёт, отметка старая. Порог выбирается по вашим данным: самый долгий нормальный разбор плюс запас. Универсального числа нет.

Когда зависание обнаружено, решение зависит от этапа:

  1. Если разбор идёт по схеме «создать или обновить по идентификатору», его можно безопасно запустить заново с начала: уже применённые позиции будут перезаписаны теми же значениями.
  2. Если разбор не идемпотентен, сначала нужно выяснить, какая часть применена, и только потом продолжать. Иначе вы рискуете получить дубли.
  3. Перед повторным запуском нужно убедиться, что прежний воркер действительно остановлен. Два воркера на одном файле — отдельный вид аварии. Для этого служит блокировка файла в базе состояния.

Хорошо, если сообщение об ошибке привязано к файлу и этапу: «import.xml: разбор остановился на позиции 8400 из 12000». Такая запись экономит часы разговоров.

Когда данные меняются прямо во время обмена

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

Рассмотрим конкретный случай, условный: в 10:00 1С формирует выгрузку остатков, где у товара осталось 5 штук. К 10:03 файл дошёл и начал разбираться. В 10:02 на сайте купили 2 штуки. Когда файл применится, на сайте снова окажется 5, хотя реально их 3 с учётом нового заказа, который в 1С ещё не доехал. Выгрузка в момент создания была верной, к моменту применения устарела.

Единого рецепта нет, но есть несколько приёмов, каждый со своей ценой:

  • Резерв на стороне сайта. Сайт хранит собственный счётчик резерва по заказам, которые ещё не подтверждены учётом, и вычитает его из пришедшего остатка. Сложнее, но точнее.
  • Отметка времени выгрузки. Остаток применяется, только если он новее уже применённого значения. Защищает от того, что старый файл затрёт свежие данные, но не решает вопрос с заказом в пути.
  • Окна обмена. Обмен в часы низкой активности сокращает расхождения, но не устраняет их и не подходит магазинам с круглосуточными продажами.
  • Частые маленькие обмены. Передавать только изменения, а не весь каталог: короче окно устаревания, меньше нагрузка на разбор. Ограничение: нужно, чтобы источник умел отдавать изменения.

То же касается заказов. Заказ, передаваемый в 1С, может быть изменён клиентом или менеджером ещё до того, как 1С его забрала. Поэтому в журнале нужен признак версии заказа или времени последнего изменения: иначе вы не узнаете, что в 1С ушла устаревшая версия. Опять же, конкретные поля определяются вашей платформой, и подбирать их надо по её схеме.

Отдельный вопрос — порядок файлов. Если предложения (цены и остатки) применятся раньше, чем структура каталога, они повиснут на несуществующих товарах. Поэтому внутри узла полезно задать зависимость: файл предложений ждёт успешного завершения импорта каталога. Это ещё одна причина считать статусы по файлам, а не по обмену в целом.

Как выглядит диагностика, когда этапы разделены

Теперь соберём всё вместе и посмотрим на типичную жалобу. Прежняя формулировка: «1С не синхронизируется». Что с ней делать? Ничего: в ней нет информации. Теперь представим, что узел ведёт журнал по этапам. Тот же случай читается так:

Пример журнала по одному обмену (условный)
ФайлВходПередачаИмпортСверка с сайтом
Структура каталогавыполненвсе части полученызавершён (success)состав совпал
Цены и остаткивыполненвсе части полученыидёт, нет продвижения 40 минутне проверялась

Из таблицы видно главное: последний полностью завершённый этап — импорт структуры каталога. Файл с ценами и остатками передан, но обработка остановилась. Проблема не в сети, не в логине и не в 1С: нужно смотреть воркер и его последнюю обработанную позицию. Это уже задача, которую можно поручить, а не расплывчатое «посмотрите, что не так».

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

Когда вы можете точно сказать «передача файла предложений завершена, импорт остановился на позиции 8400, сверка не проводилась», обмен перестаёт быть чёрным ящиком. Последний завершённый этап назван, следующий известен, а виновный участок цепочки ограничен одним воркером, а не всей интеграцией.