БесплатноБазовая интеграция с 1С в подарок при заказе интернет-магазина под ключ

Надёжные интеграции на 1С-Битрикс: очереди, идемпотентность и повторы

Надёжные интеграции на 1С-Битрикс: очереди, идемпотентность и повторы

Интеграция, которая «работает на демонстрации», и интеграция, которая надёжно работает под нагрузкой год за годом, — это два разных инженерных изделия. Первая просто дёргает внешний API и радуется ответу 200. Вторая исходит из того, что сеть моргает, чужие сервисы падают, ответы теряются, а сообщения приходят по два раза — и всё равно не теряет ни одного заказа и не плодит дублей. Эта статья — о том, как сделать вторую: про очереди, идемпотентность и повторы, то есть про инженерную основу надёжных интеграций Битрикс.

Материал — практическая выжимка из наших проектов по интеграции 1С-Битрикс с внешними системами и разработке REST API для 1С-Битрикс. Без академизма: разберём, почему прямые синхронные вызовы опасны, как устроены очереди задач, что такое идемпотентность, как правильно делать ретраи с backoff и зачем нужно логирование интеграций и мониторинг. Примеры кода — иллюстративные, на псевдокоде и D7, чтобы показать структуру, а не готовую библиотеку.

Коротко

  • Не синхронно. Никогда не вызывайте внешний API прямо в момент действия пользователя — приёмник упал, и заказ не создался или данные потерялись.
  • Очередь. Складывайте обмен в очередь (таблица БД на D7) и отправляйте фоново агентом или cron.
  • Идемпотентность. Ключ идемпотентности и upsert по внешнему ключу защищают от дублей, которые иначе создали бы повторы.
  • Повторы и dead-letter. Ретраи с экспоненциальной задержкой и лимитом попыток, а безнадёжное — в мёртвую очередь с алертом.
  • Прозрачность. Логируйте каждый обмен и вешайте мониторинг: без журнала боевую интеграцию не отладить.

Почему синхронные вызовы внешних API опасны

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

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

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

Суть проблемы: синхронный вызов делает вашу доступность равной произведению доступностей всех внешних систем. Если у вас три интеграции, каждая с аптаймом 99%, то при синхронной связке в момент оформления совокупная надёжность падает примерно до 97% — и это только по внешним причинам, до всех ваших собственных сбоев. Асинхронный обмен разрывает эту зависимость: ваша операция завершается успешно независимо от того, что сейчас с соседями.

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

Что значит «надёжная интеграция»

Прежде чем строить, договоримся о критериях. Надёжная интеграция — это не «та, что не падает» (падают все), а та, что не теряет данные и не создаёт дублей несмотря на сбои. Из этого вырастают несколько конкретных свойств, к которым мы будем возвращаться на протяжении всей статьи.

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

Синхронно против очереди: сравнение

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

КритерийСинхронный вызовОчередь + фоновая отправка
Что при падении приёмникаЛомается операция пользователя или теряются данныеЗадание ждёт в очереди, уйдёт позже
Скорость для пользователяЗависит от чужого APIМгновенно (обмен отложен)
ПовторыОпасны без защиты, плодят дублиШтатные, с backoff и лимитом
Пики нагрузкиПрямо бьют по внешней системеСглаживаются буфером очереди
ПрозрачностьОбычно только логи ошибокПолный журнал состояния каждого задания
СложностьНиже на стартеВыше, но окупается надёжностью
Когда допустимоНекритичный разовый обмен, где потеря не страшнаЛюбой обмен, где данные важны

Единственная честная область применения синхронного вызова — операции, где нужен немедленный ответ пользователю и где сбой не приводит к потере (например, проверка промокода или запрос цены доставки в реальном времени). Но и там вызов нужно обкладывать таймаутом и запасным поведением. Всё, что связано с записью бизнес-данных — заказы, оплаты, остатки, статусы, — идёт через очередь.

Синхронный вызов против очереди Риск: напрямую и синхронно Заказ оформление Внешний API лежит ✕ заказ не создался Надёжно: через очередь Заказ принят Очередь буфер Внешн. API фоновая отправка → Что даёт очередь как буфер • Операция пользователя завершается сразу, не завися от чужого API. • Падение приёмника не теряет данные — задание ждёт и уходит позже. • Пики сглаживаются, повторы и журнал — штатные.
Схема: при синхронном вызове падение внешнего API ломает оформление; очередь принимает заказ мгновенно и отправляет его фоново, переживая недоступность приёмника.

Очереди задач: где хранить обмен

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

Очередь в базе данных

Для подавляющего большинства магазинов на 1С-Битрикс правильный ответ — таблица в БД, реализованная как ORM-сущность D7. Плюсы: данные переживают перезапуск и деплой, доступны транзакции, легко посмотреть и отредактировать задание руками, не нужна новая инфраструктура. Минусы — ограниченная пропускная способность по сравнению со специализированными брокерами, но для десятков тысяч обменов в сутки этого с запасом хватает.

Файловая очередь

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

Брокеры сообщений

RabbitMQ, Kafka, Redis-очереди и облачные аналоги — это специализированные системы, заточенные под высокую пропускную способность, доставку с гарантиями, маршрутизацию и несколько потребителей. Они мощны, но добавляют инфраструктуру, которую нужно поднимать, мониторить и обслуживать. Это оправдано на больших объёмах и в сложных топологиях; подробнее — в разделе про брокеры ниже.

Практическое правило: начинайте с очереди в таблице БД. Она закрывает 90% задач магазина, отлично отлаживается и не требует новой инфраструктуры. К брокеру переходите осознанно — когда упрётесь в конкретный предел (пропускная способность, порядок, несколько независимых потребителей), а не «чтобы было по-взрослому».

Очередь в таблице БД на D7

Покажем структуру очереди на собственной ORM-сущности D7. Это не готовый модуль, а скелет, который вы адаптируете под проект. Сначала — таблица заданий с ключевыми полями: тип обмена, полезная нагрузка, статус, число попыток, время следующей попытки и ключ идемпотентности.

// Поля таблицы очереди (ORM-сущность D7), условно:
// ID              — первичный ключ
// EVENT_TYPE      — тип обмена: 'order.export', 'stock.sync' и т.п.
// PAYLOAD         — сериализованные данные (JSON)
// IDEMPOTENCY_KEY — уникальный ключ идемпотентности (индекс UNIQUE)
// STATUS          — 'new' | 'processing' | 'done' | 'failed' | 'dead'
// ATTEMPTS        — сколько попыток уже сделано
// NEXT_RETRY_AT   — когда можно повторить (для backoff)
// LAST_ERROR      — текст последней ошибки
// CREATED_AT / UPDATED_AT — метки времени

// Постановка задания в очередь при событии:
function enqueue($type, array $data, $idempotencyKey) {
    // Если задание с таким ключом уже есть — не дублируем (дедупликация на входе)
    $exists = QueueTable::getRow([
        'filter' => ['=IDEMPOTENCY_KEY' => $idempotencyKey],
        'select' => ['ID'],
    ]);
    if ($exists) {
        return (int)$exists['ID'];
    }
    $res = QueueTable::add([
        'EVENT_TYPE'      => $type,
        'PAYLOAD'         => json_encode($data, JSON_UNESCAPED_UNICODE),
        'IDEMPOTENCY_KEY' => $idempotencyKey,
        'STATUS'          => 'new',
        'ATTEMPTS'        => 0,
        'NEXT_RETRY_AT'   => new \Bitrix\Main\Type\DateTime(),
        'CREATED_AT'      => new \Bitrix\Main\Type\DateTime(),
    ]);
    return $res->getId();
}

Обработчик очереди забирает пачку готовых к отправке заданий (тех, у кого статус new/failed и наступило NEXT_RETRY_AT), помечает их processing, чтобы их не подхватил параллельный обработчик, и выполняет обмен. Важна защита от одновременного захвата одной записи двумя процессами — её решают на уровне БД (например, обновлением статуса с проверкой прежнего значения).

function processBatch($limit = 50) {
    $now = new \Bitrix\Main\Type\DateTime();
    $rows = QueueTable::getList([
        'filter' => [
            '@STATUS'        => ['new', 'failed'],
            '<=NEXT_RETRY_AT' => $now,
        ],
        'order' => ['ID' => 'ASC'],
        'limit' => $limit,
    ])->fetchAll();

    foreach ($rows as $job) {
        // Атомарно «захватываем» задание: перевод new/failed -> processing.
        // Если апдейт не затронул строку — её уже взял другой процесс, пропускаем.
        if (!claimJob((int)$job['ID'])) {
            continue;
        }
        try {
            $result = sendToExternal($job['EVENT_TYPE'], json_decode($job['PAYLOAD'], true), $job['IDEMPOTENCY_KEY']);
            markDone((int)$job['ID'], $result);
        } catch (RetryableException $e) {
            scheduleRetry($job, $e);      // backoff + инкремент попыток
        } catch (\Throwable $e) {
            markFailedPermanent($job, $e); // не повторяем, готовим к dead-letter
        }
    }
}

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

Агенты, cron и фоновые процессы Битрикса

Кто-то должен регулярно запускать processBatch(). В 1С-Битрикс есть два штатных механизма фонового выполнения, и у каждого своя область.

Агенты

Агенты — встроенный планировщик Битрикса: функция или метод, который платформа вызывает по расписанию. Плюс — не нужен доступ к системному cron, всё настраивается внутри. Важный нюанс: по умолчанию агенты запускаются «на хите», то есть их выполнение привязано к посещениям сайта. Это делает их капризными на низком трафике (агент запустится, только когда кто-то зайдёт) и потенциально тяжёлыми для страницы, на хите которой они сработали. Для обмена агенты годятся, но лучше переводить их запуск на cron-режим.

Cron

Системный cron дёргает agent.php (или ваш отдельный скрипт) по стабильному расписанию, независимо от трафика. Это надёжнее для фоновой обработки очереди: обмен идёт по часам, а не по посещениям. Стандартная практика — перевести агенты Битрикса в режим запуска на cron и/или сделать отдельный cron-скрипт, который вызывает обработчик очереди раз в минуту.

МеханизмПлюсыМинусыКогда брать
Агент «на хите»Не нужен доступ к cron, всё внутриЗависит от трафика, грузит хитПростые задачи, есть стабильный трафик
Агент в режиме cronСтабильное расписание, штатный APIНужна настройка cronРекомендуемый вариант по умолчанию
Отдельный cron-скриптПолный контроль, изоляция от хитовПишете и обслуживаете самиТяжёлый обмен, высокая частота
Важно: следите, чтобы обработчик очереди не запускался в несколько параллельных копий, если он к этому не готов. Либо ставьте блокировку (lock-файл или запись в БД), либо проектируйте захват заданий атомарно, чтобы параллельные обработчики не брали одну запись дважды. Иначе получите те же дубли, от которых пытались уйти.

Брокеры сообщений: обзор

Когда объёмы обмена перерастают возможности очереди в БД, в игру вступают брокеры сообщений. Это не «более правильный» способ, а инструмент под другие задачи — разберём обзорно, чтобы вы понимали, когда он нужен.

Общая мысль: брокер снимает с вас реализацию доставки, повторов и маршрутизации, но добавляет инфраструктуру, которую нужно разворачивать, мониторить и обслуживать. Для магазина на Битриксе это оправдано, когда обмен становится по-настоящему интенсивным или топология сложной (много систем, много типов событий, требования к порядку и масштабированию). До этого порога очередь в БД проще, дешевле и понятнее в отладке. Ключевые принципы надёжности — идемпотентность, backoff, dead-letter, журнал — одинаковы и там, и там; меняется лишь транспорт.

Идемпотентность и ключ идемпотентности

Идемпотентность — центральное понятие надёжного обмена. Операция идемпотентна, если её повторное выполнение даёт тот же результат, что и однократное. В интеграциях это означает: сколько бы раз ни пришло сообщение «создать заказ №10245», в приёмнике должен появиться ровно один заказ.

Зачем это нужно? Потому что при модели доставки «хотя бы один раз» (а это самая практичная модель) повторы неизбежны, а значит, неизбежны и повторные доставки. Вы отправили запрос, приёмник его выполнил, но ответ потерялся — и повтор доставит то же сообщение снова. Без защиты появится дубль. Идемпотентность — это то, что делает повторы безопасными.

Ключ идемпотентности

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

// На стороне приёмника (псевдокод):
function handle($idempotencyKey, $payload) {
    $seen = IdempotencyStore::get($idempotencyKey);
    if ($seen !== null) {
        // Уже обрабатывали — возвращаем прежний результат, ничего не создаём
        return $seen['result'];
    }
    $result = doBusinessOperation($payload); // например, создать заказ
    IdempotencyStore::save($idempotencyKey, $result);
    return $result;
}

Upsert по внешнему ключу

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

Правило: проектируйте обмен так, будто каждое сообщение может прийти дважды — потому что рано или поздно оно придёт. Идемпотентность и ретраи — две стороны одной медали: повторы дают гарантию доставки, идемпотентность гасит дубли, которые эти повторы иначе бы создали. Одно без другого не работает.
Идемпотентность: повтор → один результат Сообщение №1 key = order-10245 Повтор (тот же) key = order-10245 Приёмник проверка ключа повтор отсекается по ключу Один заказ №10245, не дубль Известный ключ → возвращается прежний результат. Дубль не создаётся.
Схема: два сообщения с одним ключом идемпотентности приводят к единственному заказу — приёмник отсекает повтор по ключу и возвращает прежний результат.

Дедупликация сообщений

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

Дедуплицировать можно на двух концах. На входе в очередь — как в примере с enqueue() выше: перед добавлением задания проверяем, нет ли уже задания с таким ключом идемпотентности; уникальный индекс в БД гарантирует, что двух одинаковых не появится даже при гонке. На стороне приёмника — храним обработанные ключи и отбрасываем повторы.

Практические тонкости, о которых легко забыть:

Ретраи с экспоненциальной задержкой

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

Идея проста: с каждой неудачной попыткой пауза перед следующей растёт. Например, 1, 2, 4, 8, 16, 32 секунды — и так до верхнего предела. Это даёт приёмнику время восстановиться и распределяет нагрузку во времени. К backoff почти всегда добавляют jitter — случайный разброс паузы, чтобы множество клиентов, упавших одновременно, не ударили повторно синхронно (эффект «громового стада»).

// Вычисление задержки перед следующей попыткой:
function nextDelaySeconds($attempt) {
    $base = 1;            // базовая пауза, сек
    $cap  = 600;          // потолок паузы, сек (10 минут)
    $exp  = $base * (2 ** $attempt);        // 1,2,4,8,16,...
    $exp  = min($exp, $cap);                // не растём бесконечно
    $jitter = mt_rand(0, (int)($exp * 250)) / 1000; // случайный разброс до 25%
    return $exp + $jitter;
}

function scheduleRetry($job, $error) {
    $attempt = (int)$job['ATTEMPTS'] + 1;
    if ($attempt > MAX_ATTEMPTS) {          // например, 8
        moveToDeadLetter($job, $error);     // безнадёжное — в мёртвую очередь
        return;
    }
    $delay = nextDelaySeconds($attempt);
    QueueTable::update((int)$job['ID'], [
        'STATUS'        => 'failed',
        'ATTEMPTS'      => $attempt,
        'NEXT_RETRY_AT' => addSeconds(new \Bitrix\Main\Type\DateTime(), $delay),
        'LAST_ERROR'    => mb_substr((string)$error, 0, 1000),
    ]);
}

Не менее важно, что именно повторять. Повторять имеет смысл только временные (retryable) ошибки. Постоянные ошибки повтором не исправить — их нужно логировать и разбирать.

Стратегия повтораКак работаетКогда уместна
Мгновенный повторСразу же, без паузыПочти никогда — вредит перегруженному сервису
Фиксированная паузаОдинаковый интервал между попыткамиПростые случаи с редким обменом
Линейный ростПауза растёт равномерно (1,2,3,4…)Мягкая деградация, умеренные объёмы
Экспоненциальный backoffПауза удваивается (1,2,4,8…)Рекомендуемый вариант по умолчанию
Backoff + jitterЭкспонента со случайным разбросомМного клиентов, защита от «стада»
Тип ошибкиПримерыПовторять?
Временная (retryable)Таймаут, 5xx, превышение лимита, сеть недоступнаДа, с backoff
Постоянная (non-retryable)Невалидные данные, нет прав, метод не найденНет, логировать и разбирать
АвторизационнаяИстёк токен доступаОбновить токен и повторить один раз
Ретраи с экспоненциальной задержкой (backoff) время → 1 1с 2с 4с 8с попытка 1 попытка 3 Dead-letter лимит попыток Пауза растёт с каждой попыткой; исчерпав лимит, задание уходит в dead-letter, а не крутится вечно.
Схема: паузы между попытками растут экспоненциально (1, 2, 4, 8 секунд); после исчерпания лимита задание уходит в dead-letter на ручной разбор.

Таймауты: не ждать вечно

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

Таймаутов на самом деле два, и путать их нельзя:

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

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

Circuit breaker: предохранитель

Когда внешний сервис не просто моргнул, а лежит основательно, продолжать долбить его повторами — расточительно: вы тратите ресурсы обработчика на заведомо провальные вызовы и мешаете сервису подняться. Здесь помогает шаблон circuit breaker («предохранитель»), заимствованный из электротехники: если направление стабильно «искрит» ошибками, цепь размыкается и вызовы временно прекращаются.

Предохранитель обычно имеет три состояния:

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

Dead-letter: что делать с неотправленным

Даже при идеальных повторах часть сообщений доставить не удастся: приёмник лежит слишком долго, данные оказались невалидными, событие «ядовитое» и ломает обработчик снова и снова. Такие сообщения нельзя терять молча и нельзя оставлять в основной очереди — иначе они будут бесконечно крутиться и мешать остальным. Для них существует dead-letter («мёртвая очередь»).

Dead-letter — это отдельный статус или отдельная таблица, куда переезжают задания, исчерпавшие лимит повторов, и заведомо необрабатываемые сообщения. Смысл — сохранить данные и сделать проблему видимой, а не потерять её. Работают с dead-letter так:

  1. Фиксируйте причину. Вместе с сообщением сохраняйте последнюю ошибку, число попыток и время — чтобы инженер понял, что случилось, не гадая.
  2. Шлите алерт. Попадание в dead-letter — событие, требующее внимания. Без уведомления «мёртвая очередь» превращается в тихую свалку потерянных данных.
  3. Разбирайте и чините. Инженер устраняет причину: ждёт восстановления внешней системы, правит данные, исправляет баг обработчика.
  4. Переотправляйте (replay). После починки верните сообщение в основную очередь. Именно здесь снова спасает идемпотентность: повторная обработка не создаст дубля.
  5. Анализируйте закономерности. Если в dead-letter регулярно падает один и тот же тип сообщений — это не разовый сбой, а дефект интеграции, который нужно устранять в коде.

Важно отделять «ядовитые» сообщения от временных сбоев. Если сообщение падает из-за невалидных данных, оно будет падать при любом числе повторов — его нужно уводить в dead-letter сразу, не тратя попытки. А вот сообщение, упавшее из-за недоступности приёмника, заслуживает всех повторов и попадёт в dead-letter только после их исчерпания.

Логирование каждого обмена

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

Что стоит писать по каждому обмену:

function logExchange(array $entry) {
    ExchangeLogTable::add([
        'IDEMPOTENCY_KEY' => $entry['key'],
        'DIRECTION'       => $entry['direction'],   // 'out' | 'in'
        'EVENT_TYPE'      => $entry['type'],
        'REQUEST'         => maskSecrets($entry['request']),
        'RESPONSE'        => maskSecrets($entry['response']),
        'HTTP_CODE'       => $entry['code'],
        'ATTEMPT'         => $entry['attempt'],
        'STATUS'          => $entry['status'],       // 'ok' | 'retry' | 'fail'
        'DURATION_MS'     => $entry['duration'],
        'CREATED_AT'      => new \Bitrix\Main\Type\DateTime(),
    ]);
}

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

Логирование обмена и dead-letter Обработчик очереди Журнал обмена запрос · ответ · попытки Успех → done задание закрыто Dead-letter лимит исчерпан Алерт инженеру разбор и replay Каждый обмен пишется в журнал; безнадёжное уходит в dead-letter и поднимает алерт.
Схема: обработчик пишет каждый обмен в журнал; успешные задания закрываются, безнадёжные уходят в dead-letter и поднимают алерт для ручного разбора и повторной отправки.

Мониторинг и алерты

Журнал отвечает на вопрос «что случилось с конкретным обменом», а мониторинг — на вопрос «здорова ли интеграция в целом прямо сейчас». Без мониторинга проблемы всплывают самым дорогим способом — от клиента, который заметил, что заказы не доходят. Цель — узнавать о сбое раньше, чем о нём узнает бизнес.

Метрики, за которыми стоит следить:

На критичные пороги вешают алерты в каналы, которые реально читают: мессенджер дежурной команды, почта, система инцидентов. Ключевой принцип — алерт должен быть действием, а не шумом. Если уведомления приходят по любому чиху, их перестают читать, и настоящий сбой теряется в потоке. Алертите на то, что действительно требует вмешательства: непустой dead-letter, застрявшая очередь, длительный всплеск ошибок.

Минимальный набор для магазина: алерт «в dead-letter появились записи», алерт «очередь не разбирается дольше N минут» и алерт «доля ошибок по направлению выше порога». Даже эти три уведомления закрывают большинство сценариев, где раньше проблему замечали слишком поздно.

Порядок событий и согласованность

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

Абсолютный глобальный порядок в распределённой системе дорог и почти никогда не нужен. На практике применяют три приёма, которых хватает подавляющему большинству магазинов.

Отдельно держите в голове модель итоговой согласованности (eventual consistency): в асинхронном обмене стороны на короткое время расходятся и сходятся чуть позже, когда все сообщения доставлены. Это нормально и правильно — важно лишь, чтобы расхождение было временным и ограниченным, а бизнес-логика была к нему готова (например, не считала заказ «пропавшим» в первые секунды после оформления). О том, как это стыкуется с событийной моделью Битрикса, — в статье про события и обработчики в Битрикс.

Вебхуки: подтверждение и повторная доставка

Отдельного разговора заслуживают входящие вебхуки — когда уже вы выступаете приёмником и внешняя система (или сам Битрикс) шлёт вам события. Здесь надёжность требует правильного поведения обработчика, иначе события теряются или дублируются на ровном месте.

Быстрое подтверждение

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

Повторная доставка и её последствия

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

Требование к обработчику вебхукаПочему
Быстрый ответ, обработка в фонеИначе таймаут отправителя и повторная доставка
Проверка подписи/токенаОтсечь поддельные запросы
Идемпотентность по ID событияПовторная доставка не создаёт дубль
Корректные HTTP-кодыОтправитель понимает, повторять или нет
Журнал входящихВидно, что пришло и как обработано

И зеркальное правило для случая, когда вебхуки шлёте вы: закладывайте на своей стороне повторную доставку с backoff и подтверждением. Считайте доставленным только то событие, на которое приёмник ответил успехом; остальное — повторяйте, а безнадёжное уводите в dead-letter, как и любой другой обмен.

Тестирование интеграций: моки и песочница

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

Моки и стабы

В автотестах реальный HTTP-клиент подменяется заглушкой, которая возвращает заранее заданные ответы. Это позволяет проверить вашу логику — очередь, ретраи, идемпотентность, dead-letter — детерминированно и без сети. Обязательные сценарии для моков:

Песочница и тестовый контур

Многие сервисы предоставляют песочницу (sandbox) — тестовый экземпляр API на данных, которые не жалко. Там прогоняют end-to-end сценарий целиком: от события в магазине до появления сущности во внешней системе. Если песочницы нет, поднимают отдельный тестовый контур или используют выделенный тестовый аккаунт. Главное правило — тесты не должны ходить в боевой сервис и создавать там мусорные данные.

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

Безопасность: подпись и секреты

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

Отдельно про подпись повторов: если вы проверяете подпись с меткой времени для защиты от повторного воспроизведения (replay attack), не перепутайте её с легитимной повторной доставкой вебхука. Легитимный повтор несёт тот же идентификатор события — его отсекает идемпотентность; атака replay — это перехваченный старый запрос, и от неё защищает окно валидности подписи. Механизмы разные, и работать они должны параллельно.

Практика на 1С-Битрикс: события и таблицы D7

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

Событие как источник обмена

Не вызывайте внешний API прямо в обработчике события. Обработчик события должен делать одну быструю вещь — поставить задание в очередь и завершиться. Например, на событие сохранения заказа вы кладёте в таблицу очереди задание order.export с номером заказа в качестве ключа идемпотентности. Подробно про механику событий — в отдельном разборе событий и обработчиков Битрикс.

use Bitrix\Main\EventManager;

// Регистрация обработчика (например, в init.php модуля)
EventManager::getInstance()->addEventHandler(
    'sale', 'OnSaleOrderSaved',
    ['\Vendor\Shop\Integration\OrderHandler', 'onOrderSaved']
);

class OrderHandler
{
    public static function onOrderSaved(\Bitrix\Main\Event $event)
    {
        /** @var \Bitrix\Sale\Order $order */
        $order = $event->getParameter('ENTITY');
        $orderId = $order->getId();

        // Обработчик НИЧЕГО не шлёт наружу — только ставит задание в очередь.
        // Ключ идемпотентности = стабильный номер заказа.
        enqueue('order.export', ['orderId' => $orderId], 'order-export-' . $orderId);
        // Быстро возвращаемся: пользователь не ждёт внешний API.
    }
}

Таблицы D7: очередь, журнал, идемпотентность

Три ORM-сущности закрывают всю механику: таблица очереди (QueueTable из примеров выше), таблица журнала обмена (ExchangeLogTable) и, при необходимости, таблица обработанных ключей на стороне приёмника. Всё это — обычные сущности D7 с индексами; ключевой — UNIQUE-индекс по ключу идемпотентности, который на уровне БД гарантирует отсутствие дублей заданий.

Агент или cron как обработчик

Раз в минуту cron (или агент в cron-режиме) вызывает processBatch(), который забирает готовые задания, отправляет их, пишет в журнал, планирует повторы и уводит безнадёжное в dead-letter. Обмен полностью отвязан от действий пользователя: заказ оформляется мгновенно, а его отправка в CRM происходит в фоне и переживает любую недоступность приёмника. Если объёмы вырастут, этот же контур масштабируется — например, переносом транспорта на брокер, — но логика надёжности остаётся прежней. Такие интеграции мы проектируем в рамках разработки REST API для 1С-Битрикс, а базовые принципы REST-обмена разобраны в статье про REST API в 1С-Битрикс.

Внедрение по шагам

Сведём всё в практический маршрут — как превратить хрупкую синхронную интеграцию в надёжную. Порядок неслучаен: каждый шаг опирается на предыдущий.

  1. Разорвите синхронность. Вынесите вызов внешнего API из обработчика пользовательского действия. На событие — только постановка задания в очередь и быстрый возврат.
  2. Заведите очередь. Создайте таблицу очереди на D7 с полями статуса, попыток, времени следующей попытки и ключа идемпотентности (UNIQUE-индекс).
  3. Определите ключ идемпотентности. Выберите стабильный бизнес-идентификатор (номер заказа) и передавайте его при каждой попытке; на приёмнике делайте upsert.
  4. Добавьте фоновый обработчик. Cron или агент в cron-режиме раз в минуту забирает пачку заданий и выполняет обмен, атомарно захватывая записи.
  5. Настройте таймауты. Задайте connect- и read-таймауты для каждого клиента; их превышение — обычная повторяемая ошибка.
  6. Реализуйте ретраи с backoff. Экспоненциальная задержка с jitter и лимитом попыток; различайте временные и постоянные ошибки.
  7. Заведите dead-letter. Исчерпавшие лимит и «ядовитые» сообщения уводите в мёртвую очередь с сохранением причины.
  8. Включите журнал обмена. Логируйте запрос, ответ, попытки и статусы, маскируя секреты; настройте ротацию.
  9. Повесьте мониторинг и алерты. Следите за глубиной очереди, возрастом старейшего задания и размером dead-letter; алертьте на действия, а не на шум.
  10. Протестируйте сбои. Проверьте на моках и в песочнице недоступность приёмника, повторную доставку, истёкший токен, лимит и обрыв после записи.

Частые ошибки

Грабли, на которые чаще всего наступают при построении интеграций на Битриксе:

Чек-лист надёжной интеграции

Короткий список для приёмки интеграции перед запуском:

Вывод

Надёжная интеграция отличается от хрупкой не тем, что она «не падает», а тем, что она не теряет данные и не плодит дубли, когда падает что-то вокруг. Вся эта надёжность вырастает из одного архитектурного решения — разорвать синхронную связь между действием пользователя и обменом с внешней системой. Приняли данные у себя мгновенно, поставили задание в очередь, отправили фоново с повторами и журналом. Дальше — набор проверенных механизмов: идемпотентность против дублей, backoff против перегруза, таймауты против зависаний, dead-letter против тихой потери, логирование и мониторинг ради прозрачности.

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

Частые вопросы

Почему нельзя просто вызвать внешний API прямо в момент оформления заказа?

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

Что такое идемпотентность простыми словами и зачем она в интеграциях?

Идемпотентность — это свойство операции давать один и тот же результат независимо от того, выполнили её один раз или несколько. В интеграциях это значит: если сообщение «создать заказ №10245» пришло дважды (например, из-за повтора после таймаута), в приёмнике должен появиться один заказ, а не два. Без идемпотентности любые ретраи опасны — они плодят дубли. Обеспечивают её обычно ключом идемпотентности: каждому обмену присваивают стабильный уникальный идентификатор (номер заказа, UUID), приёмник запоминает обработанные ключи и на повтор с уже известным ключом просто возвращает прежний результат, ничего не создавая заново.

Чем ретраи с экспоненциальной задержкой лучше простых повторов?

Простые повторы «в лоб» — сразу и подряд — чаще всего вредят: если внешний сервис перегружен или лежит, шквал мгновенных повторов только добивает его и вас (растут блокировки по лимитам, забивается пул соединений). Экспоненциальный backoff увеличивает паузу с каждой попыткой: например, 1, 2, 4, 8, 16 секунд, обычно с ограничением сверху и случайным разбросом (jitter), чтобы множество клиентов не били синхронно. Это даёт приёмнику время восстановиться, сглаживает пики и резко повышает шанс, что повтор попадёт в момент, когда сервис уже доступен. Число попыток обязательно ограничивают, а исчерпавшие лимит сообщения уводят в dead-letter на разбор.

Где хранить очередь заданий на 1С-Битрикс — в БД, файлах или брокере?

Для большинства магазинов на 1С-Битрикс достаточно очереди в собственной таблице БД на D7 (ORM-сущность): туда складываются задания на обмен, а агент или cron забирает их пачками и отправляет. Это надёжно (данные переживут перезапуск), просто в отладке и не требует новой инфраструктуры. Файловые очереди хрупки и плохо переносят конкурентный доступ — их лучше избегать. Полноценные брокеры сообщений (RabbitMQ, Kafka, Redis-очереди) оправданы, когда объёмы обмена очень велики, нужна высокая пропускная способность, строгий порядок или несколько потребителей. Начинать почти всегда стоит с таблицы в БД, а к брокеру переходить осознанно, когда упрётесь в её пределы.

Что такое dead-letter и что с ним делать?

Dead-letter (мёртвая очередь) — это место, куда попадают сообщения, которые не удалось доставить после исчерпания всех повторов, а также заведомо «ядовитые» сообщения, которые ломают обработчик снова и снова. Смысл в том, чтобы такие задания не терялись молча и не блокировали остальную очередь. С dead-letter работают вручную или полуавтоматически: инженер видит по журналу причину сбоя, чинит проблему (например, правит данные или ждёт восстановления внешней системы) и переотправляет сообщение обратно в основную очередь. Ключевое правило — на попадание записи в dead-letter должен приходить алерт, иначе «мёртвая очередь» превращается в тихую свалку потерянных данных.

Нужен ли circuit breaker в интеграциях небольшого магазина?

Circuit breaker («предохранитель») — это шаблон, который временно перестаёт стучаться во внешний сервис, если тот стабильно отвечает ошибками, и пробует снова через паузу. Для небольшого магазина с редким обменом полноценный circuit breaker часто избыточен — достаточно таймаутов и ретраев с backoff. Но как только внешних систем становится несколько и обмен интенсивный, предохранитель окупается: он не даёт вашей очереди тратить ресурсы на заведомо лежащий сервис и защищает от «эффекта домино», когда одна упавшая система тормозит все обмены. Можно начать с упрощённой версии: если за короткое окно накопилось много ошибок по конкретному направлению — приостановить его отправку на несколько минут.

Как гарантировать порядок событий, если они уходят из очереди?

Абсолютный глобальный порядок в распределённом обмене дорог и часто не нужен. На практике поступают проще. Во-первых, делают обработчики устойчивыми к перестановкам: приёмник должен корректно принять «изменение заказа» даже если оно пришло раньше «создания», либо отклонить и дождаться. Во-вторых, применяют версионирование: к каждому событию прикрепляют номер версии или метку времени, и приёмник игнорирует устаревшие обновления, если у него уже есть более свежее. В-третьих, там, где порядок критичен (например, статусы одного заказа), сохраняют порядок только в рамках ключа — все события одного заказа обрабатываются последовательно, а разные заказы могут идти параллельно. Это даёт согласованность без глобальной очереди-«бутылочного горлышка».

Как тестировать интеграцию, не дёргая боевой внешний сервис?

Двумя слоями. Первый — моки и стабы: в автотестах вы подменяете HTTP-клиент заглушкой, которая возвращает заранее заданные ответы, в том числе ошибки, таймауты и повторные доставки. Так вы проверяете свою логику очереди, ретраев и идемпотентности без сети и без чужой системы. Второй — песочница (sandbox) внешнего сервиса или отдельный тестовый контур: там вы прогоняете end-to-end сценарий на данных, которые не жалко. Обязательно тестируйте не только «счастливый путь», но и краевые случаи: приёмник недоступен, ответ пришёл дважды, истёк токен, превышен лимит, оборвалось соединение после записи. Именно эти сценарии в бою и отличают надёжную интеграцию от хрупкой.

Поделиться:

Нужна надёжная интеграция, которая не теряет заказы?

Спроектируем и разработаем асинхронный обмен на 1С-Битрикс: очередь на D7, идемпотентность, ретраи с backoff, dead-letter, журнал и мониторинг — данные доходят и не двоятся даже при сбоях внешних систем.

Игорь Воскресенский

Основатель студии B2Bsite. С 2014 года команда разрабатывает интеграции и REST API для интернет-магазинов и корпоративных сайтов на 1С-Битрикс: асинхронный обмен с очередями, идемпотентностью и повторами, связку с 1С, CRM, складами и маркетплейсами для среднего и крупного бизнеса.

← Все статьи блога