OpenSIPs 1.8.2: Load Balancer en un Redis Cluster

Versión para impresiónSend by email

Actualizado 23 Septiembre 2013

En este articulo veremos como configurar dos servidores OpenSIPs con el modulo Load_Balancer in un cluster Redis (una base de datos no relacional de la familia de NoSQL). Esto  a través del modulo chachedb_redis. De esta forma los dos servidores compartirán en tiempo real la carga de los servidores presenten en la configuración del balanceamiento de carga. Este tipo de escenario puede ser una optima solución para proveedores de terminación SIP o un general para cualquier tipo de configuración donde se necesiten servidores media redundantes y un Cluster de servidores OpenSIPs.

En los dos servidores se ha instalado la versión minimal de CentOS 6.3

IP local servidor A: 192.168.168.60

IP local servidor B: 192.168.180.70

En ambos servidores:

yum update -y

reboot

Ingresamos nuevamente en ambos servidores y seguimos con la instalación de los paquetes necesarios para todo el proceso:

yum install mysql mysql-devel mysql-server gcc gcc-c++ bison bison-devel flex make expat expat-devel unixODBC-devel net-snmp-devel

yum install subversion libxml2 libxml2-devel openssl-devel xmlrpc-c-devel lynx pcre pcre-devel ncurses-devel ncurses-libs

Para Redis:

yum install tcl git ruby

El modulo CACHEDB_REDIS de OpenSIPs se basa en el cliente Redis “hiredis”. Se descarga, compila e instala:

cd /usr/src
git clone https://github.com/redis/hiredis.git hiredis
cd hiredis
make
make install

Se descarga la versión 1.8.2 de OpenSIPs:

cd /usr/src
svn co https://opensips.svn.sourceforge.net/svnroot/opensips/branches/1.8 opensips_1_8
cd opensips_1_8
make menuconfig

Se entra en el menú “Configure Compile Options” y luego en “Configure Excluded Modules”. Se seleccionan los módulos “cachedb_redis” , “db_mysql” y “regex”.

Se vuelve atrás con la tecla <—, se selecciona “Save Changes” y se presiona dos veces la tecla Envío. Se vuelve atrás con la tecla <— y se selecciona “"Exit & Save All Changes”.

Se presiona la tecla Envío hasta salir del menú. Se compila e instala:

make prefix=/ all
make prefix=/ install

Se instala el script de arranque de OpenSIPs:

cd packaging/fedora

nano opensips.init

Cambiamos esta línea:

oser=/usr/sbin/$prog

para que quede:

oser=/sbin/$prog

Guardamos los cambios y terminamos la configuración:

chmod 755 opensips.init

cp opensips.init /etc/init.d/opensips

chkconfig --add opensips

chkconfig opensips on

Ya podemos pasar a la compilación e instalación de Redis. La versión que soporta el Cluster es la unstable que bajamos de github:

cd /usr/src

git clone https://github.com/antirez/redis.git redisuns

cd redisuns

make
make test
make install

Terminada la instalación pasamos a la configuración del servidor A.

 

Servidor A

En este servidor crearemos la base de datos para OpenSIPs que luego se compartirá con el segundo servidor OpenSIPs:

chkconfig mysqld on

service mysqld start

mysqladmin -u root password sesamo

Entramos en el cliente:

mysql -u root -psesamo

Creamos la base de datos OpenSIPs:

mysql> create database opensips;

Salimos del cliente y configuramos OpenSIPs para la creación de las tablas:

mv /etc/opensips/opensipsctlrc /etc/opensips/opensipsctlrc.old

nano /etc/opensips/opensipsctlrc

copiamos las líneas que siguen:

SIP_DOMAIN=198.168.168.60
DBENGINE=MYSQL
DBHOST=localhost
DBNAME=opensips
DBRWUSER=opensips
DBRWPW="opensipsrw"
DBROOTUSER="root"
INSTALL_EXTRA_TABLES=ask
INSTALL_PRESENCE_TABLES=ask
OSIPS_FIFO="/tmp/opensips_fifo"
VERIFY_ACL=1
PID_FILE=/var/run/opensips.pid

SIP_DOMAIN es la dirección IP o el nombre de dominio de nuestro servidor Linux.Guardamos los cambios y creamos las tablas:

opensipsdbctl create

Captura

Volvemos a entrar en el cliente MySQL y creamos los permisos de acceso para el segundo servidor OpenSIPs:

mysql -u root -psesamo

mysql> grant all privileges on opensips.* to 'opensips'@'192.168.180.70' identified by 'opensipsrw';

Insertamos dos servidores en la tabla load_balancer:

mysql> insert into load_balancer (group_id,dst_uri,resources,probe_mode,description) values ('1','sip:sip.servidor1.org','voip/s=20','2','20 canales SIP'),('1','sip:sip.servidor2.org','voip/s=20','2','20 canales SIP');

mysql> quit

Cuando queremos compartir recursos a traves del Cluster Redis, tenemos que indicar el nombre del recurso con /s al final. En este caso: voip/s

Pasamos a la configuración de OpenSips:

mv /etc/opensips/opensips.cfg /etc/opensips/opensips.cfg.old

nano /etc/opensips/opensips.cfg

Copiamos las líneas que siguen:

####### Global Parameters #########

debug=3
log_stderror=no
log_facility=LOG_LOCAL6

fork=yes
children=4

listen=udp:192.168.168.60:5060

disable_tcp=yes
disable_tls=yes
auto_aliases=no

####### Modules Section ########

#set module path
mpath="/lib/opensips/modules/"

#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timer", 5)
modparam("tm", "fr_inv_timer", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

#### Record Route Module
loadmodule "rr.so"
modparam("rr", "append_fromtag", 0)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerationS module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### URI module
loadmodule "uri.so"
modparam("uri", "use_uri_table", 0)

#### USeR LOCation module
loadmodule "usrloc.so"
modparam("usrloc", "nat_bflag", 10)
modparam("usrloc", "db_mode",   0)

#### REGISTRAR module
loadmodule "registrar.so"
modparam("registrar", "tcp_persistent_flag", 7)

#### ACCounting module
loadmodule "acc.so"
modparam("acc", "early_media", 0)
modparam("acc", "report_cancels", 0)
modparam("acc", "detect_direction", 0)
modparam("acc", "failed_transaction_flag", 3)
modparam("acc", "log_flag", 1)
modparam("acc", "log_missed_flag", 2)

#### CACHEDB_REDIS module
loadmodule "cachedb_redis.so"
modparam("cachedb_redis","cachedb_url","redis://localhost:6379/")

loadmodule "db_mysql.so"
loadmodule "dialog.so"
loadmodule "load_balancer.so"

modparam("dialog","cachedb_url","redis://localhost:6379/")
modparam("load_balancer","db_url","mysql://opensips:opensipsrw@localhost/opensips")

modparam("load_balancer", "probing_interval", 60)
modparam("load_balancer", "probing_reply_codes", "404")

####### Routing Logic ########
# main request routing logic

route{
    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

    if (has_totag()) {
        # sequential request withing a dialog should
        # take the path determined by record-routing
        if (loose_route()) {
           
            if (is_method("BYE")) {
                setflag(1); # do accounting ...
                setflag(3); # ... even if the transaction fails
            } else if (is_method("INVITE")) {
                # even if in most of the cases is useless, do RR for
                # re-INVITEs alos, as some buggy clients do change route set
                # during the dialog.
                record_route();
            }

           

            # route it out to whatever destination was set by loose_route()
            # in $du (destination URI).
            route(1);
        } else {
           
            if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                    # non loose-route, but stateful ACK; must be an ACK after
                    # a 487 or e.g. 404 from upstream server
                    t_relay();
                    exit;
                } else {
                    # ACK without matching transaction ->
                    # ignore and discard
                    exit;
                }
            }
            sl_send_reply("404","Not here");
        }
        exit;
    }

    # CANCEL processing
    if (is_method("CANCEL"))
    {
        if (t_check_trans())
            t_relay();
        exit;
    }

    t_check_trans();

    # preloaded route checking
    if (loose_route()) {
        xlog("L_ERR",
        "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
        if (!is_method("ACK"))
            sl_send_reply("403","Preload Route denied");
        exit;
    }

    # record routing
    if (!is_method("REGISTER|MESSAGE"))
        record_route();

    # account only INVITEs
    if (is_method("INVITE")) {
        setflag(1); # do accounting
    }

    if ($rU==NULL) {
        # request with no Username in RURI
        sl_send_reply("484","Address Incomplete");
        exit;
    }

    if (!load_balance("1","voip/s","1")) {
        send_reply("500","Failure to route");
        exit;
    }

    route(1);
}

route[1] {
    # for INVITEs enable some additional helper routes
    if (is_method("INVITE")) {
        t_on_branch("2");
        t_on_reply("2");
        t_on_failure("1");
    }

    if (!t_relay()) {
        send_reply("500","Internal Error");
    };
    exit;
}

branch_route[2] {
    xlog("new branch at $ru\n");
}

onreply_route[2] {
    xlog("incoming reply\n");
}

failure_route[1] {
    if (t_was_cancelled()) {
        exit;
    }
}

Las lineas importantes:

  • modparam("cachedb_redis","cachedb_url","redis://localhost:6379/") – Para conectarnos al servidor Redis local
  • modparam("dialog","cachedb_url","redis://localhost:6379/") – Para compartir los diálogos a través del Cluster Redis
  • if (!load_balance("1","voip/s","1")) { – Para llamar los recursos configurados en la tabla load_balancer

Guardamos los cambios y terminamos con Redis:

mkdir /etc/redis

nano /etc/redis/redis.conf

daemonize no
pidfile /var/run/redis.pid
port 6379
timeout 0
loglevel notice
logfile stdout
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
slave-serve-stale-data yes
slave-read-only yes
slave-priority 100
maxclients 10000
maxmemory 3GB
maxmemory-policy noeviction
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
cluster-enabled yes
cluster-config-file /etc/redis/nodes-6379.conf
slowlog-log-slower-than 10000
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60

client-output-buffer-limit pubsub 32mb 8mb 60

Las líneas importantes:

  • cluster-enabled yes – Para Activar el Cluster
  • cluster-config-file /etc/redis/nodes-6379.conf – Nombre del archivo que Redis creará in automático donde se guardarán los datos del Cluster y de los nodos

Guardamos los cambios y iniciamos Redis:

redis-server /etc/redis/redis.conf

Captura

Redis no encuentra todavía ningún Cluster porque no se ha configurado el segundo servidor todavía. Para salir: CTRL-C

 

Servidor B

primero configuramos OpenSIPs:

mv /etc/opensips/opensips.cfg /etc/opensips/opensips.cfg.old

nano /etc/opensips/opensips.cfg

Copiamos las líneas que siguen:

####### Global Parameters #########

debug=3
log_stderror=no
log_facility=LOG_LOCAL6

fork=yes
children=4

listen=udp:192.168.180.70:5060

disable_tcp=yes
disable_tls=yes
auto_aliases=no

####### Modules Section ########

#set module path
mpath="/lib/opensips/modules/"

#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timer", 5)
modparam("tm", "fr_inv_timer", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

#### Record Route Module
loadmodule "rr.so"
modparam("rr", "append_fromtag", 0)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerationS module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### URI module
loadmodule "uri.so"
modparam("uri", "use_uri_table", 0)

#### USeR LOCation module
loadmodule "usrloc.so"
modparam("usrloc", "nat_bflag", 10)
modparam("usrloc", "db_mode", 0)

#### REGISTRAR module
loadmodule "registrar.so"
modparam("registrar", "tcp_persistent_flag", 7)

#### ACCounting module
loadmodule "acc.so"
modparam("acc", "early_media", 0)
modparam("acc", "report_cancels", 0)
modparam("acc", "detect_direction", 0)
modparam("acc", "failed_transaction_flag", 3)
modparam("acc", "log_flag", 1)
modparam("acc", "log_missed_flag", 2)

#### CACHEDB_REDIS module
loadmodule "cachedb_redis.so"
modparam("cachedb_redis","cachedb_url","redis://localhost:6380/")

loadmodule "db_mysql.so"
loadmodule "dialog.so"
loadmodule "load_balancer.so"

modparam("dialog","cachedb_url","redis://localhost:6380/")
modparam("load_balancer","db_url","mysql://opensips:opensipsrw@192.168.168.60/opensips")
modparam("load_balancer", "probing_interval", 60)
modparam("load_balancer", "probing_reply_codes", "404")

####### Routing Logic ########
# main request routing logic

route{
if (!mf_process_maxfwd_header("10")) {
sl_send_reply("483","Too Many Hops");
exit;
}

if (has_totag()) {
# sequential request withing a dialog should
# take the path determined by record-routing
if (loose_route()) {

if (is_method("BYE")) {
setflag(1); # do accounting ...
setflag(3); # ... even if the transaction fails
} else if (is_method("INVITE")) {
# even if in most of the cases is useless, do RR for
# re-INVITEs alos, as some buggy clients do change route set
# during the dialog.
record_route();
}

# route it out to whatever destination was set by loose_route()
# in $du (destination URI).
route(1);
} else {

if ( is_method("ACK") ) {
if ( t_check_trans() ) {
# non loose-route, but stateful ACK; must be an ACK after
# a 487 or e.g. 404 from upstream server
t_relay();
exit;
} else {
# ACK without matching transaction ->
# ignore and discard
exit;
}
}
sl_send_reply("404","Not here");
}
exit;
}

# CANCEL processing
if (is_method("CANCEL"))
{
if (t_check_trans())
t_relay();
exit;
}

t_check_trans();

# preloaded route checking
if (loose_route()) {
xlog("L_ERR",
"Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
if (!is_method("ACK"))
sl_send_reply("403","Preload Route denied");
exit;
}

# record routing
if (!is_method("REGISTER|MESSAGE"))
record_route();

# account only INVITEs
if (is_method("INVITE")) {
setflag(1); # do accounting
}

if ($rU==NULL) {
# request with no Username in RURI
sl_send_reply("484","Address Incomplete");
exit;
}

if (!load_balance("1","voip/s","1")) {
send_reply("500","Failure to route");
exit;
}

route(1);
}

route[1] {
# for INVITEs enable some additional helper routes
if (is_method("INVITE")) {
t_on_branch("2");
t_on_reply("2");
t_on_failure("1");
}

if (!t_relay()) {
send_reply("500","Internal Error");
};
exit;
}

branch_route[2] {
xlog("new branch at $ru\n");
}

onreply_route[2] {
xlog("incoming reply\n");
}

failure_route[1] {
if (t_was_cancelled()) {
exit;
}
}

Guardamos los cambios y configuramos redis:

mkdir /etc/redis

nano /etc/redis/redis.conf

Copiamos la configuración para el segundo servidor:

daemonize no
pidfile /var/run/redis.pid
port 6380
timeout 0
loglevel notice
logfile stdout
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ./
slave-serve-stale-data yes
slave-read-only yes
slave-priority 100
maxclients 10000
maxmemory 3GB
maxmemory-policy noeviction
appendonly no
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
lua-time-limit 5000
cluster-enabled yes
cluster-config-file /etc/redis/nodes-6380.conf
slowlog-log-slower-than 10000
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

Guardamos los cambios y iniciamos Redis en ambos servidores:

Servidor A:

redis-server /etc/redis/redis.conf

Servidor B:

redis-server /etc/redis/redis.conf

Para que cada nodo del Cluster sepa del otro hay que “presentárselo". En el servidor A entramos en la consola de Redis (en otra ventana terminal):

redis-cli

Miramos la situación del Cluster:

redis 127.0.0.1:6379> cluster nodes
28cc1625f86f777065a057fd19cc0cb8d49cf4b3 :0 myself - 0 0 connected

Todavía hay solo uno (el local). Escribimos:

redis 127.0.0.1:6379> cluster meet 192.168.180.70 6380
OK

Miramos nuevamente:

redis 127.0.0.1:6379> cluster nodes
a862b68040167034f20a1ec535769fc6e2e175e5 192.168.180.70:6380 master - 1352482127 1352482127 connected
28cc1625f86f777065a057fd19cc0cb8d49cf4b3 :0 myself - 0 0 connected

Ya el nodo local sabe del otro y viceversa.

Salimos de la consola:

redis 127.0.0.1:6380> quit

En la consola de los dos servidores Redis aparecerá:

A:

[15170] 09 Nov 17:28:40.440 * Connecting with Node d376333ff55910a6d54c9c7dace20d7d92a5ef9a at 192.168.180.70:16380

B:

[13763] 09 Nov 17:29:03.183 * Connecting with Node 86d0dc855d790c8ec42f63486e08b87613971d05 at 192.168.168.60:16379

El sistema Cluster de Redis pone a disposición de los nodos un total de 16384 Hash Slots. Como los nodos son dos, los Hash Slots se reparten de la siguiente manera:

Servidor A

Abrimos otra ventana terminal y escribimos:

echo '(0..8192).each{|x| puts "CLUSTER ADDSLOTS "+x.to_s}' | ruby | redis-cli -p 6379 > /dev/null

Servidor B

Abrimos otra ventana terminal y escribimos:

echo '(8193..16384).each{|x| puts "CLUSTER ADDSLOTS "+x.to_s}' | ruby | redis-cli -p 6380 > /dev/null

Salimos de ambos servidores Redis con CTRL-C y los volvemos a iniciar:

redis-server /etc/redis/redis.conf

Entramos en la consola Redis del servidor A:

redis-cli

redis 127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:2

Los dos Cluster están funcionando perfectamente.

Salimos de la consola:

redis 127.0.0.1:6379> quit

OpenSIPs busca la librería libhiredis.so.0.10 en la carpeta /usr/lib mientras se encuentra en /usr/local/lib. En ambos servidores creamos un enlace simbólico:

cd /usr/lib

ln -s /usr/local/lib/libhiredis.so.0.10 libhiredis.so.0.10

Ya podemos iniciar ambos servidores OpenSIPs:

service opensips start

 

La prueba

En otro servidor (AsteriskA) en la misma red local, con instalado Asterisk, enviamos todas las llamadas hacia los servidores OpenSIPs:

nano /etc/asterisk/sip.conf

al final de archivo añadimos los dos servidores OpenSIPs:

[opensipsA]
type=peer
context=from-opensips
host=192.168.180.70
disallow=all
allow=alaw
qualify=yes

[opensipB]
type=peer
context=from-opensips
host=192.168.168.60
disallow=all
allow=alaw
qualify=yes

Guardamos los cambios y recargamos la configuración SIP

asterisk -rx "sip reload"

Configuramos oportunamente el dialplan para enviar la llamadas al servidor OpenSIPsA y si falla al servidor OpenSIPsB

Configuramos los dos servidores Asterisk presentes en la configuración del balanceamiento de carga para que acepten INVITE que lleguen desde los dos servidores OpenSIPs:

[opensipA]
type=peer
context=opensipscluster
host=192.168.180.70
disallow=all
allow=alaw
dtmfmode = rfc2833
nat=no

[opensipB]
type=peer
context=opensipcluster
host=192.168.160.60
disallow=all
allow=alaw
dtmfmode = rfc2833
nat=no

Guardamos los cambios y recargamos la configuración SIP:

asterisk -rx "sip reload"

Ahora creamos el contexto “opensipscluster” nel dialplan:

nano /etc/asterisk/extensions.conf

[opensipscluster]
exten => _X.,1,Answer
same => n,Playback(tt-monkeys)
same => n,hangup

Guardamos los cambios y recargamos el dialplan:

asterisk -rx "dialplan reload"

Ahora desde un softphone conectado al servidor AsteriskA, marcamos cualquier numero que salga por una de las troncales OpeSIPs configuradas:

El resultado en el Asterisk donde llega la llamada:

Captura

OpenSIPsA:

Captura

 

OpenSIPsB:

 

Captura

El Cluster Redis y el modulo chachedb_redis, funcionan correctamente.

Nota final: no es muy aconsejable dejar el servidor MySQL en uno de los dos servidores donde está instalado OpenSIPs. Mejor tenerlo en un tercer servidor.

 

Fuentes:

OpenSIPs CACHEDB_REDIS module

Presentación “Distributed SIP clusters with OpenSIPSby Bogdan

Articulo “Backporting into Redis 2.4 and other news” by Antirez

Distribuir contenido Distribuir contenido