Резервирование SMS-маршрутов: каскад без дублей и задержек
Как построить переключение SMS-маршрутов: критерии отказа, TTL, circuit breaker, идемпотентность, защита от дублей, совместимость отправителя и проверка каскада.

Содержание
- Какие отказы должен закрывать каскад
- Модель состояния важнее списка поставщиков
- Как описать допустимые маршруты
- TTL ограничивает бессмысленные повторы
- Circuit breaker защищает от лавины
- Как не отправить одно SMS дважды
- Разделяйте ретрай и переключение маршрута
- Когда нужен резервный канал
- Какие метрики показывают качество каскада
- Как испытать схему до аварии
- Чек-лист промышленного запуска
Резервирование SMS-маршрутов — это управляемый выбор следующего допустимого пути, а не повтор одной и той же отправки через всех поставщиков. Система должна отличать явный отказ от неопределённого результата, учитывать срок полезности сообщения, сохранять единый идентификатор бизнес-события и блокировать маршрут после подтверждённой деградации. Иначе автоматический failover увеличит число дублей и задержек вместо повышения доступности.
Какие отказы должен закрывать каскад
Маршрут может стать непригодным на разных этапах:
- API или SMPP-сессия недоступны;
- поставщик отвечает слишком медленно;
- соединение установлено, но запросы отклоняются;
- исчерпан лимит TPS;
- маршрут не поддерживает нужную страну, оператора или имя отправителя;
- сообщение принято, но время доставки резко выросло;
- доля финальных ошибок превышает рабочий порог;
- нарушена обратная доставка DLR;
- закончился баланс или сработало коммерческое ограничение.
Не все события допускают одинаковую реакцию. Явный синхронный отказ до принятия сообщения обычно позволяет сразу выбрать следующий маршрут. Тайм-аут после отправки создаёт неопределённость: предыдущая сторона могла принять сообщение, но ответ не дошёл. Немедленная повторная отправка через другого поставщика в этом случае создаёт риск дубля.
Модель состояния важнее списка поставщиков
Для каждой попытки полезно хранить отдельную запись:
business_event_id
message_id
attempt_id
route_id
submitted_at
deadline_at
provider_message_id
submission_result
final_status
business_event_id связывает все попытки с одним уведомлением. attempt_id различает технические отправки, а provider_message_id используется для запросов статуса и обработки DLR.
Минимальная модель результата отправки:
- not submitted — соединение не установлено или запрос отклонён до принятия;
- accepted — поставщик подтвердил приём и вернул идентификатор;
- unknown — клиент не получил однозначного ответа;
- terminal failure — пришёл финальный отрицательный статус;
- delivered — получен финальный положительный DLR.
Состояние unknown нельзя автоматически приравнивать к отказу. Сначала нужна сверка по клиентскому идентификатору, запрос статуса или короткое окно ожидания — в зависимости от возможностей интеграции.
Как описать допустимые маршруты
Каскад должен строиться не из абстрактного рейтинга поставщиков, а из набора правил для конкретного сообщения. Перед выбором система фильтрует маршруты по ограничениям:
- страна и мобильная сеть получателя;
- зарегистрированное имя отправителя;
- тип трафика: OTP, транзакционный или маркетинговый;
- поддерживаемая кодировка и длинные сообщения;
- максимальный TPS и доступная квота;
- требования к сроку доставки;
- наличие DLR и запроса статуса;
- согласованные часы и коммерческие условия.
Только после этого оставшиеся варианты можно упорядочить по качеству, стоимости и текущему состоянию. Такой подход не позволит резервному маршруту отправить сообщение с неподходящим именем или в сеть, которую он фактически не обслуживает.
TTL ограничивает бессмысленные повторы
У каждого уведомления должен быть срок полезности. OTP после истечения кода, напоминание после начала события и сообщение о курьере после завершения доставки уже не решают бизнес-задачу.
В очередь стоит записывать:
- время создания события;
- крайний срок первой отправки;
- крайний срок всех повторов;
- максимальное число маршрутных попыток;
- допустимость перехода в другой канал.
Перед каждой попыткой система проверяет остаток времени. Если резервный маршрут не успеет пройти очередь и операторские задержки, сообщение завершается как просроченное без дополнительной отправки.
Circuit breaker защищает от лавины
Если маршрут деградировал, продолжать направлять на него каждое новое сообщение дорого и медленно. Circuit breaker временно исключает маршрут после устойчивого сигнала проблемы.
Для решения лучше сочетать несколько показателей:
- долю сетевых и серверных ошибок;
- P95 времени ответа;
- долю отклонённых submit-запросов;
- P95 времени до финального DLR;
- долю доставки внутри целевого окна;
- возраст самого старого сообщения в очереди;
- состояние SMPP bind и частоту переподключений.
Один единичный тайм-аут не должен выключать маршрут. Нужны минимальный объём наблюдений, короткое скользящее окно и защита от колебаний. После паузы маршрут возвращают в режиме ограниченной пробы, а не сразу принимают на него весь поток.
Как не отправить одно SMS дважды
Защита от дублей начинается до обращения к поставщику:
- Бизнес-сервис создаёт стабильный ключ идемпотентности.
- Коммуникационный сервис сохраняет событие и план отправки атомарно.
- Каждая попытка получает уникальный технический идентификатор.
- Принятый поставщиком запрос не повторяется без проверки его состояния.
- Все DLR обрабатываются идемпотентно и связываются с конкретной попыткой.
- После первого подтверждённого
deliveredостальные незапущенные попытки отменяются.
Если поставщик поддерживает клиентский идентификатор и идемпотентность API, используйте их. Но локальная защита всё равно нужна: один и тот же бизнес-запрос может прийти повторно из CRM, очереди или планировщика.
Надёжный webhook DLR дополняет эту схему: повторные и пришедшие не по порядку статусы не должны повторно запускать бизнес-действие.
Разделяйте ретрай и переключение маршрута
Повтор на том же маршруте полезен при короткой сетевой ошибке или ограничении скорости. Переключение требуется, когда проблема относится к самому маршруту или его качеству. Для каждого класса ошибки задайте отдельное действие:
| Сигнал | Типовая реакция |
|---|---|
| HTTP 429 или throttling | повтор с backoff в пределах TTL |
| отказ соединения | короткий повтор, затем резервный маршрут |
| явный запрет номера или шаблона | завершение без каскада |
| тайм-аут после submit | сверка состояния, не мгновенный дубль |
| рост задержки DLR | снижение веса или временное исключение маршрута |
| истёкший TTL | завершение без новой попытки |
Коды ошибок поставщиков нужно нормализовать в собственную модель. Иначе одинаковые причины будут запускать разные действия только из-за формата ответа.
Когда нужен резервный канал
Другой SMS-маршрут не закрывает все сценарии. Если номер недоступен, устройство выключено или пользователь находится вне покрытия, повтор через второго поставщика может закончиться тем же результатом.
Переход в push, голосовое уведомление или другой согласованный канал следует проектировать отдельно. Он должен учитывать предпочтения пользователя, тип сообщения, допустимое время и защиту от повторного бизнес-действия. Омниканальный сценарий не должен превращаться в одновременную отправку одинакового уведомления везде.
Какие метрики показывают качество каскада
- доля сообщений, отправленных основным и резервными маршрутами;
- причины переключений;
- время до первой успешной отправки;
- доставка внутри целевого окна по каждой позиции каскада;
- количество состояний
unknownи время их разрешения; - доля возможных и подтверждённых дублей;
- число открытий circuit breaker;
- возраст очереди по классам трафика;
- стоимость доставленного бизнес-события;
- расхождение внутренних статусов со сверкой поставщика.
Метрики нужно смотреть вместе с расчётом TPS и очередей и нормализованными DLR. Высокая общая доставляемость может скрывать поздние OTP или постоянную работу на дорогом резервном маршруте.
Как испытать схему до аварии
Тестовый план должен включать не только полное отключение API:
- отказ DNS и TCP-соединения;
- медленный ответ после фактического принятия сообщения;
- ограничение TPS;
- серия явных отказов submit;
- задержка и нарушение порядка DLR;
- недоступность хранилища идемпотентности;
- восстановление маршрута после circuit breaker;
- исчерпание TTL при большой очереди;
- несовместимое имя отправителя на резервном пути.
Результат теста — журнал времени переключения, число сообщений в неопределённом состоянии, подтверждённые дубли и влияние на приоритетный трафик. Общая практика устойчивости внешних API помогает оформить тайм-ауты, retry и circuit breaker как единую политику.
Чек-лист промышленного запуска
- определить стабильный ключ идемпотентности бизнес-события;
- нормализовать результаты submit и финальные DLR;
- описать состояние
unknownи процедуру сверки; - задать TTL и максимум попыток для каждого класса сообщений;
- проверить имя отправителя и ограничения каждого маршрута;
- разделить повтор на маршруте и переход к следующему;
- настроить circuit breaker с ограниченным возвратом;
- изолировать OTP от массового трафика;
- включить метрики дублей, задержки и стоимости;
- провести управляемый тест деградации;
- документировать ручное отключение и возврат маршрута;
- регулярно сверять статусы и биллинг с поставщиками.
Надёжный каскад не обещает, что любое сообщение обязательно будет доставлено. Он обеспечивает более важное свойство: каждая попытка объяснима, ограничена по времени и не создаёт неконтролируемых дублей при сбое одного из участников цепочки.


