Однажды мы в Интерсвязи решили обновить Asterisk с 11 версии до 18. История получилась интересной и поучительной. Меня зовут Глеб, я инженер группы управления телефонией оператора связи. И вот наш опыт.

От коллег из других компаний знаем, что у многих сложилась схожая ситуация: используемая версия Asterisk отстает от актуальной уже на несколько лет. У кого-то даже версии 1.4 или 1.8. Почему не обновляют? У всех свои причины, наши же связаны с тем, что из-за рисков нарушить работу огромной инфраструктуры наши предшественники не решались пойти на такой шаг. А мы решились.

Зачем мы все-таки решились на обновление

Инфраструктура телефонии Интерсвязи пишется уже с давних времен. От предков нам достался кластер Asterisk 11 версии и многочисленные FastAGI скрипты, написанные на языке Perl. Казалось бы, что может быть хуже? Когда этого очень много.

Вот таких кружочков AGI я мог здесь расположить около 50, если показывать реальную схему. Со всем этим мы весьма успешно существовали вплоть до 2022 года, разрабатывая и поддерживая скрипты и FastAGI, реализующие всю нашу разностороннюю бизнес-логику.

Время шло, с выхода Asterisk 11 на момент 2022 года прошло уже 10 лет. Последующие версии оснащались новыми функциями, закрывали проблемы старых релизов, а вместе с этим появлялись причины и формировались цели на обновление.

Цель №1 — избавиться от устаревших Asterisk 11 из исходников и получить актуальную версию из пакетов, что позволит нам приобрести актуальные функции и облегчить следующие обновления.

Цель №2 — оптимизировать нашу инфраструктуру. Изначально это не рассматривали, она сформировалась в процессе обновления, когда мы разбирали возникшие трудности.

Цель №3 — внедрить использование ARI для модернизации VoiP-разработки, использования новых фич и современных приложений.

Расскажу обо всех целях подробнее, потому что на пути к каждой из них возникали свои трудности и нюансы.

Как мы обновляли Asterisk

Нам не хотелось все сводить к банальному обновлению. Хотелось получить инструмент, с которым мы могли бы одной командой сконфигурировать сервер с Asterisk 18 под свои задачи.

Мы используем кластер из четырех серверов с Asterisk, один из них — в резерве. Балансировка между ними производится с помощью Kamailio, поэтому автоматизация этого процесса для нас более актуальна. Если бы мы вручную обновляли серверы, это могло занять по два-три дня на каждый из четырех. Еще возрастал риск что-то забыть и произвести обновление не унифицировано.

В качестве инструмента выбрали Ansible. Произвели рефакторинг проекта, убрали лишние неиспользуемые элементы инфраструктуры, оптимизировали то, что работало не лучшим образом. Как итог — разработали роль Ansible, которая устанавливает свежий Asterisk и конфигурирует сервер примерно за 20 минут. В дальнейшем этой ролью можно проводить абсолютно любые манипуляции с сервером: конфигурирование iptables, systemd, обновление ПО и т.д.

С какими проблемами столкнулись:

1. Неподдерживаемая библиотека для работы с AMI.

Работа AMI состоит в том, что какое бы событие не произошло на Asterisk, будь то начало или конец звонка, задание переменной или что либо еще — все это инициализирует AMI-событие с сообщением, которое содержит в себе информацию и параметры для обработки этого события.

Ядро нашей телефонии было написано с использованием языка Perl. Мы использовали Metacpan-библиотеку Asterisk::AMI, которая вышла аж в 2011 году. Однако с выходом Asterisk 13 «выхлоп» сообщений AMI изменился, и эта библиотека перестала поддерживаться.

Из рассматриваемых вариантов решения проблемы — реализовать на серверах Asterisk собственное API, например, на Python с использованием Panoramisk, к которому мы бы обращались из наших скриптов, а он бы уже соответственно работал с AMI локально. 

Другой вариант решения проблемы — переписать все скрипты на REST-интерфейс Asterisk (ARI) и работать с Asterisk напрямую. Такой вариант, мягко говоря, очень замедлил бы процесс обновления, учитывая количество скриптов для переписывания.

В итоге решение проблемы нашлось не без везения — методом быстрого гугления  наткнулись в GitHub на готовое решение от Жени Гостькова, который, дописав обработку изменившихся сообщений, «научил» работать Perl c Asterisk 16+. Как показала практика, в том числе и с 18 версией. Проблему решили.

2. Изменение некоторых событий и как следствие — наши скрипты не принимали и не знали, как работать с новыми событиями.

Изменились не только имена, но и наполнение событий. Взгляните на самый явный пример.

Это событие Bridge, 11 версия Asterisk:

Event: Bridge
Bridgestate: <value>
Bridgetype: <value>
Channel1: <value>
Channel2: <value>
Uniqueid1: <value>
Uniqueid2: <value>
CallerID1: <value>
CallerID2: <value>

И вот, что оно из себя представляет уже в 18 версии:

Event: BridgeEnter
BridgeUniqueid: <value>
BridgeType: <value>
BridgeTechnology: <value>
BridgeCreator: <value>
BridgeName: <value>
BridgeNumChannels: <value>
Channel: <value>
ChannelState: <value>
ChannelStateDesc: <value>
CallerIDNum: <value>
CallerIDName: <value>
ConnectedLineNum: <value>
ConnectedLineName: <value>
Language: <value>
AccountCode: <value>
Context: <value>
Exten: <value>
Priority: <value>
Uniqueid: <value>
Linkedid: <value>
SwapUniqueid: <value>

Кроме того, изменилось также и поведение некоторых событий. Если раньше событие Bridge вызывалось один раз на два канала одновременно, то теперь заменившее его событие BridgeEnter вызывается два раза за звонок и на каждый канал. Нам пришлось изучить все подобные изменившиеся события и переписать их обработку.

3. Приложение Queue.

Третья проблема состояла в том, что глубоко под капотом у нас используется приложение диалплана Queue, и оно между 11 и 18 версией также претерпело немало изменений. У нас нарушилась логика при переводе звонка между очередями нашего кол-центра: наши Asterisk перестали понимать, что звонок ушел из очереди. По всей видимости, ранее в приложении существовал баг, который служил нам ориентиром, сигнализирующим о том, что звонок покидает очередь. Возможно, с выходом новых версий баг был устранен, из-за чего этого ориентира мы лишились. 

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

UserEvent — это приложение диалплана, которое позволяет создать кастомизированное AMI-событие с необходимыми нам параметрами, которые могут состоять, например, из текущих переменных канала.

Пример из диалплана:

[redirect]
exten => 1,Verbose(Перевод звонка)
  . . .
  . . .
  same => n,UserEvent(Redirect,MemberFrom:\ ${MEMBER_FROM}, MemberTo:\ ${MEMBER_TO})
  . . .
  . . .

Как мы им воспользовались? Помимо приложения Queue, наш кол-центр реализован за счет диалплана и различных скриптов, что позволяет нам легко конфигурировать любой этап звонка. На примере перевода звонка: когда оператор совершает его, вызываем в диалплане соответствующий UserEvent, который отлавливается нашим слушателем AMI-сообщений и обрабатывается соответствующим скриптом. А в этом скрипте фиксируется статистика по разговору с текущим оператором и останавливается запись разговора с ним.

Как мы оптимизировали инфраструктуру

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

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

Мы изолировали работу прослушивания AMI-событий в отдельный скрипт, который вычитывает AMI-сообщения и складывают их в кэш в виде Redis, а скрипты, работающие с обработкой этих событий, просто забирают их из кэша и работают как с обычным JSON-объектом.

Какие еще преимущества мы получили от этой схемы?

1. Мы добились повышения отказоустойчивости.

У нас есть кластер из четырех Asterisk, а также сервера, на которых крутятся наши скрипты. Скрипт, прослушивающий события, находится локально с каждым Asterisk, вычитывает сообщения и пытается их поместить в Redis, который находится на сервере с приложениями. Приложения слушают очередь этих событий в Redis и обрабатывают их. Что же произойдет, если Redis по какой-то причине, например, при сетевой проблеме, окажется недоступен? Listener это поймет и начнет отправлять сообщения на другой аналогичный сервер, на котором расположены те же приложения.

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

2. Мы оптимизировали обработку наших звонков.

Давайте взглянем на кусочек этой схемы отдельно:

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

3. Мы организовали фильтрацию событий.

В ходе дальнейших оптимизаций выяснили, что для работы нам нужны не все события, которые генерирует Asterisk, а лишь малая их часть — примерно 2,5%

Например, есть событие NewExten, которое вызывается, когда звонок переходит от экстена к экстену. Они составляли почти половину от всего потока событий, но при этом мы ими не пользовались.

Другой пример — события VarSet, которые генерируются всякий раз, когда в звонке происходит задание какой-то переменной. Для работы наших скриптов достаточно отлавливать только 10-15 таких событий.

Как мы можем это оптимизировать? Возможно кто-то не знал, но в Asterisk есть функция фильтрации AMI-сообщений в файле конфигурации manager.conf. В нем можем указать только необходимый набор сообщений. Так и сделали. 

Например, вот так мы указали только те события, которые нам нужны, исключив отсюда NewExten и др.:

eventfilter=Event: AgentConnect
eventfilter=Event: AgentComplete
eventfilter=Event: AgentRingNoAnswer
eventfilter=Event: Bridge
eventfilter=Event: BridgeEnter
eventfilter=Event: DialBegin
eventfilter=Event: Hangup
eventfilter=Event: Hold
eventfilter=Event: MusicOnHoldStart
eventfilter=Event: MusicOnHoldStop
eventfilter=Event: NewCallerid
eventfilter=Event: Newchannel
eventfilter=Event: Unhold
eventfilter=Event: UserEvent
eventfilter=Event: QueueCallerAbandon

А по такому шаблону можно указать только те переменные для VarSet, которые нам необходимы:

eventfilter=Variable: QUEUE_NAME
eventfilter=Variable: BRIDGEPEER
eventfilter=Variable: ALARM_QUEUE_ID
eventfilter=Variable: LOCAL_CALL_TO
eventfilter=Variable: AUTODIAL_NUM
eventfilter=Variable: SUBSCRIBER_DIAL
eventfilter=Variable: SUBSCRIBER_CALLERID
eventfilter=Variable: TICKET
eventfilter=Variable: DIALSTATUS
eventfilter=Variable: AUTO_MONITOR
eventfilter=Variable: REVERSE_DIAL
eventfilter=Variable: REALNUMBER
eventfilter=Variable: DAMAGE_EXISTS
eventfilter=Variable: PLANNED_KTV
eventfilter=Variable: DEBT_EXISTS

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

Если раньше мы в среднем имели 20-50 событий в очереди, а в пиках нагрузка могла доходить до 200 событий, то теперь у нас в среднем 1-2 события в очереди, а пиковые состояния доходят до 4.

Как мы внедряли ARI

Мы начали использовать ARI в нашей инфраструктуре. Например, ARI Proxy — модуль для Golang, позволяющий обеспечить горизонтальное масштабирование ARI-приложений и упрощающий работу с внешними сервисами.

Ранее мы работали с внешними API напрямую через приложение диалплана CURL. Из-за этого нам приходилось везде по-разному, в зависимости от запроса, парсить ответ от API, тратить строки диалплана на валидацию и обработку. Это было неудобно.

Теперь же с использованием приложения на ARI Proxy, в которое мы проваливаемся одной строчкой через Stasis, можем лаконично и единообразно обрабатывать запросы и ответы.

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

Второй пример: мы начали снимать данные по каналам на предмет джиттера для отображения возможных проблем с голосом и оперативного принятия мер по их устранению. Для этого используем приложение на Golang с использованием CyCoreSystems/ARI.

Также постепенно уходим от AMI. Все новые приложения пишем уже на ARI, а старое стараемся разбирать и переписывать.

Выводы

Подводя итог, осуществленное обновление позволило нам достичь следующих успехов:

  • Разработали инструмент для настройки серверов с актуальной версией Asterisk из пакетов. Напомню, этим инструментом стала роль Ansible.

  • Создали отказоустойчивую систему унифицированной и расширяемой обработки звонков посредством оптимизации и изолирования работы с AMI.

  • Начали внедрение ARI, что помимо удобств в нашей работе позволяет снизить порог входа для новых разработчиков, которые будут взаимодействовать с Asterisk с помощью привычного им REST-интерфейса.

На что обратить внимание при обновлении версии Asterisk:

  • совместимость используемых вами библиотек в приложениях; 

  • нюансы при работе с AMI, например, смена имен событий.

Удачных обновлений! Надеюсь, наш опыт поможет вам. 

Спасибо за внимание!

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


  1. turbidit
    05.04.2023 05:53

    Интересно как у вас реализована кластеризация Астерисков.

    актуальной версией Asterisk из пакетов

    что за дистрибутив такой?


    1. GlebMusin Автор
      05.04.2023 05:53

      Как упомянуто в статье, кластер организован из четырех одинаковых серверов Asterisk, балансировка между которыми производится с помощью Kamailio по алгоритму Round-Robin.

      Дистрибутив - CentOS 7 с использованием репозитория от Sangoma.


  1. select26
    05.04.2023 05:53

    Глеб, спасибо за интересную статью!
    Особенно за технические детали.
    Только "САМОПИСНОЕ API" режет глаз. Примерно как "ПРОПАЛО СОБАКО".
    API - все же interface. Он. Не апишечка.


    1. GlebMusin Автор
      05.04.2023 05:53

      Соглашусь, благодарю за замечание и отклик. :)


  1. Lazhu
    05.04.2023 05:53

    многочисленные FastAGI скрипты, написанные на языке Perl

    Еще бы на чистом шелле написали.

    Про https://github.com/marcelog никогда не слышали? 100% ООП, работает начиная с версии 1.8. Добавить event/action - 5 минут


    1. GlebMusin Автор
      05.04.2023 05:53

      Скрипты на Perl достались нам от предыдущих коллег, стараемся от них уходить. :) Что-то только поддерживаем, что-то переписали на Python и Golang с использованием panoramisk и CyCoreSystems соответственно. Про PAMI и PAGI слышали, но использовать не приходилось.


  1. neiroman2k
    05.04.2023 05:53

    А как мигрировали realtime БД ?


    1. GlebMusin Автор
      05.04.2023 05:53

      У нас realtime БД почти не используется, поэтому необходимости в миграции как таковой не было.