Scaling FastAGI Applications with Go
Lefteris Zafiris NTA LtdAstricon 2014 Las Vegas
Who we are
VoIP engineer at NTA ltd
Voice and text toolset
- Flite Asterisk App- eSpeak Asterisk App- googleTTS- speech recognition - Google speech API- Text translation
github.com/zaf
Who we are
NTA Ltd - nta.co.uk
- UK based VoIP provider since 2001- Hosted PBX services- SIP- FOSS- Kamailio / Asterisk- Perl
Perl stack
- FastAGI server
- Net::Server
- fork / prefork
- Memory
- Capacity - calls / server
Enter Go
2007 Google
Rob Pike, Ken Thompson, Robert Griesemer
- Statically typed- Garbage-collected- Natively compiled- Concurrency- C family- Pragmatic, minimalistic
Enter Go
Do not communicate by sharing memory.
Share memory by communicating.
- Goroutines
lightweight threads
cheap
multiplexed in OS threads
- Channels
communication
synchronization
Enter Go
/* A simple example in go*/
package main
import "fmt"
func main() { var msg string msg = "Hello Astricon"
// Retroactively say hello to all Astricons! for i := 0; i <= 10; i++ { fmt.Println(msg, 2004+i) }}
Go toolset
- go command
testfmtbuildinstall
- Debugging
gdb- Package management
go get
AGI Package
- AGI and FastAGI support
- Simple familiar interface
- BSD Licence
- Install:
go get github.com/zaf/agi
AGI Package
- Session structure
AGI environment variables
Env map[string]string
- Reply structure
Numeric result of the AGI command
Res intAdditional returned data
Dat string
AGI Package
- AGI commands as methods of the Session struct
func (a *Session) Answer() (Reply, error)
func (a *Session) SendText(text string) (Reply, error)
func (a *Session) Hangup(channel ...string) (Reply, error)
func (a *Session) SayDigits(digit int,escape string) (Reply, error)
AGI Package usage
- Create a new session:
myAgi := agi.New()
- Initialize, read and parse AGI environment:
myAgi.Init(nil)
- Execute AGI commands:
myAgi.StreamFile("hello-world", "#")
AGI examplepackage main
import ( "log" "github.com/zaf/agi")
func main() { // Create a new AGI session and Parse the AGI environment. myAgi := agi.New() err := myAgi.Init(nil) if err != nil { log.Fatalf("Error Parsing AGI environment: %v\n", err) } // Print a message on the asterisk console using Verbose. _, err := myAgi.Verbose("Hello World") if err != nil { log.Fatalf("AGI reply error: %v\n", err) }}
FastAGI Example
func main() { // Create a tcp listener on port 4573 and start a new goroutine for each connection. ln, err := net.Listen("tcp", ":4573") if err != nil { log.Fatal(err) } defer ln.Close() for { conn, err := ln.Accept() if err != nil { log.Println(err) continue } go connHandle(conn) }}
FastAGI Examplefunc connHandle(c net.Conn) { defer c.Close() myAgi := agi.New() // Create I/O buffers rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) err := myAgi.Init(rw) if err != nil { log.Println(err) return } rep, err := myAgi.StreamFile("hello", "1234567890#*") if err != nil { log.Println(err) return }
// Check AGI command return value if rep.Res == -1 { log.Println("Failed to playback file") }}
Error Handlingfunc connHandle(c net.Conn) { defer func() { c.Close() if err := recover(); err != nil { log.Println("Session terminated:", err) } }() myAgi := agi.New() rw := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) err := myAgi.Init(rw) checkErr(err) _, err := myAgi.Verbose("Hello World") checkErr(err)}//Check for AGI Protocol errors or hangupsfunc checkErr(e error) { if e != nil { panic(e) }}
AGI Debug
- Error testing- Performance testing- Security testing
Eliminate the use of Asterisk
Custom AGI Payloads
Agistress
A client that can connect to servers using the Asterisk Gateway Interface
- user defined payloads- user controlled session parameters- fast - parallel session spawning- performance measuring- written in Go
go get github.com/zaf/agistress
Agistress
Run once and display full debug output of the AGI session:
agistress -host 127.0.0.1 -single
Stress test the AGI server by spawning 10 sessions 10 times per second and display performance details:
agistress -host 127.0.0.1 -sess 10 -runs 10
Run once using custom AGI payload from config file:agistress -host 127.0.0.1 -single -conf payload.conf
Agistress
Config file:
- JSON format
- Environment variables
- AGI Command replies
- Reply delay
Agistress
Config file sample:
{"AgiPayload": [
{"Msg": "200 result=0", "Delay": 10}, {"Msg": "200 result=0", "Delay": 10}, {"Msg": "200 result=1 endpos=1234", "Delay": 3000}, {"Msg": "HANGUP"}
]}
Agistress
Sample output:Running FastAGI bench against: 192.168.1.10:4573Press Enter to stop.
A new run each: 100msSessions per run: 10Reply delay: 50ms
FastAGI SessionsActive: 20Completed: 520Duration: 206121479 ns (last 10000 sessions average)Failed: 0
Measuring
Comparison between FastAGI implementations:
- Perl:
Asterisk::AGI
Net::Server::PreFork
- Go:
agi package
net package
Measuring
Simple sound file Playback application:
- Start session and parse AGI Env- Parse FastAGI request URI
agi://10.11.12.13/myagi?file=echo-test
- Check channel status- Answer channel- Playback given file
Perl: 73 loc (http://goo.gl/hdsxuw)
Go: 90 loc (http://goo.gl/uxezg7)
Benchmark
Stress test using agistress:- 1 session/sec
agistress -delay=0- 25 sessions/sec
agistress -runs=5 -sess=5 -delay=0- 50 sessions/sec
agistress -runs=10 -sess=5 -delay=0- 100 sessions/sec
agistress -runs=10 -sess=10 -delay=0- 200 and 400 sessions/sec
Benchmark
Memory usage:
Many active sessions
Custom AGI Payload, 5 sec playback duration
- 12 active sessions:
agistress -sess=2 -conf=sample.conf
- 24 active sessions:
agistress -sess=4 -conf=sample.conf
- 48, 96 and 192 sessions
Results
- Up to 3 times less CPU usage
- Up to 2.5 times shorter runtime
- Enormous memory savings
Thank You!