Если вы спросите у прожжённых системных администраторов, используют ли они 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, но чем чёрт не шутит? Почему‑то мантайнеры очень не любят, когда стиль резко отличается от общего стиля модуля или системы в целом.
Справочная информация:
ссылка на оригинал статьи https://habr.com/ru/articles/717856/
Добавить комментарий