Привет! Я — Дарья, руководитель проектов в Uzum Data. В этой статье поделюсь с вами опытом работы с OpenSource SMS-шлюзом Jasmin: какие у нас были требования, с какими препятствиями столкнулись, как выбирались из трудностей.

C чего всё началось 

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

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

Предстоял выбор между тем, чтобы написать собственный sms-шлюз или использовать готовое OpenSource решение. 

Для того, чтобы сделать выбор, надо понимать требования. Наши требования были такие:
Шлюз должен подключаться к sms-центру через SMPP или HTTP.
Мониторить статусы сообщений.

Выдерживать объём отправок 150 000 (смс/сутки), скорость в пике — 30 смс/ сек. 

Сообщения не распределяются равномерно по суткам, а отправляются большими пачками (батчами), отсюда такое требование к скорости.

А также:

  • отчёты о доставке на уровне SmsC и Terminal,

  • статусы сообщений: planned, sent, delivered, failed,

  • отправка multipart-сообщений.

Мы рассмотрели несколько вариантов OpenSource смс-шлюзов и остановились на Jasmin SMS Gateway. У него тогда было 478 форков на гитхабе, что внушает доверие. Плюс к этому, мы спросили у коллег из других компаний, которые используют jasmin для своих рассылок, и отзыв был: «работает».  

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

Ожидание vs. Реальность

С самого начала мы приняли для себя нюансы, с которыми придётся иметь дело дальше: 

  • реализация на Python (в стеке нашего сервиса основной язык — Scala);

  • нет уверенности, что можно кастомизировать под все нужные кейсы;

  • количество открытых issues и непонимание, как масштабировать. 

Для A2P рассылок нам также важны были такие функции, как учёт часовых поясов, чёрных списков, и диапазон времени рассылки. Всё это мы учли в новом компоненте и написали собственный a2p‑adapter.

Весь механизм работы по smpp Jasmin реализует сам, а a2p‑adapter вызывает простые REST‑методы: передаёт группу контактов и содержимое сообщения. Далее Jasmin коммуницирует с sms центром (через A2P) и мониторит статусы доставки каждого сообщения.
Основное взаимодействие в Jasmin строится через RabbitMQ.

В теории всё довольно просто, но вот с чем мы столкнулись на практике:

  1. Jasmin написан не столько на Python, сколько на Twisted

  2. Теряется подключение к RabbitMQ 

  3. Потеря отчётов о доставке

  4. Перегрузка сервиса

  5. Утечка памяти 

Теперь о каждом пункте по порядку. 

Jasmin написан не столько на Python, сколько на Twisted

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

Теряется подключение к RabbitMQ 

При накоплении определенного кол-ва сообщений в очереди DLRLookup (получает статусы переданных на отправку сообщений через Rabbit) Jasmin перестает обрабатывать отчеты и модуль, отвечающий за их обработку, теряет соединение к RabbitMQ. Такое поведение могло привести к аварии на канале A2P в любой момент, а при увеличении нагрузки проблема проявлялась всё чаще. 

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

Как решили: ограничили объем unacked-сообщений при чтении из очереди DLRLookup. Запомнили опыт, заодно проверили, не грешат ли другие консьюмеры тем же. 

Перегрузка сервиса 

Тут стоит пояснить, что A2P коммуникации исполняются инстансами, в зависимости от расписания. Например, есть рассылки, где новые инстансы выполняются раз в сутки, в других — каждый час, в третьих — каждые несколько минут. Инстанс считается финальным тогда, когда мы получаем статусы переданных на отправку сообщений. В предыдущей описанной проблеме одним из негативных эффектов была потеря части статусов о доставке. Так вот, когда не приходят статусы отправки сообщений, инстансы рассылок не закрываются, и происходит их постоянный поллинг в ожидании получении статуса, что также даёт дополнительную нагрузку, т.к. одновременно поллятся очень много инстансов и они накапливаются, что приводит к деградации сервиса. Такое возникает при обработке крупных батчей с сообщениями, или большом количестве мелких батчей.

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

Утечка памяти

Это одна из самых серьёзных проблем, с которыми столкнулись, работая с Jasmin.
Возникали ситуации, когда компонент работал, работал, доходил до критического объёма потребляемой памяти и под с Jasmin падал по «Out of memory».

В ходе проведения нагрузочных тестов выявили, что:

  1. В очереди DLRLookup был установлен троттлинг на 30 сообщений, который влиял на количество используемой памяти: при увеличении нагрузки и росте кол‑ва сообщений объём памяти увеличивался, т.к. сообщения загонялись в память.

  2. При достижении лимита по памяти происходило отключение от RabbitMQ одним из компонентов Jasmin, а переподключения не происходило. Приходилось рестартовать под с Jasmin вручную.

метрики пода c Jasmin в момент проведения нагрузочного теста
метрики пода c Jasmin в момент проведения нагрузочного теста

Оказалось, что проблемы с памятью сразу две:

  1. Росла память working set по причине скопления логов в контейнере smpp. Поняли, что память не растёт бесконечно, а растёт до какого‑то предела, поэтому решили проблему, просто подняв лимит для памяти.

  2. Росла RSS‑память, или память, необходимая для задействованных операционных процессов. Эта проблема заключается в том, что информация для маппинга DLR‑статусов кэшируется в памяти процесса dlrlookup и удаляется либо когда приходит DLR PDU от оператора, либо по таймауту. Если смс состоит более чем из одной части (multipart = 2 и более PDU в сообщении), то кэшируются все PDU‑части сообщения. При этом, оператор присылает статус только по последней части сообщения, даже если оно состоит из 5 PDU, а остальные продолжают висеть в кеше и удаляются только после таймаута. Таким образом, чтобы сократить расход памяти, пришлось уменьшить таймаут dlr_expiry.

Для полноты картины, есть связанный баг на github, где объясняется: «When a SMS‑MT is not acked, it will remain waiting in memory for dlr_expiry seconds, after this period, any received ACK will be ignored»

Скажем честно, с Jasmin было много манипуляций, непредсказуемых проблем, да ещё и пришлось части команды освоить Python (Twisted). Но в результате, канал A2P работает и отвечает нашим требованиям по скорости. А когда придёт время для масштабирования, это будет уже новая история.

 

Комментарии (1)


  1. beg_1294
    00.00.0000 00:00
    -1

    Рад что uzum начал свой блог