Делаем отказоустойчивый Asterisk realtime

Если вы спросите у прожжённых системных администраторов, используют ли они realtime‑конфигурацию в Asterisk, с вероятностью 90% ответ будет отрицательный. В качестве обоснования, скорее всего, услышите «При недоступности источников данных телефония станет неработоспособной». Если интересно узнать, как мы обошли это ограничение, читайте дальше.

Тяжела и неказиста жизнь простого программиста
Предупреждение об осторожности при использовании описываемого решения
  • Данная модификация «из коробки» позволяет использовать только backend на основе cURL. Для использования других backend требуется сделать в них соответствующие доработки.

  • Протестированы только те участки кода, которые используются в наших конфигурациях. Часть изменений была сделана с прицелом «на будущее» и полноценно не проверялась.

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

Постановка задачи

Исследуем инструменты

Посмотрим, что есть в Asterisk «из коробки». Sorcery позволяет хранить конфигурационные данные где угодно, используя для доступа к ним соответствующие backend. Можно указать один или несколько источников, в том числе разного типа. Уже хорошо. Но телефония откажет при полной недоступности их всех. Конечно у нас несколько территориально распределённых ЦОД‑ов и такая ситуация маловероятна, но ведь бутерброд падает маслом вниз, не так ли?

Также «из коробки» предоставляется модуль кэширования. Причём достаточно интересный: для каждого типа данных можно указать полное время жизни (expire), в течении которого они сохраняются в кэше, и время потенциального устаревания (stale). Если какой‑либо подсистеме Asterisk требуется получить конкретный элемент, то sorcery сначала проверит, нет ли его в кэше. Если он найден и период stale не истёк, то данные возвращается из кэша, при этом обращений к backend не происходит. Если же период stale истёк, то данные опять‑таки возвращаются из кэша, но при этом запускается фоновый процесс его обновления.

Создаем конфигурацию

Так как у нас несколько серверов Asterisk, надо их как-то идентифицировать на provision-сервисе.

Укажем для каждого сервера уникальный идентификатор (asterisk.conf):

[options] 
entityid=12:34:56:78:9a:bc      ; Entity ID

Опишем сами источники данных (extconfig.conf):

[settings]
ps_endpoints => curl,http://192.168.75.37:7000/provision/asterisk/${ENTITYID}/endpoint
ps_auths => curl,http://192.168.75.37:7000/provision/asterisk/${ENTITYID}/auth
ps_aors => curl,http://192.168.75.37:7000/provision/asterisk/${ENTITYID}/aor

И их применение в качестве поставщиков параметров абонентов с учетом применения кеширования (sorcery.conf):

[res_pjsip]
endpoint/cache = memory_cache,expire_on_reload=yes,object_lifetime_maximum=86400,object_lifetime_stale=60
endpoint=realtime,ps_endpoints
auth/cache = memory_cache,expire_on_reload=yes,object_lifetime_maximum=86400,object_lifetime_stale=60
auth=realtime,ps_auths
aor/cache = memory_cache,expire_on_reload=yes,object_lifetime_maximum=86400,object_lifetime_stale=60
aor=realtime,ps_aors

Если помимо realtime необходимо добавить статическую конфигурацию, добавляем в sorcery.conf параметры следующего вида:

aor=config,pjsip.conf,criteria=type=aor
endpoint=config,pjsip.conf,criteria=type=endpoint
auth=config,pjsip.conf,criteria=type=auth

Место добавления зависит от того, с каким приоритетом вы хотите их использовать. Если требуется, чтобы в случае нахождения информации в текстовом конфигурационном файле обращений к realtime backend не производилось, эти строки следует расположить выше строк с типом «realtime». Если же обращение к realtime должно происходить в любом случае, и только при отсутствии в нём запрошенных данных информация должна браться из текстовых конфигурационных файлов, то строки добавляются после соответствующих строк с типом «realtime».

Опишем параметры для поставщика данных cURL (res_curl.conf):

[globals]
conntimeout=1
dnstimeout=1
httptimeout=2
followlocation=true
httpheader=Content-Type: application/x-www-form-urlencoded
httpheader=x-app-name: asterisk-server
httpheader=x-app-version: 18.12.1

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

Запросы к сервису-поставщику данных всегда отправляются при помощи HTTP метода POST. Конкретный URL зависит от типа запроса.

Запрос одиночного элемента типа endpoint с явно указанным идентификатором id:

http://192.168.75.37:7000/provision/asterisk/12:34:56:78:9a:bc/endpoint/single

Запрос множества элементов типа endpoint (при этом параметр id используется в качестве фильтра):

http://192.168.75.37:7000/provision/asterisk/12:34:56:78:9a:bc/endpoint/multi

«Если что-нибудь может пойти не так, оно пойдёт не так» (закон Мёрфи)

Запускаем сервис‑поставщик данных, запускаем Asterisk, пытаемся зарегистрировать абонента. Ура, получилось!

Останавливаем сервис конфигурации, и… абонент мгновенно исчезает из кэша. А вот это уже непорядок. Ладно... Если не получилось решить проблему штатными средствами, придётся немного поработать руками.

Определение причины такого поведения

Ставим точку останова на функцию в backend, в которой обрабатывается ошибка источника данных, и начинаем обратную трассировку. После прохождения возврата из двух уровней вложенности — бинго! Классическая ошибка дизайна: в sorcery не предусмотрена обработка ошибок backend, данные ожидаются только в двух вариантах: «элемент найден» (возвращаемое значение не NULL) и «элемент не найден» (возвращаемое значение NULL). Так что ситуации «элемент не найден по причине его отсутствия в источнике данных» и «элемент не найден по причине ошибки источника данных» для sorcery неотличимы. Как следствие, при остановке сервиса все элементы удаляются как отсутствующие во всех источниках данных.

Посмотрели реализацию sorcery начиная с 12-й версии Asterisk и заканчивая последней доступной на данный момент 20+ — ничего не изменилось: обработку ошибок добавлять никто не собирается.

«Ну и ладно!», — сказали Суровые Сибирские Мужики

Если это упростит дальнейшую жизнь, то сделаем всё сами. Начнём с того, что в модуле func_curl.c, используемом в backend res_config_curl.c, забыли о возврате статуса ошибки в случаях таймаута при соединении, ошибки DNS и других сетевых проблем. Исправляем.

Идём дальше: в res_config_curl.c запрос выполняется в виде вычисления строки, в которую включена функция диалплана CURL(). Идея хорошая, позволяет задавать части URL в виде параметров, но реализация — как всегда: потенциальные ошибки игнорируются. Исправляем относительно безопасным способом: изменяем тип данных с void на int. На использование в других местах повлиять не должно.

Ну и теперь самое весёлое: исправляем дизайн sorcery, оставив обратную совместимость. Для этого вводим аналоги функций, читающих значения из backend, но с возможностью получить также код ошибки, а не просто статус «Элемент в хранилище отсутствует». Поскольку этот статус мы сделали необязательным, старый вариант вызова реализуем через новый, добавив параметр NULL в качестве адреса для получения ошибки.

Последними вносим исправления в модуль res_sorcery_memory_cache.c, добавив проверку на ошибки backend‑а при обновлении элемента по истечении интервала stale.

Теперь остаётся сделать метрики от backend в модуле res_prometheus (знаю я наших инженеров: им хочется наблюдать за работой всего и вся) — и... А вот и нет! Модуль res_prometheus появился в Asterisk относительно недавно, видимо поэтому он обеспечивает только базовый функционал. В частности все экспортируемые динамические метрики формируются им самим по snapshot‑ам, уже существующим в ядре Asterisk для других целей. В качестве альфы сойдёт, но хотелось бы сделать что‑то более универсальное.

Всё же лучше встраивать поставщиков метрик непосредственно в модули, которые эти метрики производят. Тем более в Asterisk имеется для этого специальный механизм под названием «optional api«. Его применять очень просто: вместо обычной сигнатуры функции в h‑файле используем макрос AST_OPTIONAL_API(), а в имплементирующем модуле определяем символ AST_API_MODULE и саму функцию объявляем при помощи макроса AST_OPTIONAL_API_NAME.

Работает это так: если вызывается API, поставляемое модулем, при этом сам модуль ещё не загружен, то возвращается код ошибки AST_OPTIONAL_API_UNAVAILABLE. А вот если модуль загружен, тогда выполняется имплементируемая функция. Только следует учесть, что вызов таких API из других модулей Asterisk возможен до завершения функции load_module() имплементирующего модуля. Для каких‑то вызовов это может быть не критично, но для вызовов, изменяющих глобальное состояние модуля, стоит предусмотреть какую‑нибудь защиту.

Теперь если модуль res_prometheus загружен, то значения метрик будут экспортироваться по HTTP(S). Если модуль не загружен или его инициализация не завершена, то метрики будут собираться в том модуле, к которому они относятся.

Наконец‑то стало красиво: если конфигурационный backend имеет доступ к источнику данных, то всё работает в реальном времени. Если источник данных по какой‑то причине недоступен (сетевые проблемы) или неработоспособен (возвращает 5xx), то время жизни элемента в кэше автоматически продлевается на значение параметра stale.

Стоит отметить, что стиль вносимых изменений намеренно сделан аналогичным тому, как написаны оригинальные модули. Конечно маловероятно, что данный патч в обозримом будущем попадёт в mainline, но чем чёрт не шутит? Почему‑то мантайнеры очень не любят, когда стиль резко отличается от общего стиля модуля или системы в целом.

 Справочная информация:

  1. Sorcery

  2. Sorcery caching

  3. cURL configuration backend

  4. Optional API

  5. Патч для Asterisk 20.1.0

@Vedga
21.02.2023 14:41 UTC
Первоисточник

Комментарии

@
21.02.2023 10:00 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@Vedga
21.02.2023 10:16 UTC
0

Если сервис упадет, Asterisk продолжит работу в состоянии, предшествующему аварийной ситуации.

21.02.2023 10:27 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@IGoverdovskiy
21.02.2023 13:51 UTC
0

Даже для среднего бизнеса это слишком "масштабное" решение относительно трудозатрат на запуск телефонии. Но вот для всего крупного бизнеса, однозначно полезное и нужное решение. Тут даже обсуждать нечего.

@dprotopopov
21.02.2023 15:03 UTC
+1

А крупному не очень-то и надо - они покупают решения от вендоров и обычно по результатам тендеров

@arheops
22.02.2023 00:41 UTC
0
Крупному агресивная маркетинговая политика Freeswitch довела, что они должны все делать на FS.
Эту войну диджиум уже походу проиграли.
В реальной жизни разница в скорости больше зависит от архитектуры системы, но рекламная компания работает, крупные бизнесы даже с успешными кейсами 50+ тыс каналов на астериск затребывают разработку новых сервисов на FS.
@arheops
21.02.2023 20:03 UTC
0
Угу. Теперь у вас астериск зависит не только от базы данных, которую проще было задуалить.
Но также от 1) кеширующей части 2) веб сервиса 3) ну и от базы тоже.
Также это очевидно не исправляет ситуации «база работает, но данные удалены».
@Vedga
22.02.2023 15:14 UTC
0

БД и так реплицирована в 3-х датацентрах (..."клиентская балансировка между источниками данных в разных ЦОД‑ах является зоной ответственности отдельной команды"...). Пока жив хотя бы один, будет штатный режим работы.
Нас интересовал случай, когда ничего внешнее недоступно. В этом случае телефония должна работать как минимум в аварийном режиме (т.е. вся локальная связь плюс городские операторы, если они заведены не через ЦОД).

22.02.2023 18:19 UTC
0
Для этого традиционно пишут в файлы.
При правильной организации это работает сильно лучше и для «локальных» систем вполне доступно.
В любом случае не вижу смысла городить такую сложную систему для ЛОКАЛЬНЫХ атс.
22.02.2023 19:26 UTC
0

А кто сказал, что у нас система локальная? Она как раз вполне себе распределенная, только в случае аварий поддерживает минимальный функционал в локальном режиме.
Раскатывать файлами на 60+ серверов во-первых долго, во-вторых сбои тоже возможны, в-третьих dialplan/pjsip reload здоровья Asterisk не добавляют. Клиента надо включить здесь и сейчас, при этом конфигурация абонента должна быть на всех серверах, ибо роуминг (автоматический, конечно. Без костылей на AMI/ARI/AGI).

22.02.2023 19:28 UTC
0
Можно подумать realtime добавляет здоровья. Особенно вот так, как у вас описано.
Не надо «раскатывать файлами» на 60 серверов. Обновляйте только те части, которые поменялися. Если все равно часто — разбейте ваших пользователей на псевдо-группы.
Перегружать без изменений тоже не стоит
Хотя да, если не думать — видел я системы где только диалплан перегружается 4 минуты. Ну просто там по 100 строчек на каждый DID из 5 тысяч.

В любом случае статический диалплан гараздо более оттестирован. И делать в реалтайм части, которая меняется — еще неподдерживаемые патчи — ну таксебе идея. Особенно если оно по сути ничего и не даст.
@IgorG
21.03.2023 18:03 UTC
0

Патч точно не "пролезет", если его нет в gerrit. Стоит его залить и получить feedback от разработчиков из sangoma.