Análisis de Colas con Python

Ya hemos tenido una introducción sobre los modelos de cola, y una descripción de SimuladorQ. Pero como este trabajo está enmarcado en un Proyecto Final de Asignatura, detallaremos a continuación como trabaja el software. En la sección posterior mostraremos resultados del programa comparándolos con los valores obtenidos teóricamente.

SimComponents

De la librería de SimComponents utilizamos las clases Packet, PacketGenerator, PacketSink, SwitchPort y PortMonitor.

Packet

Esta clase representa un paquete, el cual recorrerá desde el Generador de Paquetes hasta el Sumidero. El parámetro de salida será un string con los atributos del paquete, como: número de identificación, la fuente que envía, el tiempo en que se genera y el tamaño del mismo.

class Packet(object):

    def __init__(self, time, size, id, src="a", dst="z", flow_id=0):
        self.time = time
        self.size = size
        self.id = id
        self.src = src
        self.dst = dst
        self.flow_id = flow_id

    def __repr__(self):
        return "id: {:<6}, src: {}, time: {:<10f}, size: {:.10}".\
            format(self.id, self.src, self.time, self.size)

PacketGenerator

El PacketGenerator o Generador de Paquetes, emula un cliente que envía trafico a una red, el cual en un nodo se encontrará con un buffer que responderá de determinada manera, dependiendo del modelo de cola que hayamos presentado.

Los paquetes que genera tendrán un tiempo de inter arribo dado por una distribución que pasaremos como parámetro de entrada.

A partir de esta clase, de SimComponents comienza a aparecer el parámetro de entrada env el cual nos define el Entorno de Simpy que generará la simulación.

Adist será una función que devuelve los tiempos de inter-arribo.

Sdist será una función que devuelve los tamaños de los paquetes. Hay que prestar atención a este parámetro, ya que más adelante, por como trabaja la librería SimComponents, será fundamental para los tiempos de servicios de los paquetes en la cola.

class PacketGenerator(object):
    def __init__(self, env, id,  adist, sdist, initial_delay=0, finish=float("inf"), flow_id=0):
        self.id = id
        self.env = env
        self.adist = adist
        self.sdist = sdist
        self.initial_delay = initial_delay
        self.finish = finish
        self.out = None
        self.packets_sent = 0
        self.action = env.process(self.run())  # starts the run() method as a SimPy process
        self.flow_id = flow_id

    def run(self):
        #El generador de función utilizado en la simulación.
        yield self.env.timeout(self.initial_delay)
        while self.env.now < self.finish:
            # Espera por la siguiente transmisión
            yield self.env.timeout(self.adist())
            self.packets_sent += 1
            p = Packet(self.env.now, self.sdist(), self.packets_sent, src=self.id, flow_id=self.flow_id)
            self.out.put(p)

En definitiva, el generador, pone en la salida de la clase un Packet, con las características que vimos anteriormente. Estos atributos, serán utilizados para el cálculo de las medidas de rendimiento del sistema.

PacketSink

PacketSink o sumidero, es donde terminan los paquetes. El sumidero recibe los paquetes y colecta información sobre los tiempos de espera.

Se define env, el entorno de simulación. La librería posee más información acerca de esta clase.

class PacketSink(object):
    def __init__(self, env, rec_arrivals=False, absolute_arrivals=False, rec_waits=True, save=True, debug=False, selector=None):
        self.store = simpy.Store(env)
        self.env = env
        self.rec_waits = rec_waits
        self.rec_arrivals = rec_arrivals
        self.absolute_arrivals = absolute_arrivals
        self.waits = []
        self.arrivals = []
        self.debug = debug
        self.packets_rec = 0
        self.bytes_rec = 0
        self.selector = selector
        self.last_arrival = 0.0
        self.save= save 
        self.data=[]

    def put(self, pkt):
        if not self.selector or self.selector(pkt):
            now = self.env.now
            if self.rec_waits:
                self.waits.append(self.env.now - pkt.time)
            if self.rec_arrivals:
                if self.absolute_arrivals:
                    self.arrivals.append(now)
                else:
                    self.arrivals.append(now - self.last_arrival)
                self.last_arrival = now
            self.packets_rec += 1
            self.bytes_rec += pkt.size
            if self.debug:
                print(pkt)
            if self.save:
                self.data.append('{}'.format(pkt))

SwitchPort

El SwitchPort o cola, será el componente mas importante de nuestro sistema. Modela un puerto de salida con un bitrate dado y un límite de tamaño de búfer, en bytes. Nuevamente aparece env para definir el entorno de simulación. El argumento rate, es la tasa a la cual van a ser servidos los clientes (o bytes). El tamaño de la cola dado por qlimit en bytes. En caso de cola infinita para los modelos M/M/1 y M/G/1 el parámetro qlimit=None.

 class SwitchPort(object):
    def __init__(self, env, rate, qlimit=None, limit_bytes=True, debug=False):
        self.store = simpy.Store(env)
        self.rate = rate
        self.env = env
        self.out = None
        self.packets_rec = 0
        self.packets_drop = 0
        self.qlimit = qlimit
        self.limit_bytes = limit_bytes
        self.byte_size = 0  # Tamaño actual de la cola en bytes
        self.debug = debug
        self.sizes = []
        self.busy = 0  # Usado para rastrear si un paquete está siendo enviado actualmente
        self.action = env.process(self.run())#inicia el método run() como un proceso SimPy

    def run(self):
        while True:
            self.msg = (yield self.store.get())
            self.busy = 1
            self.byte_size -= self.msg.size
            delay = abs(self.msg.size*8.0/self.rate)
            yield self.env.timeout(delay)
            self.out.put(self.msg)
            self.sizes.append(self.msg.size)
            self.busy = 0
            if self.debug:
                print(self.msg)

    def put(self, pkt):
        self.packets_rec += 1
        tmp_byte_count = self.byte_size + pkt.size
        if self.qlimit is None:
            self.byte_size = tmp_byte_count
            return self.store.put(pkt)
        if self.limit_bytes and tmp_byte_count >= self.qlimit:
            self.packets_drop += 1
            return
        elif not self.limit_bytes and len(self.store.items) >= self.qlimit-1:
            self.packets_drop += 1
        else:
            self.byte_size = tmp_byte_count
            return self.store.put(pkt)

Los paquetes que salen del Generador y llegan al SwitchPort, primero se cuentan, y luego se realiza una validación para saber si entra en el lugar disponible que posee la cola. En caso de no entrar, pasa a ser un paquete drop o rechazado. Una vez que entra en la cola, pasa a formar parte de store, esperando a ser servido.

El método run() va sacando los paquetes de store y los va sirviendo. La manera de servir los paquetes

 yield self.env.timeout(self.msg.size*8.0/self.rate)

El yield pasiva el proceso durante un tiempo. Ese tiempo depende del tamaño del paquete que está siendo servido, multiplicado por 8 bit, y dividido por el rate en bit por segundo.

Entonces se puede pensar que el tiempo de servicio, tendrá la misma distribución que la de los tamaños de paquetes, que habíamos definido en un principio en el Generador de Paquetes.

Como ejemplo, para ver mejor lo que estamos diciendo, pondremos el modelo M/M/1. Tenemos como tasa de servicio en este modelo, valores distribuidos exponencialmente. Entonces la media de una distribución exponencial es . No confundir este lambda con la tasa de inter arribo.

Donde la media esta dada en byte, el 8 es en bit, y el rate en bps.

A partir de conocer , tendremos entonces la tasa de servicio de nuestra cola, que en el caso del modelo M/M/1, será el parámetro de la distribución exponencial.

Para los otros modelos, el análisis será similar, y dependiendo de la distribución del tamaño de los paquetes en el Generador, estaremos definiendo implícitamente el servicio de nuestra cola.

Visto que no tiene demasiado sentido estar haciendo cálculos previos en el tamaño de los paquetes para definir la tasa de servicio, en SimuladorQ se pasa la tasa y se calcula despejando de la fórmula anterior el parámetro que meteremos en el generador. Más adelante mencionaremos esto nuevamente.

PortMonitor

El PortMonitor lo que hace es mirar el número de clientes, o bytes, que se encuentran en la cola. Con los valores que rescata esta clase, se calculan los valores medios de ocupación en el sistema y la cola. Además del parámetro env, se le pasa como argumento el puerto que debe observar, y la distribución de tiempos de cada cuanto tiene que hacerlo.

class PortMonitor(object):
       def __init__(self, env, port, dist, count_bytes=False):
        self.port = port
        self.env = env
        self.dist = dist
        self.count_bytes = count_bytes
        self.sizes = []
        self.action = env.process(self.run())

    def run(self):
        while True:
            yield self.env.timeout(self.dist())
            if self.count_bytes:
                total = self.port.byte_size
            else:
                total = len(self.port.store.items) + self.port.busy
            self.sizes.append(total)

Modelos

Las librerías de SimComponents, nos permite a través de las clases, poder generar los bloques de un sistema de comunicación, con: 1. un cliente que genera tráfico, 2. una cola con un búfer y cierta capacidad de servir los paquetes que llegan a ella, 3. y un sumidero donde mueren los paquetes y se contabilizan características de los envíos.

Las siguientes funciones que mencionaremos, nos servirán para introducir y definir parámetros de cada uno de los componentes de SimComponents, realizar los cálculos pertinentes para las medidas de eficiencia que necesitamos para SimuladorQ con su respectivas salidas de la función y la muestra de gráficos.

MM1

Para el modelo M/M/1 tenemos la función MM1. Para ver en detalle toda la función, ir a

def MM1(lamda, mu, user, port_rate, Tsim, bins, directorio):

Vemos que los parámetros de entrada son lamda= para la tasa de inter-arribo, mu= para la tasa de servicio, port_rate la velocidad para el PortSwitch, Tsim es el tiempo de la simulación en segundos (aclaramos que no es en tiempo real la simulación, sino que se genera un tiempo aleatorio cada vez que ocurre un evento), y directorio, que se utiliza para brindar la ubicación de donde se van guardar temporalmente los histogramas para mostrarlo a través de la interfaz gráfica.

Introduciéndonos en la función, con los parámetros que le pasamos, definimos las clases de SimComponents.

    ####Parametros del Sumidero
    debugSink=False 
    #muestra en la salida lo que llega al servidor
    rec_arrivalsSink=True 
    #Si es verdadero los arribos se graban
    abs_arrivalsSink=False 
    # Si es verdadero se graban tiempos de arribo absoluto; False, el tiempo entre arribos consecutivos

Con la siguiente línea se convierte la tasa de los tiempos de inter-arribo al parámetro de la distribución exponencial para la distribución de los tamaño de paquetes generados con PacketSink.

    ####Parametros del Sumidero
    mu=(mu*8)/port_rate

Las funciones de distribución de los tiempos de inter-arribo y de los tamaños de los paquetes.

    adist = functools.partial(random.expovariate, lamda) #Tiempo de inter arribo de los paquetes
    sdist = functools.partial(random.expovariate, mu)  # Tamaño de los paquetes 

El tamaño de la cola no se pasa como parámetro de entrada de la función MM1, ya que al tener búfer infinito este modelo, éste ya lo definimos internamente.

    #Parametros de la Cola
    ql=None #byte. Largo de cola infinita

Por último, en la definición de los componentes, PortMonitor, el cual necesita la distribución de tiempos en que observará la cola.

    #Parametros del Monitor 
    Smd = 0.5
    samp_dist = functools.partial(random.expovariate,Smd)

Una vez cargados los parámetros de las clases de SimComponents, definimos cada uno de ellos. Vemos como creamos el entorno de simulación env, el cual se lo damos como entrada a las demás clases de SimComponents.

    ############# CREACION DEL ENTORNO Y SUS COMPONENTES  #########################

    # Creación del Entorno de Simpy
    env = simpy.Environment()  

    # Creación del Generador de Paquetes, Servidor, Cola y Monitor.
    psink = PacketSink(env, 
                    debug=debugSink, 
                    rec_arrivals=rec_arrivalsSink, 
                    absolute_arrivals=abs_arrivalsSink)
    pg = PacketGenerator(env, 
                         user, 
                         adist, 
                         sdist)
    switch_port= SwitchPort(env, 
                             rate=port_rate, 
                             qlimit=ql)
    pm = PortMonitor(env, 
                     switch_port, 
                     samp_dist)

A continuación, vemos cómo se realiza el conexionado de cada uno de los bloques. Se define que bloque se coloca a la salida.

    ############# CONEXION DE LOS COMPONENTES DEL MODELO  #########################

    pg.out = switch_port
    switch_port.out = psink

En este caso, la conexión nos quedaría de la siguiente manera.

Una vez todo definido, corremos la simulación, con el tiempo Tsim.

    # Correr la simulacion del entorno. Parametro el tiempo
    env.run(until=Tsim)

El tiempo de simulación no es más que un parámetro que necesita la librería Simpy para generar un clock interno, poder hacer la generación de paquetes y así darle consecuencia a los mismos de una manera ordenada. Es decir, da marcas temporales a los eventos, en este caso la generación de los paquetes. Cabe destacar que esta variable no representa una unidad temporal real, como el segundo (mas allá de que internamente, la identidad que le da es el segundo). Puede apreciarse que no hay una analogía directa entre el segundo del clock interno y el segundo de tiempo real. La frecuencia de éste clock depende del procesamiento de CPU, por eso, la duración de la simulación puede variar de un equipo a otro. Para mas información, visitar la documentación de Simpy.

Una vez simulado, calculamos, a partir de los atributos, las diferentes medidas de eficiencia del modelo.

Los paquetes enviados, perdidos y recibidos, son atributos de cada una de las clases. Se puede ver mejor en SimComponents como se generan.

    pkt_drop=switch_port.packets_drop
    pkt_enviados=pg.packets_sent
    pkt_recibidos_serv = psink.packets_rec

El tiempo medio que van a esperar los paquetes en ser servidos, lo veremos haciendo un promedio de todas las esperas que sufren cada uno de los paquetes que llegan al sumidero.

    espera_sist_W=sum(psink.waits)/len(psink.waits)

El tamaño medio, entre la cola y el paquete que está siendo servido, también es posible pensarlo como un promedio con todas las veces que el monitor mira la longitud de la misma.

    ocup_sistema_L=float(sum(pm.sizes))/len(pm.sizes)

La pérdida en este modelo es nula. Entendiendo que el sistema es estacionario y todos los paquetes en algún momento serán servidos. La manera en que se calcula puede traer algún error si hay algún paquete que no se consume en el sumidero, problema que existe cuando, por el tiempo de simulación, quedan paquetes en el camino.

    tasa_perdida=1.0 - float(pg.packets_sent)/switch_port.packets_rec

Como se dijo anteriormente, la manera de calcular la tasa de servicio, es a partir del parámetro de la distribución exponencial a la hora de generar los paquetes en el PacketGenerator. Es por esto que, sabiendo que la intensidad de tráfico es obtenemos a como siendo el el parámetro anteriormente mencionado. Entonces se calcula la intensidad de tráfico de esa manera.

    intensidad_trafico = (lamda)*((float(sum(switch_port.sizes)/len(switch_port.sizes))*8.0/port_rate))

Conociendo la intensidad de tráfico y definido como argumento de entrada de la función MM1, podemos calcular los demás parámetros por Little.

    espera_sist_W=sum(psink.waits)/len(psink.waits)
    pkt_drop=switch_port.packets_drop
    pkt_enviados=pg.packets_sent
    tasa_perdida=1.0 - float(pg.packets_sent)/switch_port.packets_rec
    ocup_sistema_L=float(sum(pm.sizes))/len(pm.sizes)
    pkt_recibidos_serv = psink.packets_rec
    intensidad_trafico = (lamda)*((float(sum(switch_port.sizes)/len(switch_port.sizes))*8.0/port_rate))
    mu_serv=1/(intensidad_trafico/lamda)
    espera_cola_Wq=espera_sist_W - 1/mu_serv
    ocup_cola_Lq= espera_cola_Wq * lamda 

Es de esperar que mu_serv, que calculamos en la línea anterior, se corresponda con el que se pasa como argumento.

También, se realizan los gráficos que se explicarán en la sección siguiente, Pruebas del Funcionamiento.

Por último, los datos que calculamos, se devuelven con un return, que se llaman desde el archivo principal para mostrarlos en SimuladorQ.

MG1

A nivel general, la función MG1, trabaja de la misma manera que MM1. Se puede sospechar que cambiará la distribución de servicio de la cola.

Anteriormente habíamos dicho que la distribución y su respectiva tasa dependía de los tamaños de los paquetes. En el caso de MG1, esta definición se pasa de la siguiente manera:

#Parametros del Generador de Paquetes. 
    if G==1:
        sdist = functools.partial(random.normalvariate, a, b)  
    elif G==2:
        sdist = functools.partial(random.uniform, a, b)  

Primero, vemos que ahora también se pasa como parámetro la distribución general que queremos. Como opción proponemos trabajar con la distribución Normal y la Uniforme.

En esta función, ahora no existe más el parámetro . Se pasa los valores de a y b. En la distribución Normal, representan la media y la desviación estándar respectivamente. En la Uniforme, los valores mínimo y máximo del rango que define la función de densidad de probabilidad.

Ahora entonces, la tasa de servicio de la cola estará dada por

donde la media de la Normal está definida en el argumento, y la de la distribución uniforme es .

MM1K

La función MM1K trabaja de la misma manera que MM1, con la salvedad de que en este modelo, la cola es finita, por lo que el parámetro ql ya no es None como en el anterior, y se define directamente en los parámetros de entrada de la función.

def MM1K(lamda, mu, ql, user, port_rate, Tsim, bins, directorio):

Histogramas

El simulador provee 3 histogramas: Tiempos de inter-arribos de paquetes, Tiempos de espera de los paquetes y Tiempos de ocupación del sistema.

Tiempos de inter-arribos de paquetes

Para el caso de los tiempos de inter-arribos de paquetes tenemos que analizar como van a ser servidos los paquetes. Es por eso, que en modelos.py se definen

    debugSink=False #False, muestra en la salida lo que llega al servidor
    rec_arrivalsSink=True #Si es verdadero los arribos se graban
    abs_arrivalsSink=False # true, se graban tiempos de arribo absoluto; False, el tiempo entre arribos consecutivos
    [...]
    psink = PacketSink(env, 
                        debug=debugSink, 
                        rec_arrivals=rec_arrivalsSink, 
                        absolute_arrivals=abs_arrivalsSink)

La variable psink indica que recibe paquetes y recopila información de delay en la lista de espera de paquetes.

Ahora bien, para graficar el histograma, se realiza de la siguiente forma:

fig, axis = plt.subplots()
axis.hist(psink.arrivals, bins, normed=True, alpha=1, edgecolor = 'black',  linewidth=1)
axis.set_title("Tiempos de Inter-Arribo a la Cola - Normalizado")
axis.set_xlabel("Tiempo")
axis.set_ylabel("Frecuencia de ocurrencia")
fig.savefig(directorio + "ArrivalHistogram_normal.png")

El histograma se genera con el método .hist y como argumentos están los arribos (psink.arrivals); los bins (explicados en la sección de Funcionamiento de SimuladorQ); si se desea que este normalizado o no (normed=True o normed = False); el grado de opacidad (alpha=1), en nuestro caso al máximo; color de bordes de las barras (edgecolor = 'black') y su grosor (linewidth = 1).

SimuladorQ brinda las opciones de normalización. Por defecto normed = False la frecuencia es proporcional a la altura, no al área. La opción normed=True cambia la representación a proporcionalidad con el área, y además cambia la escala para que el área total sea 1.

Tiempos de espera de los paquetes

Para el caso de los tiempos de espera de los paquetes hacemos un análisis similar al de los tiempos de inter-arribo, porque lo vemos con psink, pero ahora solicitamos el tiempo de espera que experimentan los paquetes con psink.waits. El resto se mantiene igual en el código, pero cambiamos psink.arrivals por psink.waits.

Tiempo de ocupación en el sistema

Para el caso de los tiempos de ocupación en el sistema el análisis es parecido a los anteriores. Solo agregamos un monitor en la cola.

#Parametros del Monitor 
       Smd= 0.5
       samp_dist = functools.partial(random.expovariate,Smd)
       [...]
       switch_port= SwitchPort(env, 
                                rate=port_rate, 
                                qlimit=ql)
       [...]
# PortMonitor para rastrear los tamaños de cola a lo largo del tiempo
       pm = PortMonitor(env, 
                        switch_port, 
                        samp_dist)

Se utiliza PortMonitor para controlar el tamaño de la cola a lo largo del tiempo para un SwitchPort. Observa la cantidad de elementos en SwitchPort en servicio en la cola y registra esa información en la lista de sizes. El monitor mira el puerto a intervalos de tiempo dados por la distribución elegida.

Se especifica una distribución de muestreo, es decir, una distribución que proporcione el tiempo entre muestras sucesivas. Para todos los casos y modelos, se utiliza un distribución exponencial especificando su tasa de arribo. En este caso usamos . Al SwitchPort se le da como argumento la tasa de salida de la colaport_rate, y el limite de la cola qlimit(infinito para todas los modelos, salvo para el M/M/1/K).

Para graficar el histograma, es bastante parecido a los anteriores, cambia en el argumento pm.sizes para trabajar con los tamaños de los paquetes de la cola.

fig, axis = plt.subplots()
axis.hist(pm.sizes, bins, normed=True, alpha=1, edgecolor = 'black',  linewidth=1)
axis.set_title("Tiempos de Ocupación del Sistema - Normalizado")
axis.set_xlabel("Nro")
axis.set_ylabel("Frecuencia de ocurrencia")
fig.savefig(directorio + "QueueHistogram_normal.png")