miércoles, 27 de agosto de 2014

Crea tu propio benchmarking de páginas web

El siguiente código en Python te permitirá realizar tus propios benchmarkings de páginas Web, simulando un escenario concurrente de muchos usuarios al mismo tiempo accediendo al mismo sitio Web. El resultado se almacena en un archivo de log que después podrá llevarse a una hoja de cálculo para calcular estadísticas de tiempos, o para identificar errores.

import time
import datetime
import urllib2
import logging
import threading
 
# Coleccion de urls a solicitar
urls = [
   'http://192.168.0.3/oiddigity_actual/index.html',
   'http://192.168.0.3/oiddigity_actual/registrado.asp',
   'http://192.168.0.3/oiddigity_actual/inicio.html',
   'http://192.168.0.3/oiddigity_actual/botones2.html',
   'http://192.168.0.3/oiddigity_actual/horafecha.asp'

 
# Nombre del fichero de log para guardar resultados
fich_log = 'testeo.log'
 
# Numero de hilos
num_hilos = 200
 
# Numero de procesos por hilo
num_procesos = 100
 
# Conteo de errores
num_errores = 0
 
# Metodo: proceso_urls()
# Procesa las urls, retornando el
# tiempo que consume
def proceso_urls():
   inicio = datetime.datetime.now()
   resultado = ""
 
   # Solicita y lee todas las urls
   for i in urls:
      try:
         peticion = urllib2.Request(i)
         respuesta = urllib2.urlopen(peticion)
         contenido = respuesta.read()
         resultado = "|OK"
         break
      except Exception as e:
         resultado = "|NOK[" + i + "]" + format(str(e))
 
   fin = datetime.datetime.now()
   tiempo = inicio.strftime("%T.%f")+'|'+fin.strftime("%T.%f")+"|"
   tiempo += get_tiempo(fin-inicio)
   tiempo += resultado
 
   return tiempo
 
# Metodo: get_tiempo()
# Retorna, a partir de un time, el tiempo en formato %H|%M|%S|%f
def get_tiempo(tiempo):
   resultado = ""
   segundos = tiempo.seconds
 
   if (segundos>=3600):
      horas = int(segundos/3600)
      resultado += str(horas) + "|"
   else:
      resultado += "0|"
 
   if (segundos>=60):
      minutos = int(segundos/60)
      resultado += str(minutos) + "|"
   else:
      resultado += "0|"
 
   resultado += str(segundos) + "|" + str(tiempo.microseconds)
 
   return resultado
 
# Metodo: proceso_hilo()
# Metodo principal del hilo
def proceso_hilo(count):
   global num_errores # Referencia a variable global compartida
 
   print 'Iniciando hilo ', count
 
   for i in range(num_procesos):
      resultado = proceso_urls()
 
      if (resultado.find("NOK") > 0):
         num_errores += 1
         print "#", num_errores, " > ", str(count), "[", str(i), "] > ", resultado
 
      logging.info(''+str(count)+"|"+str(i)+"|"+resultado)
 
   print 'Hilo', count, 'finalizado'
 
 
# Configuracion del fichero de log
logging.basicConfig(filename=fich_log, filemode='w', level=logging.DEBUG)
# Cabecera del log
logging.info('Hilo|Proceso|Inicio|Fin|Horas|Minutos|Segundos|Milisegundos|Resultado')
 
print '----------------------------------'
print 'Testeo. 2014 by Rafael Hernamperez'
print '----------------------------------'
print 'URLs a testear:'
 
for i in urls:
   print ' - ', i
 
print '> Ejecutando ', num_hilos, ' hilos'
print '> Total procesos: ', num_hilos * num_procesos
 
# Genera los hilos y los ejecuta
threads = list()
 
for i in range(num_hilos):
   t = threading.Thread(target=proceso_hilo, args=(i,))
   threads.append(t)
   t.start()

La variable urls permite definir las urls que van a solicitarse. Una página web puede tener varios iframes incluyendo otras partes de código web (otras páginas o servicios), las cuales han de invocarse para tener tiempos reales.

La variable fich_log define el nombre del fichero de log, en el cual se volcarán todos los resultados.

La variable num_hilos define el número de conexiones concurrentes. Esto simulará el escenario de n usuarios accediendo al mismo tiempo a las urls definidas.

La variable num_procesos define el número de veces que cada hilo llamará a las urls definidas.

La variable num_errores recoge el número de errores que se pueda producir durante el testeo.

El método proceso_urls() solicita las urls definidas y leyendo las respuestas por parte del servidor web. Tras el proceso guarda en el log el resultado con los tiempos y los posibles errores.

El método get_tiempo() retorna un literal con la descomposición del tiempo que se pasa por parámetro. Este literal se utiliza para el log, y es invocado por proceso_urls() para obtener el desglose del tiempo que ha tardado el proceso en ejecutarse.

El método proceso_hilo() es utilizado por cada uno de los hilos para lanzar todos los procesos definidos en la variable num_procesos. Es decir, repite num_procesos veces el acceso a las urls definidas.

Por último, para hacer funcionar esta aplicación, crea un fichero llamado (por ejemplo) testeo.py, copia y pega el código, guárdalo y ejecútalo desde la consola o terminal mediante el comando:

$ python testeo.py

Configurar la concurrencia en Apache y MySQL

Llega un momento en el que una aplicación Web tiene tanto éxito que el número de conexiones concurrentes satura las capacidades del servidor. Empiezan a aparecer errores de timeout o errores 500 Internal Server Error.

Para evitar ésto, se puede configurar Apache y MySQL para que admitan más concurrencia, es decir, más conexiones funcionando al mismo tiempo.

Concurrencia en Apache

Para configurar la concurrencia en Apache, hemos de configurar el archivo httpd.conf (el archivo será apache2.conf si está instalado en un Linux basado en Debian e instalado mediante sudo apt-get install apache2) o el archivo 000-default.conf.

Lo primero que debemos hacer es extraer la configuración actual de Apache. Para ello ejecutaremos el siguiente comando desde la terminal:

$ apache2 -V

El resultado será similar al siguiente:
Server version: Apache/2.2.22 (Debian)
Server built: Jul 24 2014 15:34:00
Server's Module Magic Number: 20051115:30
Server loaded: APR 1.4.6, APR-Util 1.4.1
Compiled using: APR 1.4.6, APR-Util 1.4.1
Architecture: 64-bit
Server MPM: Prefork
threaded: no
forked: yes (variable process count)
Server compiled with....
-D APACHE_MPM_DIR="server/mpm/prefork"
-D APR_HAS_SENDFILE
-D APR_HAS_MMAP
-D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
-D APR_USE_SYSVSEM_SERIALIZE
-D APR_USE_PTHREAD_SERIALIZE
-D APR_HAS_OTHER_CHILD
-D AP_HAVE_RELIABLE_PIPED_LOGS
-D DYNAMIC_MODULE_LIMIT=128
-D HTTPD_ROOT="/etc/apache2"
-D SUEXEC_BIN="/usr/lib/apache2/suexec"
-D DEFAULT_PIDLOG="/var/run/apache2.pid"
-D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
-D DEFAULT_LOCKFILE="/var/run/apache2/accept.lock"
-D DEFAULT_ERRORLOG="logs/error_log"
-D AP_TYPES_CONFIG_FILE="mime.types"
-D SERVER_CONFIG_FILE="apache2.conf"

Los primeros ajustes a realizar son:

  • KeepAlive On: Permite conexiones persistentes (más de una petición por conexión)
  • MaxKeepAliveRequests 100: Número máximo de peticiones permitidas durante una conexión persistente
  • KeepAliveTimout 5: Número de segundos de espera para la próxima petición en la misma conexión

A continuación, se ha de configurar el módulo de multiprocesamiento (MPM). En el ejemplo anterior lo conoceremos por la primera línea de la sección "Server compiled with...", en el parámetro APACHE_MPM_DIR. Como se puede observar, el módulo de multiprocesamiento es prefork. Por tanto, hemos de buscar en el archivo de configuración de Apache la sección correspondiente a este módulo, el cual contendrá los siguientes parámetros:

<IfModule mpm_prefork_module>
   StartServers 50
   MinSpareServers 20
   MaxSpareServers 70
   ThreadLimit 64
   ThreadsPerChild 25
   ServerLimit 500
   MaxClients 500
   MaxRequestsPerChild 0
</IfModule>

Los significado de estos parámetros es el siguiente:

  • StartServers: Número de servidores con los que arranca
  • MinSpareServers: Número mínimo de procesos de servidor que se mantienen de repuesto
  • MaxSpareServers: Número máximo de procesos de servidor que se mantienen de respuesto
  • ThreadLimit: Número máximo de hilos concurrentes
  • ThreadsPerChild: Número de hilos en funcionamiento en cada proceso del servidor
  • ServerLimit: Núḿeros máximo de servidores
  • MaxClients: Número máximo de procesos de servidor con los que arranca
  • MaxRequestsPerChild: Número máximo de peticiones que un proceso puede servir

NOTA: En el caso de nuestro Apache no tenga instalado un módulo de multiprocesamiento, podemos instalar uno mediante los siguientes comandos:

sudo apt-get update
sudo apt-get install apache2-mpm-prefork

Concurrencia en MySQL

Aunque configuremos Apache para que abastezca una gran cantidad de concurrencia, si nuestras webs acceden a bases de datos MySQL, encontrarán otro cuello de botella, pues MySQL también tiene una configuración predeterminada de concurrencia a la base de datos (unas 100 conexiones).

Para ajustar la concurrencia en MySQL, hemos de editar en el archivo my.cnf los siguientes parámetros:

  • max_connections: Número máximo de conexiones
  • thread_concurrency: Número de hilos concurrentes

A continuación, hay que rearrancar la base de datos para que MySQL asuma los cambios:

/etc/init.d/mysql restart

NOTA: Este comando es para Linux. En Windows, es similar, salvo la ruta.

Por último, verificamos que MySQL está funcionando con los valores actualizados. Para ello, arrancamos el cliente MySQL:

mysql -u root -p

Una vez nos logamos como administrador (hay que introducir la contraseña), en la línea de comandos del cliente MySQL introducimos la siguiente sentencia:

mysql> SHOW VARIABLES;

Buscamos las variables max_connections y thread_concurrency para verificar sus valores.