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

—

от автора

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

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

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

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

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

  • 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


ссылка на оригинал статьи https://habr.com/ru/articles/717856/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *