Как разделить маршрутизацию и логику звонка в Asterisk
Маршрутизацию, меню и очереди я оставляю в диалплане. Короткий HTTP-запрос можно сделать через CURL с настроенным таймаутом; разбор данных из CRM и несколько внешних шагов удобнее вынести в AGI или FastAGI. ARI нужен, когда отдельное приложение должно управлять каналами и мостами. Ниже — критерии выбора и безопасный порядок изменений.
Почему extensions.conf превращается в лабиринт
Типовая история: телефонию поднял подрядчик пять лет назад, потом каждый новый запрос директора дописывали прямо в extensions.conf. Сначала меню, потом переадресация на мобильные, потом «если звонит постоянный клиент — сразу на менеджера», потом проверка задолженности. Через пару лет это файл на несколько тысяч строк, где GotoIf прыгает в контекст, который включён через include в другой контекст. Когда мы берём такую систему на обслуживание сервера телефонии и ИТ-аутсорсинг, первым делом слышим: «только ничего не сломайте, там всё работает». Работает — пока никто не дышит рядом.
Проблема не в том, что диалплан плохой. Это хороший язык для того, для чего он создан: принять канал, решить, куда его отправить, проиграть файл, поставить в очередь, записать разговор. Проблема начинается, когда в него пытаются запихнуть бизнес-логику: циклы по спискам, разбор ответа от CRM строковыми функциями, хранение состояния в переменных канала и в astdb одновременно. У диалплана есть свои средства просмотра и отладки, но сложную бизнес-логику в нём труднее отдельно тестировать и менять. Если каждый новый сценарий добавляет переходы и исключения, разбор реального пути звонка превращается в ручную работу.
Второй источник хаоса — обратный перекос. Кто-то начитался про AGI и сделал так, что на каждый входящий звонок стартует PHP-скрипт, который сам решает всё: меню, очередь, запись. Диалплан там из трёх строк, а вся логика — в скрипте, который пишет один человек и который не обновлялся с тех пор, как этот человек уволился. Лабиринт никуда не делся — он просто переехал из конфигурации в код. Если внешний скрипт не обрабатывает ошибку или зависает на запросе к CRM, абонент может ждать вместо перехода к запасному маршруту. Это не повод обвинять AGI: это повод описать отказ и проверить сценарий без CRM.
Что Asterisk умеет без внешних скриптов
Прежде чем выносить логику наружу, стоит честно посмотреть, что диалплан делает сам. Ветвления — GotoIf и ExecIf. Подпрограммы — Gosub с аргументами и Return, это нормальная замена копипасте одинаковых блоков по десятку внутренних номеров. Время работы — приложение GotoIfTime. Хранение простых флагов вроде «ночной режим включён» — функция DB() во встроенной базе astdb. Запросы во внешние HTTP-сервисы — функция CURL(), к SQL-базе — func_odbc. Одного процента для всех офисов не существует: объём зависит от сценариев и установленных модулей. Моя граница практическая: пока условие понятно в диалплане и не требует сложной обработки данных, оставляю его там.
Главное преимущество диалплана — он исполняется внутри процесса Asterisk. Для обычного диалплана не запускается отдельный AGI-скрипт; сетевой вызов появится только если его задаёт сам сценарий. Перечитать диалплан можно командой dialplan reload: документация Asterisk указывает, что это загружает новую конфигурацию без нарушения работы PBX. Перед изменением всё равно проверьте синтаксис и сделайте тестовый звонок. Посмотреть, что реально загружено, — dialplan show имя_контекста. Это дешёвый, быстрый и предсказуемый слой, и я хочу, чтобы маршрутизация звонков жила именно там. Всё, что лежит в диалплане, видно в одном месте, проверяется в консоли и не зависит от того, жив ли соседний сервер с CRM или упал после ночного обновления.
Читаемость диалплана — вопрос дисциплины, а не языка. Мы раскладываем конфиг на файлы через #include: отдельно входящие по транкам, отдельно внутренние, отдельно подпрограммы. Каждая подпрограмма — один смысл: sub-record для записи, sub-worktime для проверки графика, sub-crm-lookup для обращения к CRM. Метки вместо номеров приоритетов, комментарий над каждым контекстом. Такую раскладку проще искать и сравнивать в системе контроля версий. Отдельный плюс: такой конфиг удобно сравнивать между версиями, и можно проверить, в каком изменении появился новый маршрут.
- Маршрутизация по DID и времени — диалплан
- Меню IVR и очереди — диалплан
- Запись разговоров и пред-обработчики Dial — диалплан, через Gosub
- Простые флаги режимов — DB() в astdb
- Одиночный HTTP-запрос с коротким ответом — CURL() с таймаутом
Когда без AGI действительно не обойтись
AGI нужен там, где начинается настоящая программа: несколько запросов к разным системам, разбор JSON, обработка ошибок, повторные попытки, логирование в понятном виде. Пример: по номеру звонящего найти контрагента в CRM, проверить, закреплён ли за ним менеджер, свободен ли менеджер, и если нет — выбрать дежурного из того же отдела. На диалплане это реально написать, но сопровождать — нет. В отдельном скрипте проще разделить шаги, разобрать структурированный ответ, записать диагностические данные и проверить ветви ошибки. Логируйте идентификатор вызова и итог решения, а персональные данные и секреты не записывайте без необходимости. В диалплане ту же диагностику придётся собирать из вывода консоли по кусочкам.
Механика простая: приложение AGI() в диалплане передаёт управление внешней программе, та общается с Asterisk через stdin и stdout командами AGI; Asterisk передаёт канал и окружение скрипту, а скрипт отправляет команды протокола. Например, GET VARIABLE читает переменную канала, SET VARIABLE задаёт её. Обычный AGI запускает новый процесс на каждый вызов, и при скрипте на интерпретируемом языке это ощутимые накладные расходы: старт интерпретатора, загрузка библиотек, подключение к базе. Время запуска и нагрузку надо измерять на своей системе: язык, библиотеки и работа самого скрипта меняют результат. Если один процесс при каждом вызове загружает большой справочник или долго ждёт базу, ищите узкое место там, а не в названии интерфейса.
FastAGI использует тот же протокол через TCP-соединение с внешним сервисом; URI имеет вид agi://узел/скрипт, порт задаётся в URI или настройке сервиса. Это позволяет держать процесс и соединения к CRM вне Asterisk, но добавляет сетевую зависимость. Если же логика должна полностью управлять звонком — мосты, переводы, сложные сценарии с событиями, — смотрите в сторону ARI: там звонок передаётся приложению Stasis, а дальше вы управляете каналами через REST и WebSocket. ARI выбирают, когда приложению нужны события и управление каналами, а не просто один ответ CRM. Для простой маршрутизации это лишний уровень ответственности.
Производительность и отказоустойчивость: честное сравнение
Скорость здесь нельзя честно свести к универсальной таблице миллисекунд: результат зависит от кода, сети, модулей и нагрузки. Разница в устройстве заметнее: обычный AGI запускает локальную программу, FastAGI подключается к внешнему TCP-сервису, а ARI требует приложения, которое работает с событиями и управляет каналами через API. Для этих вариантов важны разные точки отказа.
Внешняя система может отвечать медленно или быть недоступной. Поэтому после AGI() проверяйте AGISTATUS: официальная документация перечисляет SUCCESS, FAILURE, NOTFOUND и HANGUP. Сам SUCCESS говорит о результате запуска AGI, но не подтверждает, что бизнес-ответ CRM корректен. Скрипт должен отдельно вернуть понятный результат, а диалплан — проверить его и отправить вызов в согласованную очередь или на другой резервный маршрут при ошибке. Не оставляйте абонента ждать неизвестно чего.
Для CURL() в диалплане задавайте лимиты ожидания через CURLOPT(): для HTTP-действия в документации есть httptimeout, для установления соединения — conntimeout, также можно ограничить DNS-ожидание через dnstimeout. Это именно настройки libcurl в поддерживаемой сборке Asterisk; проверьте наличие функции и поведение на своей версии. Пустой ответ — не единственный признак сбоя: проверяйте и формат, и код ответа, а на ошибке выбирайте запасной маршрут. Для диагностики AGI используйте CLI-команду agi set debug on временно и выключайте подробный вывод после разбора.
Практический разбор: CRM отдельно, маршрут — в диалплане
Покажу типовую схему без привязки к реальному заказчику и без выдуманной статистики. В старой конфигурации встречается такой узел: диалплан вызывает CRM, разбирает ответ строковыми функциями и по результату выбирает внутренний номер. Пока CRM отдаёт данные в ожидаемом формате, сценарий выглядит рабочим. После изменения ответа проверка может тихо выбрать неверную ветку. Здесь проблема не в количестве строк, а в том, что формат интеграции и правила маршрутизации смешаны.
Я разделяю ответственность. Диалплан получает результат вроде «назначенный сотрудник» или «не найден», проверяет его и решает, куда передать вызов. Внешняя программа обращается к CRM, проверяет ответ, журналирует исход запроса и возвращает короткий статус. Если CRM недоступна, сценарий не должен выдавать техническую ошибку абоненту: он отправляет звонок в заранее выбранную общую очередь. Сам таймаут выбирают по SLA и допустимому ожиданию, а не копируют магическое число из чужого примера.
Перед переключением я сохраняю исходную конфигурацию, записываю контракт между диалпланом и скриптом и проверяю четыре исхода: найден сотрудник, запись не найдена, CRM вернула некорректный ответ, CRM не отвечает. Затем проверяю сам маршрут тестовым звонком и смотрю журнал. Так видно, что именно произошло, и возврат к прежнему вызову AGI остаётся простым. Ни один из этих шагов не требует заявлять, что мы сократили чужой конфиг втрое или ускорили работу на выдуманный процент.
Как провести границу у себя: пошаговый план
Начните с инвентаризации. Выгрузите dialplan show в файл, составьте список контекстов и найдите переходы, которые на них ссылаются. Отдельно проверьте входы от транков и каналов: отсутствие очевидного Goto ещё не доказывает, что контекст мёртв. Затем разметьте каждый оставшийся кусок: это маршрутизация звонка или это бизнес-правило. Маршрутизация остаётся, бизнес-правила с внешними данными и сложными условиями едут в FastAGI. Не пытайтесь переписать всё за один раз: выносите по одной функции, проверяйте, потом следующую. До проверки держите прежний сценарий доступным для быстрого возврата; длительность отката зависит от того, как устроен конфиг и релизный процесс.
Дальше — контракт между слоями. Скрипт получает ограниченный набор входных переменных и выставляет ограниченный набор выходных, например CRM_MANAGER и CRM_STATUS. Диалплан ничего не знает о том, как устроена CRM, а скрипт ничего не знает о том, как устроены очереди. Такой контракт пишется на половину страницы, и именно он спасает при смене подрядчика или CRM. Новый программист читает контракт и сразу понимает, что от него ждут, а не реконструирует логику по чужому коду. Описание лежит рядом с конфигом в том же репозитории, а не в почте уволившегося админа.
И последнее: конфиги — в git. Это не прихоть разработчиков, а способ ответить на вопрос «что поменяли вчера, что всё сломалось». Перед каждым dialplan reload — коммит, после — проверка тестовым звонком по чек-листу: входящий в рабочее время, в нерабочее, на закреплённого менеджера, при выключенной интеграции. Короткая проверка маршрутов дешевле, чем разбирать пропущенные звонки после выкладки. И держите копию рабочего конфига, к которой можно вернуться одной командой, — страховкой пользуются редко, но всегда в самый неудобный момент.
- Выгрузить dialplan show и найти мёртвые контексты
- Разделить маршрутизацию и бизнес-правила
- Вынести внешние запросы в FastAGI с таймаутом
- Описать контракт переменных между слоями
- Положить конфиги и скрипты в git
- Проверять каждую правку тестовыми звонками
Частые вопросы
Что быстрее — диалплан или AGI?
Диалплан не создаёт отдельный AGI-процесс для выполнения своей логики. Обычный AGI запускает локальную программу, FastAGI подключается к внешнему сервису; фактическую задержку нужно измерять на своей конфигурации.
Чем FastAGI отличается от AGI?
FastAGI использует AGI-протокол поверх TCP: Asterisk подключается к постоянно работающему сервису по URI agi://. Это снимает запуск локального скрипта при каждом вызове, но добавляет зависимость от сервиса и сети.
Нужен ли ARI небольшому офису?
Чаще нет. ARI нужен, когда приложение должно полностью управлять звонками и мостами. Для интеграции с CRM и умной маршрутизации обычно хватает диалплана и FastAGI.
Можно ли перечитать диалплан без обрыва звонков?
Да. Команда `dialplan reload` загружает диалплан без нарушения работы PBX. После изменения всё равно проверьте вывод `dialplan show` и выполните тестовый вызов.
Что делать, если AGI-скрипт завис?
Ограничьте ожидание ответа CRM внутри скрипта и задайте запасной маршрут. После `AGI()` проверяйте `AGISTATUS` и отдельный статус бизнес-ответа: `SUCCESS` не гарантирует корректность данных CRM.
Источники
- Asterisk Documentation — AGI() — Варианты AGI, запуск локального скрипта и FastAGI по TCP, переменная AGISTATUS и её значения: https://docs.asterisk.org/Asterisk_22_Documentation/API_Documentation/Dialplan_Applications/AGI/
- Asterisk Documentation — Dialplan — Назначение диалплана, путь к extensions.conf и перечитывание через dialplan reload без нарушения работы PBX: https://docs.asterisk.org/Configuration/Dialplan/
- Asterisk Documentation — CURLOPT() — Параметры ожидания соединения, DNS и HTTP-ответа: https://docs.asterisk.org/Asterisk_24_Documentation/API_Documentation/Dialplan_Functions/CURLOPT/
- Asterisk Documentation — Gosub — Вызов подпрограмм диалплана и возврат через Return(): https://docs.asterisk.org/Configuration/Dialplan/Subroutines/Gosub/
- Asterisk Documentation — ARI — ARI как REST API и WebSocket интерфейс для приложений Stasis: https://docs.asterisk.org/Configuration/Interfaces/Asterisk-REST-Interface-ARI/



