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

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

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

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

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

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

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

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

  • Алгоритм обработки вызова</p>" data-abbr="Диалплан">Диалплан модифицируется достаточно редко, для него делать realtime особого смысла нет. А вот параметры абонентов изменяются активно, для них это будет подходящее решение.

  • В случае недоступности источника данных работоспособность телефонии должна сохраняться как минимум на уровне, предшествующем аварийной ситуации.

  • Изменения конфигурации должны применяться на серверах за минимально возможный промежуток времени.

  • Серверы Asterisk не должны создавать чрезмерную нагрузку на источники данных.

  • В Ozon клиентская балансировка между источниками данных в разных ЦОД‑ах является зоной ответственности отдельной команды, поэтому для нас (как потребителей) видимой является только одна общая точка входа. Которая может быть или доступна (если доступен хотя бы один ЦОД), или недоступна.

  • Решение не должно добавлять новые потенциальные точки отказа.

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

Посмотрим, что есть в 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 в виде параметров, но реализация — как всегда: потенциальные ошибки игнорируются. Исправляем относительно безопасным способом: изменяем тип функции с voip на 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

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


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

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

Привет, Хабр! Сегодня мы позвали в наш блог Валерию Скворцову — ассистента Лаборатории робототехники Университета Иннополис, чтобы она рассказала о разработке научного прототипа робота для реабилитаци...
В маркетинге очень популярен когортный анализ. Его популярность вызвана, скорее всего, легкостью алгоритма и вычислений. Никаких серьезных математических концепций в основе нет, элементар...
Слышали про такой инструмент, как Airtable, но не знали, с чего начать? Тогда приглашаем в мир визуального программирования построения БД! Этим постом мы начинаем цикл обучающих статей, ...
Всем привет! Не так давно на работе в рамках тестирования нового бизнес-процесса мне понадобилась возможность авторизации под разными пользователями. Переход в соответствующий р...
Битрикс24 — популярная в малом бизнесе CRM c большими возможностями даже на бесплатном тарифе. Благодаря API Битрикс24 (даже в облачной редакции) можно легко интегрировать с другими системами.