Отказоустойчивый VoIP-сервер в облаке

Статья для специалистов, желающих получить знания об архитектуре отказоустойчивых VoIP-систем, с примерами конфигураций.
Читать 5 минут
Поделиться

Мы в Axmor реализовали более десятка VoIP-проектов, например, такие:

Хотим обобщить свой опыт и поделиться с вами. Данная статья рассчитана на специалистов, желающих получить знания об архитектуре отказоустойчивых VoIP-систем. Базовое знание FreeSWITCH, Asterisk или аналогов желательно, но не является обязательным. В статье мы приведем примеры реальных конфигураций для организации кластера.

Введение в VoIP

Если слова FreeSWITCH, Asterisk, SIP, RTP, WebRTC для вас не пустой звук, можете смело пропускать эту часть.

Мир VoIP стоит на двух больших слонах: SIP и RTP. Эти 2 протокола были разработаны в конце прошлого века и пришли к нам из мира телефонии. SIP — командный протокол. Отвечает за выбор кодеков, начало/конец звонка, управление звонком (hold/transfer/etc) и имеет огромное количество расширений, в том числе для отсылки текстовых сообщений, нотификации об оставленных голосовых сообщениях и т.д.

IP-телефоны разных производителей могут поддерживать свой собственный набор расширений. Например, весьма распространен BLF (Busy Lamp Field) — поле светодиодов на настольном телефоне, которые можно настроить так, что они будут показывать статус телефона другого сотрудника.

RTP — медиапротокол. Отвечает за передачу аудио- и видеоданных. Использует внутри себя различные кодеки для кодирования медиаданных.

Оба протокола по умолчанию реализованы поверх UDP. TCP как опция есть почти у всех, но UDP лучше подходит по целому ряду причин. Также для обоих протоколов доступно шифрование.

В архитектуру VoIP заложена идея о том, что SIP- и RTP-сервер — это два разных сервера, поэтому встречаются реализации серверов, поддерживающих только SIP или только RTP. Тем не менее FreeSWITCH и Asterisk — это open source сервера, поддерживающие оба протокола и многое другое. Выбор между ними во многом зависит от личных предпочтений, требований и задач интеграции, но оба позволяют из коробки получить офисную мини-АТС.

Нельзя не упомянуть и WebRTC. Де факто это стандарт для голосовых звонков в web. WebRTC под капотом использует SRTP (secure RTP), оставляя реализацию командного слоя на откуп JS-коду. Для p2p-звонков нам нужно всего лишь передать второй стороне свой адрес и некие параметры звонка, что можно сделать на базе любого протокола. Если же нам нужна интеграция с SIP-сервером, то обычно используется протокол SIP over WebSocket. Реализация — sipjs.com.

FreeSWITCH поддерживает как SIP over WebSocket, так и собственный альтернативный протокол, реализованный модулем mod_vertoo. Он разработан специально для интеграции с WebRTC и решает неприятные проблемы несовместимости WebRTC и SIP, приводящие к задержкам 1-5 секунд перед созданием звонка. Впрочем, эти проблемы можно решать и по-другому, на стороне JS, но это тема для отдельной статьи.

Как и в случае обычных аналоговых звонков, VoIP требует наличия АТС, чтобы пользователи могли найти друг друга по некому номеру телефона и чтобы обойти проблему отсутствия белых IP у клиентов. После того, как клиенты нашли друг друга и договорились о кодеках, необходимо установить соединение для потока медиаданных. В зависимости от требований проекта и конфигурации сети, поток данных может идти напрямую от клиента к клиенту, либо через сервер.

Мир VoIP обычно тяготеет к пробросу трафика через сервер. Это решает проблему отсутствия белых IP у клиентов и позволяет централизованно подслушивать/записывать любой разговор.

WebRTC по умолчанию предлагает прямое соединение между клиентами, но это не работает в случае симметричного файрвола, когда у обоих клиентов нет белого IP. В таких случаях нам нужен медиапрокси-сервер STUN/TURN. Для коммерческих проектов вам придется устанавливать и оплачивать свой собственный прокси-сервер, чтобы ваше WebRTC based-приложение работало. Бесплатно браузеры предоставляют только услуги STUN сервера — распознавание внешнего IP-клиента, а вот проксирование трафика (TURN) — уже платно.

Примечание:

Есть технология, позволяющая в теории устанавливать соединение двум клиентам без белых IP c небольшой помощью сервера, но в наших проектах такой необходимости не возникало. Называется эта технология «симметричный NAT».
Если верить документации, Freeswitch поддерживает симметричный NAT. Но мы советуем тщательно протестировать перед использованием в production.

Масштабирование SIP-траффика с помощью OpenSIPs

FreeSWITCH и Asterisk позволяют обрабатывать на одной машине огромное количество одновременных звонков — 500-1000. Но что делать, если нам надо больше? Или если в ТЗ сразу заложена необходимость облака с балансировкой нагрузки?

Тут на помощь приходит SIP-балансировщик нагрузки. Мы выделяем одну машину под балансировщик, который на уровне SIP выбирает конечный узел и перенаправляет туда обработку медиатрафика. Через балансировщик не проходит RTP media-трафик, поэтому он способен выдержать значительно большее количество одновременных звонков. Фактически после создания звонка нагрузка исчезает.

Выбор обычно идёт между OpenSIPs и Kamailio. Оба проекта имеют общего предка и схожую структуру модулей. Сравнение и выбор конкретного решения мы оставим для отдельной статьи, а пока расскажем об OpenSIPs.

Многие уже успели поработать с FreeSWITCH и/или Asterisk, в сети достаточно how-to примеров, но настройка SIP-балансировщика в OpenSIPs или Kamailio — это совершенно другое.

FreeSWITCH и Asterisk требуют всего лишь задать список пользователей и довольно простых правил роутинга звонков. Более того, есть бесплатные админпанели, которые позволят вам сконфигурировать всё в веб-интерфейсе.

В противовес, OpenSIPs и Kamailio заставляют пользователя разобраться с протоколом SIP хотя бы на начальном уровне и вручную прописать, что делать с каждым SIP-сообщением.

Конфигурация OpenSIPs — это программа на верхнеуровневом псевдоязыке, которая должна увязать между собой в единую систему 10+ разных модулей. Список модулей значительно больше, но часть дублирует друг друга, а часть может не понадобиться.

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

Если же вы хотите добавить много кастомной логики, то в какой-то момент может оказаться проще вынести бизнес-логику в FreeSWITCH (Asterisk) и/или отдельный сервер, который и будет управлять роутингом звонков, очередями и т.д.

Простейший скрипт для балансировки нагрузки выглядит следующим образом:

  1. Если входящее SIP-сообщение не первое, и уже известно куда его роутить, передаем управление модулю dialog.
  2. Иначе вызываем модуль balancer для поиска менее загруженной ноды, проставляем её адрес в target и передаем управление модулю dialog.
  3. Обрабатываем ошибки:
    • Выбранная нода может вернуть код ошибки (аналогично http-кодам), и нам надо прописать, что делать в каждом случае — сбросить звонок или попробовать выбрать другую ноду.
    • Модуль balancer может вернуть ошибку и сказать, что свободных нод нет.

Очень важный момент:

Когда вы пишете программы обработки для OpenSIPs, помните, что вы должны обработать как сообщения, идущие снаружи в ваше облако, так и сообщение, идущее из вашего облака наружу. Поэтому зачастую программа бьется на 2-3 огромных If-блока в зависимости от направления SIP-пакета.

Пример:

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

route {
if (!has_totag()) {
#Первое сообщение в диалоге, запоминаем его в базе и не выходим, надо найти куда его роутить
record_route();
} else {
#Не первое сообщение, роутим по уже проторенной дороге и выходим
loose_route();
t_relay();
exit;
}


//Ищем свободный узел. 1 - вес нашего запроса, call-ресурс. Можно сделать сложную логику и выделять под разные звонки разный набор машин
load_balance("1","call");
if ($retcode<0) {
//нет свободных узлов
sl_send_reply("500","Service full");
exit;
}

xlog("Selected destination is: $du\n");

if (!t_relay()) {
sl_reply_error();
}
}

Особенности OpenSIPs:

  1. Значения заголовков не меняются в процессе выполнения программы. Если вы что-то присвоите и потом попробуете вывести в лог, выведется первоначальное значение.
  2. Есть 2 разных типа пользовательских переменных, и они несовместимы. Часть модулей использует одни, часть другие.
  3. Многие функции ничего не возвращают, а просто делают что-то полезное, а что именно, можно узнать только полностью прочитав документацию и иногда — исходники. Например, load_balance() проставляет значение переменной $du. А record_route() сохраняет в заголовке сообщения $du, но при этом вызывается раньше load_balance(). Явное нарушение причинно-следственной связи. Предполагаем, что заголовки считаются в самом конце, и record_route() скорее проставляет некий флажок или ссылку.
  4. Stateless-парадигма. SIP позволяет все необходимые для роутинга данные хранить прямо в заголовках сообщений. Клиент же обязан их копировать при ответе, что позволяет серверу быть stateless. Тем не менее, по необходимости можно сохранять в базе таблицу роутинга и не оповещать весь мир о внутренней структуре своего кластера, но это добавляет нагрузку на базу и задержки. Выбор за вами.

Отказоустойчивость и масштабирование VoIP-систем

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

На уровне протокола VoIP поддерживает возможность восстановления звонка даже при отказе сервера.

  1. SIP failure. По умолчанию SIP использует протокол UDP, который в отличие от TCP не устанавливает постоянного соединения. Но клиент перерегистрируется примерно каждую минуту (настраивается). Фактически на каждый пакет от клиента может отвечать новый сервер. Единственное ограничение — IP-адрес ответного пакета не должен меняться, и нам надо где-то хранить состояние звонков пользователя (если они есть в данный момент), чтобы любой сервер мог работать с этими данными.
  2. RTP failure. Если у нас по каким-то причинам отказал RTP-сервер, который также использует UDP-протокол, мы не можем легко восстановить медиапоток, но мы можем послать через SIP-протокол команду пересоздания потока (reinvite). В идеале клиент заметит всего лишь выпадение нескольких секунд аудио. Под вопросом остается реализация обнаружения падения. Решения из коробки нет.

Рассмотрим разные варианты архитектур.

Нужен VoIP-проект? Напишите нам, и мы оценим его бесплатно.

Резервирование SIP-сервера

Детали реализации сильно отличаются в зависимости от используемого сервера, не меняется только одно — необходимость в Virtual (floating) IP, который мы можем динамически переназначить на другой сервер.

Задача решается на уровне shell-скриптов, которыми мы проверяем, работает ли сервер, и если нет, переназначаем IP на другой. Главная проблема — баланс между ложными срабатываниями и долгим ожиданием перед переключением.

Но это не всё. SIP-сервер хранит информацию о подключенных клиентах, и чтобы не потерять её, мы должны сконфигурировать наш сервер, чтобы он сохранял эту информацию в распределенную базу данных. Тогда в случае рестарта или переключения на slave мы не потеряем никаких данных. Практически все популярные SIP-сервера поддерживают использование баз данных из коробки.

Масштабирование SIP-серверов

Правильно реализованное масштабирование заодно решает и проблему отказоустойчивости. Умер один сервер — возьмем другой. Главная проблема — SIP-клиент игнорирует пакеты, посланные с другого IP. Пример «Как работать не будет»:

Клиент B просто проигнорирует входящий звонок (invite), потому что его source IP отличается от сервера, на котором клиент зарегистрирован.

Как работать будет? В зависимости от конкретного проекта, имеющегося железа и наших возможностей, проблему можно решать 3 способами.

Подмена IP-адреса пакетов

Самый простой вариант — поставить ещё более простой (и быстрый) load balancer, который подменит source IP на свой для всех исходящих пакетов. И спрятать все SIP-сервера за него. Но нам нужно настроить все SIP-сервера на использование общей базы данных.

Очевидный плюс — простота настройки.

Очевидные минусы:

  1. Single Poing of Failure в виде load balancer.
  2. Может не хватить пропускной способности load balancer.

Проброс команды Invite

Настраиваем ферму SIP-серверов с разными IP-адресами, DNS в режиме round robin. Клиенты цепляются случайным образом к одному из множества серверов.

Программируем SIP-сервер пересылать invite нужному серверу (на котором зарегистрирован другой клиент), чтобы уже он от своего имени переслал команду клиенту.

Плюс — всё замечательно масштабируется.

Небольшой минус — SIP register действительно хорошо масштабируется, а вот SIP invite и прочие команды, относящиеся к звонку, масштабируются чуть хуже, потому что с большой вероятностью будут проходить через 2 узла. Впрочем, register случается значительно чаще.

Шардинг

Если наше приложение — это SaaS для бизнеса, то с большой вероятностью клиентам разных компаний не надо звонить друг другу, и с помощью хорошо подобранной hash-функции мы можем раскидать множество наших кастомеров по разным SIP-серверам.

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

Хуже, если мы используем чисто SIP-клиент. Тогда имеет смысл решать проблему на уровне DNS и/или роутинга. Например, выдать каждой компании свой адрес VoIP-сервера и динамически менять таблицы DNS.

Резервирование RTP-сервера

В общем случае, нам необходимо отследить падение RTP-сервера и послать reinvite обоим клиентам.

Задача разбивается на несколько подпунктов:

  1. Сохранить SIP-диалог в базу данных.
  2. Отследить смерть сервера.
  3. Послать reinvite для каждого диалога.

Детали реализации сильно отличаются в зависимости от конфигурации кластера.

В качестве примера: FreeSWITCH имеет встроенную команду ‘sofia recover’ для восстановления звонков после рестарта сервера. Но судя по отзывам, эта команда не будет работать в случае кластера из FreeSWITCH-серверов. Здесь вам наверняка понадобится кастомная разработка.

Масштабирование RTP-сервера

Очевидный способ масштабировать медиапотоки — вынести их на отдельный от SIP-трафика сервер. Ниже рассмотрим разные по сложности варианты архитектуры, от простого к сложному.

OpenSIPs|Kamailio + RTP proxy

SIP load balancer в лице OpenSIPs или Kamailio управляет RTP Proxy или его аналогом. Общение между ними осуществляется неким протоколом (не SIP), в рамках которого load balancer должен суметь сделать запрос на выделение портов для RTP-протокола, чтобы отправить эти данные клиентам. Клиент открывает новое соединение по полученному IP и порту, соединяясь с RTP proxy.

Вся бизнес-логика сосредоточена внутри SIP load balancer. Решение подходит, если у вас очень мало бизнес-логики, и всё, что вам нужно — коммутировать звонки.

Фактически тут даже не обязательно использовать SIP-сервер. Один из наших проектов коммутации для службы 911 был реализован на Java: Маршрутизация телефонных звонков.

OpenSIPs|Kamailio + FreeSWITCH|Asterisk

SIP load balancer в лице OpenSIPs или Kamailio пробрасывает через себя SIP-пакеты до выбранного FreeSWITCH|Asterisk сервера. Самая близкая аналогия — http load balancer с липкими сессиями. OpenSIPs cам обрабатывает только регистрацию. При инициализации нового диалога (звонка), он по какой-то стратегии выбирает наименее загруженную ноду Freeswitch и перенаправляет туда все SIP-пакеты, связанные с этим диалогом. И уже сам Freeswitch, обрабатывая SIP-пакеты клиента, выделяет у себя RTP-порт и шлёт номер порта и собственный белый IP через OpenSIPs клиенту.

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

Это решение лучше подойдёт для систем с IVR, Voicemail, Call Center и прочими фичами продвинутых PBX-систем.

Пример:

Система унифицированных коммуникаций

OpenSIPs|Kamailio + FreeSWITCH + App server

Решение аналогично предыдущему, но вся бизнес-логика вынесена на Application server. Такой подход позволяет избавиться от необходимости использования lua-скрипта, но добавляет потенциальную точку отказа.

С другой стороны, если у вас уже есть Application server, и он уже является жизненно важным элементом вашего Unified Communication-решения, то почему бы и нет. В FreeSWITCH есть модуль xml_curl, который позволяет всю конфигурацию загружать по http со стороннего сервера. На каждый запрос, будь то авторизация или входящий звонок, FreeSWITCH сначала запрашивает по http xml инструкции, а потом их выполняет. В остальном схема аналогична.

Нужен VoIP-проект? Напишите нам, и мы оценим его бесплатно.

Опубликовано 06 февраля 2020
Автор Ксения Николаева
Поделиться