Asterisk — это болид «Формулы-1», а не рейсовый автобус

Asterisk — фи, это же моветон


Здравствуйте уважаемые читатели этого замечательного ресурса. По уже сложившейся традиции — являюсь давним читателем habr'а, но только сейчас решил сделать пост. Что, собственно, побудило к написанию? Честно сказать, и сам не знаю. То ли притянутые статьи о производительности FreeSWITCH/Yate/3CX/etc в сравнении с Asterisk, то ли действительные, реальные проблемы архитектуры последнего, а, возможно, желание сделать что-нибудь уникальное.


И что удивительно, в первом случае, как правило, сравнивают мягкое и теплое, так сказать, FreeSWITCH/Yate/etc и FreePBX. Да-да, именно FreePBX. Это не опечатка. Причем интересно, что во всех сравнениях зачастую один Asterisk в дефолтной конфигурации. Ну, вы знаете, эта конфигурация — загруженные все имеющиеся модули, кривой диалплан (FreePBX как бы способствует) и куча остальной необъективщины. Что до родовых болячек Asterisk'а — да, объективно их вагон и маленькая тележка.


Что со всем этим делать? Разрушать стереотипы и исправлять родовые травмы. Этим и займемся.


Скрещиваем ежа с ужом


Многие из новичков испытывают дискомфорт, глядя на синтаксис описания диалплана в Asterisk'е, а некоторые на полном серьезе обосновывают выбор другого сервера телефонии именно необходимостью писать диалплан в том виде, в котором он есть по дефолту. Типа, перелопачивать многострочный XML — это верх комфорта. Да, есть возможность юзать LUA/AEL, и это хорошо. Но лично я отнес бы эту возможность в минусы и в частности то, что касается pbx_lua.


Как уже было сказано, иметь возможность описывать диалплан полноценным языком программирования — это хорошо. Проблема в том, что время жизни скрипта и его окружения равно времени жизни канала. Для каждого нового канала запускается свой экземпляр скрипта, следовательно, прощай, разделяемые между каналами переменные, единичная загрузка сторонних модулей, один разделяемый коннект к базе и т.д., и т.п. Строго говоря, от встраиваемого языка описания сценария этого и не нужно, но уж сильно хочется. А если хочется – значит, надо.


Итак, от классического Asterisk'а возьмем принципы pbx_lua, от Yate возьмем модель маршрутизации, а от FreeSWITCH ничего брать не будем, ибо "overhead" не нужен. ОК, с тем, что нам нужно родить, определились. Что же будем использовать для генетических экспериментов:


  • Asterisk, причем хотелось бы без привязок к версии. Тот же ARI был анонсирован, если мне не изменяет память, с 12-й версии. Если учесть, что до сих пор где то юзаются 1.8/1.6, а возможно и 1.4, то зависимость от версионных плюшек нам не нужна.
  • Lua — замечательный, гибкий и крайне функциональный скриптовый язык. Сам бог велел, так сказать, без комментариев.
  • Lunapark — интересный проект на github'е, своего рода сервер voip-приложений.

Про Lunapark стоит рассказать подробнее. Это сервер, реализующий потенциал AMI-протокола в связке с классическим FastAGI, что немаловажно в едином пространстве выполнения. То есть, получаем аналог ARI посредством тесной кооперации AGI и AMI в одном флаконе.


Предвижу логичный вопрос: для чего это все? Есть же Asterisk REST Interface, чей функционал ты тут пытаешься переизобрести! Ответ на этот вопрос неоднозначен. Согласен, ARI декларирует ряд преимуществ: да, он асинхронен, да, позволяет работать с "сырыми" примитивами, WebSockets и да, стильный, модный, молодежный XML/JSON — куда ж без него. Но, черт возьми, часть этих так называемых преимуществ крайне сомнительна и добавляет один, а то и более уровней абстракции. Другая же часть — вообще не преимущества. Преимущества — это когда что-то свойственно только тебе, ниже мы это увидим на примере той же асинхронности.


Как это работает? Стандартными средствами заворачиваем канал в FastAGI-приложение, внутри которого получаем возможность управлять звонком, как будто юзаем pbx_lua с незначительным изменением синтаксиса. Вишенкой на торте является возможность управлять состоянием самого Asterisk’а и окружением канала, для этого в распоряжении текущего FastAGI-приложения есть глобальный AMI-объект. Кстати, можно не заворачивать канал в FastAGI-приложение, а создать глобальный обработчик события, допустим, для NewChannel. А это уже преимущество по сравнению с ARI, там как известно, вне stasis'а ARI слеп.


Реализован Lunapark в лучших традициях кооперативной многозадачности, а именно всеми любимая асинхронность на сопрограммах. И как следствие отсутствие проблем с "shared data". То есть плюсы присутствуют, но и проблемы появляются. Одна из них — это необходимость описывать логику с оглядкой на асинхронность, но я думаю, это мы как-нибудь переживем.


Что дальше?


Синтаксис описания контекстов — а что же с ним не так? Да все с ним нормально, более того, практику написания диалплана нужно прописывать как профилактику для формирования структурированного мышления у новичков. Но, вместе с тем нужно понижать порог вхождения. Поэтому будем упрощать и в то же время добавлять функционала.


Простой пример:


[test]
exten => _XXX/102,1,Hangup()
exten => _XXX,1,Dial(SIP/${EXTEN})

В этом примере идет дозвон до трехзнака, кроме абонента 102. Вроде бы все логично и лаконично за исключением того, что шаблоны соответствия экстеншена ограничены небольшим набором правил, а так называемая extended маршрутизация возможна только по CallerID звонящего. А хотелось бы, к примеру, по CallerIDName или по текущему состоянию звонящего канала, а возможно по имени самого канала, а если реализовать полноценный regexp, так вообще красота. И да, я знаю, все эти хотелки можно реализовать, расписав контекст в таком виде:


[test]
exten => _XXX/102,1,Hangup()

; по CallerIDName
exten => _XXX,1,ExecIf($[ "${CALLERID(name)}" == "Vasya" ]?Hangup())

; По состоянию канала
exten => _XXX,n,ExecIf($[ "${CHANNEL(state)}" != "Ring" ]?Hangup())

; По имени канала
exten => _XXX,n,ExecIf($[ "${CUT(CUT(CHANNEL,-,1),/,2)}" == "333" ]?Hangup())

exten => _XXX,n,Dial(SIP/${EXTEN})

Но мой внутренний перфекционист начинает бунтовать при виде такого, а если представить аналогичную выборку по всем пользователям, да еще и действия нужны разные и посложнее Hangup'а, то extensions.conf превращается в длииинную портянку вызовов Goto, GoSub, Macro и, не дай бог, с каналами типа Local.


Выход один — прикручивать свои правила маршрутизации с подкидным и дамами с низкой социальной ответственностью.


В качестве примера:


${Exten}:match('%d%d%d')
           and 
(
  ${CallerIDNum}:match('201') or 
  ${CallerIDName}:match('Vasya') or 
  ${State}:lower() ~= 'ring' or 
  ${Channel}:match('^[^/]+/([^%-]+)') == '333'
) => Hangup();

${Exten}:match('%d%d%d') => Dial {callee = ('SIP/%s'):format(${Exten})};

Хм, вырвиглазненько получилось, но на удивление читается и понимается с первого взгляда. А самое главное, что у нас появился аналог regexp'ов и группировка правил на действие, что, несомненно, упростит составление маршрутов в будущем.


Что тут думать, прыгать надо.


В итоге имеем Lunapark как замену pbx_lua. Его средствами нам и нужно создать логику обработки нашей модели маршрутизации. Для начала нужно распарсить набор правил и заменить все вхождения ${...} на соответствующие им значения, то есть привести к виду ('...'). Значения будут браться из окружения текущего канала.
Затем приводим каждое правило к виду условного оператора, чтобы получить нечто похожее:


-- Exten = 123
-- Sate = Ring
-- CallerIDNum = 100
-- CallerIDName = Test
-- Channel = SIP/100-00000012c

if ('123'):match('%d%d%d') and
(
  ('100'):match('201') or
  ('Test'):match('Vasya') or
  ('Ring'):lower() ~= 'ring' or
  ('SIP/100-00000012c'):match('^[^/]+/([^%-]+)') == '333'
) then
  Hangup()
end

if ('123'):match('%d%d%d') then
  Dial {callee = ('SIP/%s'):format(('123'))}
end

Делать это будут две функции fmt и syntax соответственно:


local fmt = function(str, tab)
 return (str:gsub('(%${[^}{]+})', function(w)
  local mark = w:sub(3, -2) 

  return (mark:gsub('(.+)',function(v)
   local out = tab[v] or v

   return ("('%s')"):format(out)
  end))
 end))
end

local syntax = function(str)
 return (str:gsub('([^;]+)=>([^;]+)',function(p,r)
  return ([[ 
   if %s then
    %s
   end
  ]]):format(p,r)
 end))
end

В принципе ничего сложного, все просто и понятно. Идем дальше — считывать наши правила будем из файла в переменную при старте сервиса, а парсить их уже при звонке с актуальным окружением. Считыванием правил займется функция routes.


local routes = function(...)
  local conf, content = ...

  local f, err = io.open(conf, "r")

  if io.type(f) ~= 'file' then
   log.warn(err)  -- Глобальный LOG объект доступный благодаря Lunapark'у
   return ""
  else
   content = f:read('*all')
  end

  f:close() return content
end

Осталось сделать две вещи: завернуть звонки в Lunapark и соответственно их обработать с учетом наших маршрутов. Тут стоит немного пояснить такой момент — в Lunapark вся логика описывается в handler'е. Это текстовый файл, в котором мы будем определять наши FastAGI-приложения и работать с AMI и нашими маршрутами.


Как уже было сказано, объект AMI — глобальный и, помимо роли AMI-клиента, может устанавливать своего рода слушатели для конкретных AMI событий. Этим мы и воспользуемся, но для начала сделаем некоторые приготовления в extensions.conf.


[default]
exten => _[hit],1,NoOp()
exten => _.,n,Wait(5)

exten => _.,1,AGI(agi://127.0.0.1/${EXTEN}${IF($[ "X${PRMS}" != "X" ]?"?${PRMS}")})

Wait(5) в примере выше позволит нам не обрывать канал при завершении FastAGI-приложения, так как в маршрутах может быть описано несколько приложений, а выполнение их осуществляется по средствам Redirect на контекст default по ${EXTEN}.


Таким образом, беря во внимание все выше описанное и помня о кооперативной природе Lunapark'а, попробуем закодить логику обработки маршрутов через FastAGI-приложения.


-- Считываем наши правила в переменную rules
local rules = routes('routes.conf')
-- Очищаем все обработчики, таким образом очистятся только не именные обработчики
-- Это даст возможность не затирать цепочку выполнения при сигналах HUP/QUIT
ami.removeEvents('*')
-- Обработчик события создания нового канала
ami.addEvents {
 ['newchannel'] = function(e)
  -- Условия, только каналы с набором и каналы с контекстом users
  if (e['Context'] and e['Context']:match('users')) and e['Exten'] then
   -- Переменная, указывающая на выполнении, какого FastAGI приложения мы находимся
   local step
   -- Будущий порядковый номер FatsAGI приложения в цепочке выполнения
   local count = 0
   -- Парсим маршруты для текущего окружения канала
   local code, err = loadstring(syntax(fmt(rules,e))) 
   -- В описании маршрутов нет ошибок, двигаемся дальше
   if type(code) == 'function' then
    -- Проксируем будущие FastAGI приложения 
    setfenv(code,setmetatable({indexes = {}},{__index = function(t,k)
     -- Вот они последствия кооперативности
     return coroutine.wrap(
      function(...)
       local prms = {} -- Будущие параметры FastAGI приложения
       local owner = t -- Копия окружения
       local event = e -- Копия таблицы event
       local thread = coroutine.running() -- ID текущей сопрограммы 
       -- Парсим параметры и приводим к виду URI
       for p,v in pairs({...}) do
        if type(v) == 'table' then
         for key, val in pairs(v) do
          table.insert(prms,("%s=%s"):format(key,val))
         end
        else
         table.insert(prms,("%s=%s"):format(p,v))
        end
       end
       -- Если это не первое FastAGI приложение в цепочке
       if step then
        -- Запоминаем предыдущее перед этим
        local last = ("%s"):format(step)
        -- Добавляем ИМЕННЫЕ обработчики события UserEvent по доп. условиям
        -- И записываем в таблицу indexes(в окружении) их порядковые номера
        -- Именные обработчики требуют последующего удаления самостоятельно
        table.insert(owner['indexes'],ami.addEvent('UserEvent',function(evt)
         -- Ловим событие AGIStatus указывающее на завершение приложения
         -- Если это предыдущее перед нами, пробуждаем сопрограмму
         if (evt['Channel'] and evt['Channel'] == event['Channel'])
               and
          (evt['UserEvent'] and evt['UserEvent']:match('AGIStatus'))
               and
          (evt['Script'] and evt['Script'] == last)
         then
          -- Соответствие порядкового номера нашей сопрограмме
          -- В цепочке может быть вызов одного приложения несколько раз
          -- Это позволит выполнять сопрограммы в порядке их определения
          if owner['indexes'][count] == thread then
           if coroutine.status(thread) ~= 'dead' then
            coroutine.resume(thread)
           end
          end
         end
        end,thread))
        -- Устанавливаем маркер текущего FastAGI приложения
        step = k
        -- Приостанавливаем сопрограмму
        coroutine.yield()
       else -- Здесь обрабатывается первое FastAGI приложение в цепочке
        local index -- Индекс для обработчика Hangup события
        -- Устанавливаем маркер текущего FastAGI приложения
        step = k
        -- Добавляем ИМЕННОЙ обработчик события Hangup для канала
        -- В этом месте подчищаем за собой
        index = ami.addEvent('Hangup',function(evt)
         if evt['Channel'] and evt['Channel'] == event['Channel'] then
          -- Удаляем обработчик событие Hangup по ранее запомненному индексу
          ami.removeEvent('Hangup',index)
          -- Удаляем все обработчики цепочек выполнения по индексу
          for _,v in pairs(owner['indexes']) do
           ami.removeEvent('UserEvent',v)
          end
          -- Делаем приятно сборщику мусора
          owner = nil
         end
        end,thread)
       end
       -- По средствам AMI выставляем переменную для канала и вызова в цепочке
       ami.setvar{
        Value = table.concat(prms,'&'),
        Channel = event['Channel'],
        Variable = 'PRMS'
       }
       -- Перенаправляем канал на AGI-приложение через контекст default
       ami.redirect{
        Exten = k,
        Priority = 1,
        Channel = event['Channel'],
        Context = 'default'
       }
       -- Выставляем индекс приложения
       count = count + 1
      end)
    end}))()
   else
    -- Если что-то пошло не так
    log.warn(err)
   end
  end
 end
}

Для тех, кто не смотрел или не понял код, поясню простым языком, что же мы там накодили. Для каждого действия в маршруте создается сопрограмма с копированием всего, чего нужно через замыкания и в приостановленном состоянии. Кроме сопрограммы первого действия, она выполняется как есть. Приостанавливаем мы их для эмуляции последовательного выполнения, то есть строго одна за другой, вторая после первой, третья после второй и т.д.


Стоп, так они и так друг за другом выполняются. На самом деле нет — если не эмулировать синхронность, то асинхронный redirect пробежится по каждому действию и займет это доли секунды. В нашем же коде мы выполняем каждое действие, по наступлению определенного события, а именно завершении предыдущего FastAGI-приложения. Lunapark заботливо генерирует специальный UserEvent по окончании выполнения каждого FastAGI-приложения с соответствующими параметрами — вот на это событие и ориентируемся. Сами же сопрограммы просто редиректят текущий канал в контекст default с экстеншном, равным текущему действию, предварительно установив переменную канала PRMS.


Самое интересное, что звонок после redirect'а придет опять в handler, но уже в контексте выполнения AGI и на соответствующее приложение. В нашем случае это Hangup() и Dial(). Давайте же напишем их для полноты повествования.


function Hangup(...)
  local app, channel = ... -- В этом отличие от pbx_lua

  app.verbose(('The Channel %s does not match by routing rules'):format(channel.get('CHANNEL')))
  app.hangup()
end

function Dial(...)
  local app, channel = ...
  local leg = app.agi.params['callee'] or ''

  app.verbose(('Trying to make a call from %s to %s'):format(
   channel.get('CALLERID(num)'),
   leg:match('^[^/]+/([^%-]+)'))
  )
  app.dial(leg)
end

Ну, вот и все — допрыгались


Итак, давайте подытожим. Что же мы получили в результате этих генетических экспериментов?


  • гибкий, функциональный подход описания маршрутов;
  • возможность создания полнофункциональных VoIP-приложений. Теперь нам не нужно приложение Queue, мы можем его сами написать, не заморачиваясь с созданием своего собственного модуля для asterisk'а;
  • вынесли логику формирования, управления звонками на сторону сервера VoIP-приложений, тем самым сделав из asterisk'а Mediahub, что позволило повысить производительность VoIP-системы в целом;
  • возможность использовать достаточно простой, расширяемый и очень гибкий скриптовый язык для создания VoIP-приложений;
  • расширили возможности интеграции с внешними системами из VoIP-приложений.

Кому как, а мне пока все нравится.


handler целиком
local fmt = function(str, tab)
 return (str:gsub('(%${[^}{]+})', function(w)
  local mark = w:sub(3, -2) 

  return (mark:gsub('(.+)',function(v)
   local out = tab[v] or v

   return ("('%s')"):format(out)
  end))
 end))
end

local syntax = function(str)
 return (str:gsub('([^;]+)=>([^;]+)',function(p,r)
  return ([[ 
   if %s then
    %s
   end
  ]]):format(p,r)
 end))
end

local routes = function(...)
  local conf, content = ...

  local f, err = io.open(conf, "r")

  if io.type(f) ~= 'file' then
   log.warn(err)  -- Глобальный LOG объект доступный благодаря Lunapark'у
   return ""
  else
   content = f:read('*all')
  end

  f:close() return content
end

-- Считываем наши правила в переменную rules
local rules = routes('routes.conf')
-- Очищаем все обработчики, причем таким образом очистятся только неименные обработчики событий
-- Это даст возможность не затирать цепочку выполнения при сигналах HUP/QUIT
ami.removeEvents('*')
-- Обработчик события создания нового канала
ami.addEvents {
 ['newchannel'] = function(e)
  -- Условия, только каналы с набором и каналы с контекстом users
  if (e['Context'] and e['Context']:match('users')) and e['Exten'] then
   local step -- Переменная, указывающая на выполнении, какого FastAGI приложения мы находимся
   local count = 0 -- Будущий порядковый номер FatsAGI приложения в цепочке выполнения
   -- Парсим маршруты для текущего окружения канала
   local code, err = loadstring(syntax(fmt(rules,e))) 
   -- В описании маршрутов нет ошибок, двигаемся дальше
   if type(code) == 'function' then
    -- Проксируем будущие FastAGI приложения 
    setfenv(code,setmetatable({indexes = {}},{__index = function(t,k)
     -- Вот они последствия кооперативности
     return coroutine.wrap(
      function(...)
       local prms = {} -- Будущие параметры FastAGI приложения
       local owner = t -- Копия окружения
       local event = e -- Копия таблицы event
       local thread = coroutine.running() -- ID текущей сопрограммы 
       -- Парсим параметры и приводим к виду URI
       for p,v in pairs({...}) do
        if type(v) == 'table' then
         for key, val in pairs(v) do
          table.insert(prms,("%s=%s"):format(key,val))
         end
        else
         table.insert(prms,("%s=%s"):format(p,v))
        end
       end
       -- Если это не первое FastAGI приложение в цепочке
       if step then
        -- Запоминаем предыдущее перед этим
        local last = ("%s"):format(step)
        -- Добавляем ИМЕННЫЕ обработчики события UserEvent по доп. условиям
        -- И записываем в таблицу indexes(в окружении) их порядковые номера
        -- Именные обработчики требуют последующего удаления самостоятельно
        table.insert(owner['indexes'],ami.addEvent('UserEvent',function(evt)
         -- Ловим событие AGIStatus указывающее на завершение приложения
         -- Если это предыдущее перед нами, пробуждаем сопрограмму
         if (evt['Channel'] and evt['Channel'] == event['Channel'])
               and
          (evt['UserEvent'] and evt['UserEvent']:match('AGIStatus'))
               and
          (evt['Script'] and evt['Script'] == last)
         then
          -- Соответствие порядкового номера нашей сопрограмме
          -- В цепочке может быть вызов одного приложения несколько раз
          -- Это позволит выполнять сопрограммы в порядке их определения
          if owner['indexes'][count] == thread then
           if coroutine.status(thread) ~= 'dead' then
            coroutine.resume(thread)
           end
          end
         end
        end,thread))
        -- Устанавливаем маркер текущего FastAGI приложения
        step = k
        -- Приостанавливаем сопрограмму
        coroutine.yield()
       else -- Здесь обрабатывается первое FastAGI приложение в цепочке
        local index -- Индекс для обработчика Hangup события
        -- Устанавливаем маркер текущего FastAGI приложения
        step = k
        -- Добавляем ИМЕННОЙ обработчик события Hangup для канала
        -- В этом месте подчищаем за собой
        index = ami.addEvent('Hangup',function(evt)
         if evt['Channel'] and evt['Channel'] == event['Channel'] then
          -- Удаляем обработчик событие Hangup по ранее запомненному индексу
          ami.removeEvent('Hangup',index)
          -- Удаляем все обработчики цепочек выполнения по индексу
          for _,v in pairs(owner['indexes']) do
           ami.removeEvent('UserEvent',v)
          end
          -- Делаем приятно сборщику мусора
          owner = nil
         end
        end,thread)
       end
       -- По средствам AMI выставляем переменную для канала и вызова в цепочке
       ami.setvar{
        Value = table.concat(prms,'&'),
        Channel = event['Channel'],
        Variable = 'PRMS'
       }
       -- Перенаправляем канал на AGI-приложение через контекст default
       ami.redirect{
        Exten = k,
        Priority = 1,
        Channel = event['Channel'],
        Context = 'default'
       }
       -- Выставляем индекс приложения
       count = count + 1
      end)
    end}))()
   else
    -- Если что-то пошло не так
    log.warn(err)
   end
  end
 end
}

function Hangup(...)
  local app, channel = ... -- В этом отличие от pbx_lua

  app.verbose(('The Channel %s does not match by routing rules'):format(channel.get('CHANNEL')))
  app.hangup()
 end

function Dial(...)
  local app, channel = ...
  local leg = app.agi.params['callee'] or ''

  app.verbose(('Trying to make a call from %s to %s'):format(
   channel.get('CALLERID(num)'),
   leg:match('^[^/]+/([^%-]+)'))
  )
  app.dial(leg)
 end

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 31

    0
    Не плохое расширение функционала, но тяжеловато все же читать lua. Часто использую Asterisk и для себя нашел несколько минусов по сравнению с тем же freeswitch:
    1. Проблемы в webrtc из коробки зачастую просто так не заводится, требует обязательно wss
    2. по мне pjsip очень не удобен, кучу конфигов для настройки и усложненный мониторинг
      0

      Хм, странно. Lua как раз и был выбран из-за его простоты и наглядности.
      По поводу WebRTC — я придерживаюсь мнения, что Кесарю кесарево, а браузеру браузерово. Ну, то есть, web — это web, а voip — это voip и лучше их не смешивать.
      Если же нужно web звонилку, то юзаю kamailio с rtpengine и в астериск приходит чистый VoIP. Лучше этого пока не нашел.
      С PJSIP да, согласен — хотели как лучше, а получилось как всегда. Можно же было поменять движок, но оставить синтаксис тех же конфигов.

      0
      Почему FastAGI, а не AGI:async? Напомнило Adhearsion ))
        +1

        Привет землякам…
        Почему не AsyncAGI? Хм, наверное, потому что это AGI контрролируемый через AMI, а это и так реализовано в lunopark'е.
        Мы же можем накатать что-то типа:


        ami.removeEvents('*')
        
        ami.addEvent('AsyncAGI',function(e)
          if e['SubEvent'] and e['SubEvent'] == 'Start' then
            ami.Agi{channel = e['Channel'], command = 'VERBOSE "Processing inside AsyncAGI"'}
            ami.Agi{channel = e['Channel'],command = 'ASYNCAGI BREAK'}
          end
        end)

        Но это, ну так себе, не красиво. Да и согласитесь с FastAGI как-то по понятней получилось.

          0
          Привет землякам ))

          Может и понятней. с async agi как бы одна точка подключения, а так получается две. но с другой стороны — все одно звонок в agi заводить.

          а еще с корутинами вопрос: т.е. для каждого вызова работает своя корутина? если в ней происходит ошибка, то как идет работа с другими звонками?
            0
            а еще с корутинами вопрос: т.е. для каждого вызова работает своя корутина? если в ней происходит ошибка, то как идет работа с другими звонками?

            Если коротко, то в лог/stderr падает ошибка с причиной и т.д и т.п. и работа продолжается.

          +1
          во, во) да и спорное решение, ты прекрасно помнишь как я отношусь к AMI, и проектов на нём.
            0
            да, помню )) AMI, AGI, ARI… когда будет AUI — Asterisk Universal Interface? ))
          0
          Круто. А есть сравнение, ну, например, с чистым диалпланом?

          Сравнивать с freepbx бесполезно, там большая часть ядра написана людьми не понимающими, как что работает…

          А по поводу скорости с моей точки зрения на первом месте диалплан+func_odbc+ cel. Lua и AMI все же прилично грузят систему, особенно если много евентов генерится(например, куча очередей).
            0

            С чистым диалпланом не сравнивал, так как давно перешел на аналогичные системы управления.
            Если тесты накидаете, могу заморочиться.


            на первом месте диалплан+func_odbc+ cel

            Вынужден не согласиться, CEL еще тот монстр.


            Lua и AMI все же прилично грузят систему

            Но не в этом случае. AMI из всех возможных подходов самый легковесный, а Lua, в данном случае, вообще вынесен из астериска во вне.
            Опять-таки, luajit. Можно же lunapark через него запустить.

              0
              Ну зато CEL можно выключить все, что не надо, а AMI если включен, процессит все сообщения(как минимум внутри) и ничего не сделаешь.
              Вообще странно, куча штампов. Вы начинаете статью с отсылкам к скорости, и вообще ее не измеряете, как и нагрузку. Ну и «возможность описывать диалплан полноценным языком» — та еще шняга, особенно учитывая, что как языки, что диалплан, что lua — где-то на одном уровне как по понятности так и по выразительности.
                0
                Ну зато CEL можно выключить все, что не надо

                В AMI то же. Только делается это не очевидными способами. Да и eventfilter еще ни кто не отменял.


                и вообще ее не измеряете, как и нагрузку.

                Измеряю, но не публикую, ибо смысл вне сравнения. Я могу написать, что аналогичные подходы упираются в пропускную способность сети, а никак не в производительность сервера. Но это все вода без противоположенных результатов тестов.


                Основной посыл поста — это не тесты производительности в сравнении, а то, что его (asterisk) не умеют "готовить".


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

                Хм, громкое заявление. Впрочем, тут уж кому как.

                  0
                  Хотелось бы узнать как производите измерения, за какими показателями следите и какие используете величины для измерения?
                  Так же на сколько я понял ваше приложение одно-поточно, а если это так то как масштабировать его? Если это не так то как консолидировать память потоков?
                    0

                    Измерений как таковых нет, задачи не стояло.


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

                      0
                      Бытует мнение что при увеличение нагрузки и усложнении логики вы можете упереться в ресурс одного ядра и как следствие замедление работы asterisk.
                      Те же очереди заставили нас призадуматься, lua и asterisk не лучший выбор, но самый простой при небольших объемах(до 10-15 cps).

                      Так же хочу отметить что asterisk, к сожалению, не про скорость, как мне кажется.
                      Это скорее не болид, а комбайн с пекарней и магазином на борту, дает много функционала, но жертвует «скоростью».
                      Одна из причин модули SIP, chan_sip — deadlock при больших нагрузках, pjsip — не производителен.

                      Но если всё же хочется делать что-то подобное на asterisk ведь есть же GLOBAL для общей кучи, подключение к бд можно как через res_odbc с пулом, так и на каждый звонок, например lua.mysql(dba конечно спасибо не скажут).
                      Вы рассматривали такие варианты? Если да, то в чем минусы?
                        0
                        Бытует мнение что при увеличение нагрузки и усложнении логики вы можете упереться в ресурс одного ядра и как следствие замедление работы asterisk.

                        Бытует мнение, что настаящая многозадачность возможна только на многопроцессорных системах. Все остальное это псевдо-многозадачность, в которой накладные расходы по переключению задач куда значительней чем на переключение контекстов в сопраграммах.


                        Те же очереди заставили нас призадуматься, lua и asterisk не лучший выбор

                        Если вы под lua имеете в виду pbx_lua, то это не то, в посте/статье описывается совсем другой подход, в посте не используется астерисковский pbx_lua. А по поводу последнего я высказался в самом посте.


                        Так же хочу отметить что asterisk, к сожалению, не про скорость, как мне кажется. Это скорее не болид, а комбайн с пекарней и магазином на борту, дает много функционала, но жертвует «скоростью».

                        Вы сейчас перефразировали посыл поста. Я про то же, ни что не мешает выкинуть пекарню, магазин и прочее с борта, есть вероятность что получите скорость.
                        Про chan_sip — что есть то есть, тут уж ни чего не поделаешь, а вот pjsip — это вы зря.


                        например lua.mysql(dba конечно спасибо не скажут).

                        Сейчас точно уверен, что вы о pbx_lua — еще раз акцентирую внимание на том что в посте совсем другой подход. Асинхронность требует определенного подхода, вы не можете внутри асинхронного кода просто вызвать конект к базе и выполнить запрос. Точнее можете, ни кто вам не помешает, но если запрос подвиснет, то подвиснет вашь поток.

                          0
                          Если вы под lua имеете в виду pbx_lua, то это не то, в посте/статье описывается совсем другой подход, в посте не используется астерисковский pbx_lua.

                          Я не о pbx_lua, а в целом о задаче и её решении с помощью asterisk и lua/python/php/perl…

                          Я про то же, ни что не мешает выкинуть пекарню, магазин и прочее с борта

                          Конструкции несущие))

                          а вот pjsip — это вы зря

                          А можно чуть больше информации?

                          Сейчас точно уверен, что вы о pbx_lua

                          Верно.

                          Точнее можете, ни кто вам не помешает, но если запрос подвиснет, то подвиснет вашь поток.

                          А как вы решили это в своем приложении?
                            0
                            Конструкции несущие))

                            Ну как сказать. Гляньте в modules.conf, все ли там "несущие".


                            А можно чуть больше информации?

                            Встречный вопрос, какой cps для вас выше среднего?


                            А как вы решили это в своем приложении?

                            Асинхронностью, не синхронные запросы к базе и т.д. и т.п.

                              0
                              Ну как сказать. Гляньте в modules.conf, все ли там «несущие».

                              32 modules loaded
                              если отключить что-то еще то комбайн не заработает как надо.

                              Встречный вопрос, какой cps для вас выше среднего?

                              Тут всё зависит от версии астера, модуля SIP, и функционала.
                              Очень примерно от 30 до 70. После этих пределов SIP стэк астера не успевает на тайминги и далее разного рода проблемы как снежный ком.

                              Асинхронностью, не синхронные запросы к базе и т.д. и т.п.

                              А что вам помешало реализовать в pbx_lua?

                              Мне интересно в чем именно «killer feature», ведь роутинг не требует асинхронности, а статистику можно собрать другими способами, более простыми.
            +1
            я не хочу изучать ARI и что такое stasis. Вы понимаете что при нагруженных сервисах это весьма спорное решиние ?( AMI который парсится постоянно). Преимущество REST как раз в подписке на конкретное событие.
              0
              AMI который парсится постоянно

              Так ARI то тоже парсится постоянно, ну, разве что в JSON и только подписанные события. А если вам нужны ВСЕ события, кроме скажем десятка из тысячи, вы оформляете подписку на нужные. Тоже прекрасно реализовывается через eventfilter.


              Хочу акцентировать внимание на том, что я не против ARI, я лишь топлю за то, что потенциал AGI/AMI не используется даже на 50% своих возможностей.

              0
              Кстати, никто не подскажет, Asterisk с Teams от Microsoft уже подружили?
                0
                Интересный проект конечно, но чисто из академического интереса. pbx_lua вполне себе. У астериска, как по мне, нет проблем с динамическим диалпланом. Проблемы с пользователями, очередями и в целом с механизном realtime. Вот тут как раз ему бы пригодился опыт freeswitch'a с динамической конфигурацией.
                  0
                  Интересный проект конечно, но чисто из академического интереса. pbx_lua вполне себе.

                  Так вот именно, что НЕ вполне. Сделайте тестовое логирование в extensions.lua, в самом начале перед формированием таблицы extensions. Сильно удивитесь при звонке.
                  А интерес то может быть и академический, только вот на основе такого проекта вполне себе успешно перемалывается куча voip трафика с lcr, fas detection и цепочек маршрутов.


                  Проблемы с пользователями, очередями и в целом с механизном realtime.

                  А что с realtime в астериске не так, как раз там то все вполне себе. А очереди, лично я, реализовал на основе lunaparka как voip-приложение.

                    0
                    Сделайте тестовое логирование в extensions.lua, в самом начале перед формированием таблицы extensions.

                    Не очень понял, если честно. Вы к тому что на каждый звонок дергается этот скрипт? В лучае с lunapark у вас каждый вызов это новый tcp коннект. Единственная проблема pbx_lua в том что нельзя шарить между звонками данные напрямую (например коннектор к БД. Но и тут можно найти решение. Например у меня на нагруженных системах у каждого астериска есть своя реплика postgresql с которой он читает + коннект через сокет файл.)

                    А что с realtime в астериске не так, как раз там то все вполне себе

                    Во-первых: нужно иметь определенную структуру БД(название таблиц и колонок). Если бы не view в БД, не знаю как вообще этим пользовался бы. Во-вторых: непрозрачный и не очень удобный механизм кеширования(это я про Sorcery). Ну и в целом он морально устарел(вы удивитесь сколько он делает обращений в БД). Хотелось бы более удобного механизма.

                    Смущает так же необходимость держать еще 1 сервис. Как вы его резервируете? Цепляете ли к одному сервису больше 1 астериска? Или 1 lunapark = 1 астериск?

                    Вы не подумайте, я не хейчу. Просто про этот инструмент слышу впервые. Поэтому много вопросов. Может даже соберу тестовый кластер и погоняю вызовы. Хочется знать сколько звонков и какой cps вытянет lunapark
                      0
                      Не очень понял, если честно. Вы к тому что на каждый звонок дергается этот скрипт?

                      Пример


                      local log, err = io.open("/tmp/pbx_lua.log","a")
                      
                      if log then
                        log:setvbuf("no")
                        log:write('--- iteration ---\n')
                        log:close()
                      end
                      
                      extensions = { 
                         lua = { 
                            ["000"] = function(context, extension)
                               app.Verbose("---------------- Test LUA Call -------------------------")
                            end 
                        }
                      }

                      Один звонок и смотрим в /tmp/pbx_lua.log, удивляемся…
                      Лучше новый tcp коннект, чем такое.


                      Но и тут можно найти решение. Например у меня на нагруженных системах у каждого астериска есть своя реплика postgresql с которой он читает + коннект через сокет файл.)

                      Тоже решение так себе если честно. Нагруженный сервер(asterisk), база(postgre), что то еще и все на одной "бочке", я бы разнес. Прелесть lunaparka'а в асинхронности — это как nodejs и nginx(nginx скорее event driven, но не суть) с одной стороны и apach prefork с другой.


                      По поводу realtime'а — возможно вы правы, но мне на транзите его хватает с лихвой.


                      Цепляете ли к одному сервису больше 1 астериска? Или 1 lunapark = 1 астериск?

                      Тут как бы из архитектуры все ясно. FastAGI — цепляем кучу *, а вот с AMI — один слушатель/клиент, один астер.


                      Хочется знать сколько звонков и какой cps вытянет lunapark

                      Выше писал про тесты, на "боевых" трафик размеренный, больше 64 в cps я и не видел. Из того, что имею для тестов это учетки мультифон-бизнес, с учетом многоканала(300 каналов) отрабатывают все. И это не какой то sipp тест, а с полноценным звонком на 0500 и записью.

                        0
                        Один звонок и смотрим в /tmp/pbx_lua.log, удивляемся…
                        Получил 3 записи
                        — iteration ---
                        Одна на звонок, одна на hangup hook, одна на gosub в Dial. Ничего криминального и удивительного как по мне =). Или должно было быть что-то еще?

                        Тоже решение так себе если честно. Нагруженный сервер(asterisk), база(postgre), что то еще и все на одной «бочке», я бы разнес.

                        Я может не так выразился. У каждого астериска именно реплика одной базы с настройками. Тоесть нагрузка на БД размазана. Каждый астериск читает из своей копии. Нагрузки на Master базу при этом нет вообще. Если упадет база на каком нибудь из астерисков, kamailio просто попробует позвонить через другой астериск.
                          0
                          Одна на звонок, одна на hangup hook, одна на gosub в Dial. Ничего криминального и удивительного как по мне =). Или должно было быть что-то еще?

                          OK, тогда почему на не существующий экстеншен в этом контексте записей уже 4-е. Стоп, по такой логике на каждое application будет gosub. И обратите внимание, что в примере нет Dial.


                          Да и это все не важно — такое дерганье это же файловая операция + выполнение.

                      0

                      а как делается lcr? может быть вообще выложите свой конфиг на посмотреть? )

                        0

                        хм, тут больше от запросов в базу зависит, а не от lunapark'а

                    0
                    del

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое