Как мы с Jasmin SMS Gateway боролись (и победили)

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет! Я — Дарья, руководитель проектов в 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 работает и отвечает нашим требованиям по скорости. А когда придёт время для масштабирования, это будет уже новая история. 

 

Источник: https://habr.com/ru/company/uzum/blog/725816/


Интересные статьи

Интересные статьи

Особенность архитектуры 1С-Битрикс предполагает наличие контента как в базе (например: инфоблоки), так и непосредственно в статических файлах проекта.Данный формат создавал проблемы при совместной р...
Часто при разговорах с клиентами мы спрашиваем, как они ведут учет различных данных и используют ли они CRM-систему? Популярный ответ — мы работаем с Excel-файлами, а пот...
Кто бы что ни говорил, но я считаю, что изобретение велосипедов — штука полезная. Использование готовых библиотек и фреймворков, конечно, хорошо, но порой стоит их отложить и создать ...
В NASA анонсировали выбор подрядчика для строительства центрального блока лунной орбитальной станции «Lunar Gateway», который будет оснащен силовым энергомодулем «The power and propulsion ele...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...