Testing en aplicaciones web: Load testing y Stress testing

por Gustavo Lombardo  1. abril 2011

El rendimiento es una de las mayores preocupaciones en nuestras aplicaciones web y de comercio electrónico, ya que procesan miles de transacciones para miles de usuarios en algunos segundos, la escalabilidad y estabilidad de cargas es esencial para que las aplicaciones tengan éxito. Diversos estudios han indicado que los usuarios no toleran un interfaz con tiempos de respuesta mayores a 8 segundos y que el 58% de los clientes nunca vuelven a un sitio Web con bajo rendimiento.

Las crecientes demandas por mayor calidad en estos sistemas, han transformado al testing en una amenaza importante para los equipos de desarrollo de software y para los presupuestos de las compañías. Por este motivo les contaré de un par de herramientas bastante útiles para el testeo de nuestra aplicaciones on-line.

Primero veamos cuales son las diferencias entre el Load testing y Stress testing. Los test de carga (Load testing) nos permiten verificar el comportamiento de nuestra aplicación bajo condiciones de carga máxima y normales verificando si el sistema satisface los requisitos de rendimiento para situaciones críticas como, por ejemplo, la cantidad límite de usuarios accediendo de forma concurrente a los servicios, cantidad de transacciones que se pueden procesar de forma concurrente cada minuto, etc. Con esto comprobaremos que la aplicación satisface con los objetivos de rendimiento que deseados. Podes medir:

  • tiempos de respuesta,
  • las tasas de rendimiento,
  • la utilización de los niveles de recursos,
  • identificar el punto de ruptura de la aplicación, en el supuesto de que se produzca bajo condiciones de carga máxima.

 

Los test de estrés o de tensión (Stress testing) en cambio, permite evaluar el comportamiento de la aplicación cuando las forzamos más allá de las condiciones de carga máxima o normal desbordando sus recursos o reduciéndolos. Con dicho testing identificaremos los puntos débiles de la aplicación, cómo se comporta bajo condiciones de carga extrema y asegurar que tras un fallo el sistema, se recupera sin causar graves problemas.

Con estos tipos de test podremos identificar cuellos de botella y sus causas y optimizar la configuración de la plataforma (tanto el hardware y software) para obtener el máximo rendimiento. El proceso para ambos testing incluye los siguientes pasos:

Drawing1

Recomendaciones a tener en cuenta

  • El testeo de carga y de estrés hay que hacerlo siempre antes de que el sistema entre en producción reduciendo la probabilidad de llevarnos con una sorpresa cuando el sistema comienza a utilizarse.
  • Realizar el testeo en una red privada aislada.
  • Contar con un ancho de banda de red 100 Mbps o superior.
  • Varias placas de red para distribuir la carga.
  • Una máquina multiprocesador para las pruebas de escalabilidad.

Para las pruebas de estrés escogí la herramienta gratuita: Web Capacity Analysis Tool 5.2 (WCAT). Con esta herramienta podremos probar y planear la capacidad que tiene nuestro sitio, probar el servidor y diferentes configuraciones de red mediante contenido de diseño personalizado y simulaciones de carga de trabajo. Cuenta con tres componentes:

  • Web Server es el sistema bajo prueba. El software de servidor web puede ser de cualquier versión de IIS, Apache, etc.
  • WCAT Controller envía señales a los WCAT Client para iniciar o detener la generación de carga HTTP, cuyos contadores de rendimiento recopila y consolida todos los datos recogidos por WCAT Client en un único informe.
  • WCAT Client genera carga HTTP directamente contra el servidor o servidores web .Los clientes serán controlados mediante el WCAT Controller.

    Les dejo el link para que la descarguen. Los pasos para realizar las pruebas son los siguientes:

1. Una vez descargada la herramienta es necesario crear tres archivos de configuración.

· script.txt: este archivo define las solicitudes, es decir, qué páginas de solicitar y cómo solicitarlo. Ejemplo:

NEW TRANSACTION
classId = 1
NEW REQUEST HTTP
Verb = "GET"
URL = “http://localhost/prueba/Page1.aspx” (aquí remplazan por la url que quieren probar)

· distribution.txt: define los pesos entre las diferentes peticiones. Por ejemplo, si tengo que generar solicitud de Page1.aspx dos veces a Page2.aspx, esto quedará establecido en este archivo. En el caso de la carga de una sola página, el archivo no tiene sentido. Ejemplo: 1 50 el primer parámetro hace referencia a classId en el archivo script.txt, y 50 es que el 50% de la carga que no tiene sentido si hago una solicitud a una sola página por lo que recibirá el 100% de carga completa. Ejemplo:
1 50
· config.txt: determina la duración de la prueba, el número de clientes que va a generar las peticiones contra la aplicación web. Ejemplo:

Warmuptime 5s
Duration 30s
CooldownTime 5s
NumClientMachines 1
NumClientThreads 20

2. Guardar los tres archivos la carpeta en "C:\Program Files (x86)\IIS Resources\WCAT Controller".

3. Ejecutar la prueba, para ello en una ventana de DOS ejecutar el comando:

wcctl -c config.txt -d distribution.txt -s script.txt -a localhost

4. En una nueva ventana de DOS ejecutar en "C:\Program Files (x86)\IIS Resources\WCAT Client" el comando wcclient.exe localhost.

Los resultados la herramienta los arroja en las ventana de consola correspondiente tanto en la del server como en el cliente. En la siguiente imagen se muestra los resultados en el cliente.

image_thumb3

En el caso de los test de carga, les presentaré una herramienta que esta integrada el Visual Studio desde la versión del 2008, se trata Load Test del Visual Studio, para ello:

1. En la misma solución del proyecto de ASP.NET, hay que agregar un nuevo Test project.

2. Hacer clic en el nodo TestProject en el Solution Explorer seleccionar la opción "Add", y se despliegan diferentes tipos de pruebas. Elegir un "Web Performance Test". En el Internet Explorer aparece una barra “Web Test Recorder”. Se introduce en el browser la url de la página que se desea probar. La grabadora de prueba grabará todo lo que se está haciendo: acceder al sistema, la navegación a otras páginas, y así sucesivamente. Para finalizar con la grabación, simplemente debemos hacer clic en "Stop Recording". De esta manera quedará en parte configurada la prueba web.

3. Para finalizar solo basta añadir una Load Test. De nuevo con el menú contextual sobre el proyecto, seleccionamos Agregar y, esta vez seleccionar "Load Test". Se abre un wizard como el que se muestra a continuación:

image_thumb7

Componentes del escenario

Los escenarios son importantes porque proporcionan la flexibilidad en la configuración de características de la prueba que permiten simulaciones complejas. Por ejemplo, se puede estar probando un sitio que tiene una gran cantidad de usuarios simultáneos con diferentes velocidades de conexión y diferente browser. También podríamos tener dos grupos diferentes de usuarios: aquellos usuarios internos que acceden al sitio con el mismo navegador y en una conexión LAN de alta velocidad y también los clientes externos.

  • Browser Mix: simula usuarios virtuales examinando un sitio Web a través de una variedad de exploradores Web, además de Internet Explorer.
  • Network Mix: simula usuarios virtuales examinando un sitio Web a través de una variedad de conexiones de red. La combinación de redes ofrece opciones que incluyen una red LAN, cable módem, y otros.
  • Load Pattern: especifica un número de usuarios virtuales activos durante una prueba de carga y la velocidad a la que los nuevos usuarios se han iniciado.
  • Test Mix: especifica la probabilidad de que un usuario virtual ejecute una prueba determinada en un escenario de prueba de carga. Se puede seleccionar la combinación de pruebas que se prefiera ajustando los controles deslizantes en la distribución de la columna, o escribiendo los valores porcentuales directamente en el % de la columna.

Los reportes que nos devuelve esta herramienta pueden visualizarse tanto en el Visual Studio o exportarse en formato Excel si lo quisiese. Así que solo resta jugar un poco con los diferentes parámetros de configuración e ir analizando los resultados que arroja.

Tags:

A pensar en paralelización: Parallel Extensions

por Gustavo Lombardo  29. julio 2010

Si deseamos que nuestro software siga beneficiándose de las capacidades futuras de hoy y mañana de los procesadores debemos empezar a cambiar nuestra manera de desarrollar. La razón de esto es que las aplicaciones de negocio van tener que ejecutarse en máquinas multi-core o con varios procesadores. Hasta hace pocos años una máquina con varios procesadores estaba prácticamente reservada a la supercomputación, pero hoy en día ya son comunes en cualquier PC de escritorio.

La programación concurrente y el paralelismo no son una materia trivial, la sincronización entre procesos no lo es y dividir el trabajo, asignarlo a varios procesadores, recoger los resultados y mezclarlos para reconstruir la solución final tampoco.

En el pasado, la paralelización requería manipulación de bajo nivel de los subprocesos y bloqueos. Hoy en día, los desarrolladores no tendrían que luchar contra esta complejidad, porque sería dar un paso atrás en productividad. Su tarea es seguir dedicando sus esfuerzos a lo que mejor saben hacer que es desarrollar aplicaciones de alto nivel que resuelven problemas de negocio sin controlar la ejecución concurrente de sus procesos.

Para ello, debemos acostumbrarnos a usar el término task en lugar del thread como seguramente la mayoría de nosotros veníamos haciendo y aprovechar las nuevas capacidades que ofrece las APIs de .NET Framework 4, así como también las del Visual Studio 2010.

.NET Framework 4, proporciona un nuevo runtime, nuevos tipos de biblioteca de clases y nuevas herramientas de diagnóstico para la programación paralela y concurrente. Es aquí donde hace su aparición Parallel Extensions, simplificando el desarrollo en paralelo, de modo de poder escribir código paralelo eficaz, específico y escalable de forma natural sin tener que trabajar directamente con subprocesos. Sin embargo, no todo código se presta para la paralelización por ejemplo, si un bucle realiza solo una cantidad reducida de trabajo en cada iteración o no se ejecuta para un gran número de iteraciones, la sobrecarga de la paralelización puede dar lugar a una ejecución más lenta del código. Además, al igual que cualquier código multiproceso, la paralelización hace que la ejecución del programa sea más compleja.

Parallel Extensions se compone de dos partes: Parallel Linq (PLINQ) y Task Parallel Library (TPL). También consta de un conjunto de estructuras de datos de coordinación (CDS) utilizada para sincronizar y coordinar la ejecución de tareas concurrentes. La siguiente ilustración proporciona información general de alto nivel de la arquitectura de programación paralela en .NET Framework 4.

IC389193

A continuación explicaré cada uno de sus componentes. PLINQ implementa el conjunto completo de operadores de consulta estándar de LINQ  e incluye otros adicionales para las operaciones paralelas. Combina la simplicidad y legibilidad de la sintaxis de LINQ con la eficacia de la programación paralela. En muchos escenarios, PLINQ puede aumentar significativamente la velocidad de las consultas LINQ  to Objects utilizando todos los núcleos disponibles en el equipo host de una forma más eficaz. Para hacer esto divide el origen de datos en segmentos y, a continuación, ejecuta la consulta en cada segmento en subprocesos de trabajo independientes en paralelo en varios procesadores. La clase System.Linq.ParallelEnumerable expone casi toda la funcionalidad de PLINQ.  Les mostraré un ejemplo en el que invoco el método de extensión ParallelEnumerableAsParallel() para indicarle que la query debe ejecutarse en forma paralela.

for (var i = 0; i < data.Length; i++)
{
    data[i] = i;
}
 
data[1000] = -1;
data[14000] = -2;
data[15000] = -3;
data[676000] = -4;
data[8024540] = -5;
data[9908000] = -6;
 
var negativos = from valor in data.AsParallel()
                where valor < 0
                select valor;

 

Al ejecutarse la query en paralelo los resultados de cada subproceso de trabajo deben volver a combinarse en el subproceso principal por ejemplo para la inserción en una lista. El tipo de combinación que  PLINQ realiza depende de los operadores que se encuentran en la consulta. Con el método WithParallelMergeOptions(), puede indicarse a PLINQ una sugerencia de que tipo de combinación se va a realizar.

La biblioteca TPL es un conjunto de API que tiene como propósito lograr que los desarrolladores aumenten la productividad simplificando el proceso de agregar paralelismo y simultaneidad a las aplicaciones, utilizando eficazmente todos los procesadores que están disponibles. Además, se encarga de la división del trabajo, la programación de los subprocesos y otros detalles de bajo nivel.

El concepto principal de Parallel Extensions es una tarea, que es una pequeña unidad de código, de forma independiente. Se encarga de dividir el trabajo y lanzar un número óptimo de threads basándose en el número de CPUs o cores que tenemos.

Yendo aun mas allá, sin llegar al nivel de granularidad de una task, podemos trabajar a más alto nivel aún con la clase estática Parallel y escribir código como el siguiente:

static void Main()
{
    Parallel.For(from, to, i=> )
    {
    });
}
 

Mediante un ciclo como el anterior, dejamos que el runtime de Parallel Extensions se encargue de paralelizar el trabajo creando las tasks y encargándose de  todo el proceso. A continuación les mostraré las mejoras de rendimiento al paralelizar un programa muy sencillo. El método SumaRaicesNesimaDeX devuelve la suma de las raíces n-énesima de todos los enteros entre 1 y un 10 millones donde n es una variable recibida como parámetro. La forma de resolución de la raíz contiene pasos de procesamiento adicionales que aumentan el tiempo de ejecución total de la consulta. Además, en el main utilizo la clase Stopwatch para determinar cuantos milisegundos tarda el programa en ejecutarse.

static void Main()
{
    var tiempo = Stopwatch.StartNew();
    
    for (var i = 2; i < 20; i++)
    {
        var resultado = SumaRaicesNesimaDeX(i);
        Console.WriteLine("Raiz {0} = {1} ", i, resultado);
    }
 
    Console.WriteLine(tiempo.ElapsedMilliseconds);
    Console.ReadLine();
}
 
public static double SumaRaicesNesimaDeX(int n)
{
    double resultado = 0;
 
    for (var x = 1; x < 10000000; x++)
    {
        //Raíz n-ésima de x
        resultado += Math.Exp(Math.Log(x) / n);
    }
 
    return resultado;
}
 

El programa lo ejecute en AMD Phenom II X4 810 de 64 bits con 4 GB de memoria RAM y tardó aproximadamente 16 segundos.

Ahora si remplazo el ciclo:

for (var i = 2; i < 20; i++)
{
    var resultado = SumaRaicesNesimaDeX(i);
    Console.WriteLine("Raiz {0} = {1} ", i, resultado);
}

 

por el siguiente código añadiéndole paralelismo:

Parallel.For(2, 20, (i) =>
{
    var resultado = SumaRaicesNesimaDeX(i);
    Console.WriteLine("Raiz {0} = {1} ", i, resultado);
});

Mediante pequeñas modificaciones del código como son un índice de inicio y de fin y la llamada a través de un delegate, la ejecución del programa solo tarda aproximadamente 4 segundos y medio.

Visual Studio 2010 incluye nuevas herramientas de profiling y debugging para la ejecución concurrente. Un ejemplo de esto es el visualizador de concurrencia. Esta herramienta ayuda  a analizar nuestras aplicaciones secuenciales para descubrir las oportunidades de paralelismo. El visualizador de concurrencia incluye visualización y herramientas de reporte. Hay tres puntos de vista principales: utilización de CPU, subprocesos y núcleos.

imageEl eje X muestra el tiempo transcurrido desde el inicio de la traza hasta el final de la actividad de la aplicación. El eje Y muestra el número de núcleos de procesadores lógicos en el sistema. La zona verde representa el número medio de cores lógicos que se analiza en la aplicación en un momento dado en la ejecución del profiling. El resto de los núcleos por otros procesos que se ejecutan en el sistema (que se muestra en amarillo).

Si queremos paralelizar nuestra aplicación, debemos buscar áreas de ejecución que presentan largas regiones verdes a nivel de un solo núcleo en el eje Y o regiones donde no hay mucha utilización de la CPU, donde el verde no demuestra ni es considerablemente menos de 1 en promedio. Ambas circunstancias podrían indicar una oportunidad para la paralelización. En segundo lugar, si estamos tratando de afinar la aplicación paralela, esta vista le permite confirmar el grado de paralelismo que existe cuando la aplicación se ejecuta.

Este cambio de paradigma nos exige una nueva manera de pensar. Los threads son una abstracción demasiado débil. Debemos empezar a pensar en tareas no en threads y en relaciones entre esas tareas y su concurrencia en lugar de sincronización de threads. Seremos los desarrolladores quienes de un modo u otro tendremos que empezar a pensar en paralelización.

Tags:

.NET | Entity Framework | Visual Studio

Sintaxis diferentes para consultas en LINQ to Entities

por Gustavo Lombardo  4. febrero 2010

Hoy en día la mayor parte de las aplicaciones se desarrollan para que accedan a los datos de las bases interactuando con los mismos ya que se encuentran representados en un formato relacional (modelo relacional). Entity Framework nos permite trabajar con datos en forma de objetos y propiedades específicas del dominio sin tener que pensar en las tablas de las bases de datos o columnas en las que se almacenan estos datos. LINQ (Language-Integrated Query) nos facilita la vida a la hora de formular consultas en el código de las aplicaciones para interactuar con la base de datos, sin tener que usar un lenguaje de consulta independiente. A través de la infraestructura de Entity Framework, ADO.NET  muestra una vista conceptual común de los datos, como objetos del entorno .NET. Esto hace que la capa de datos sea compatible con LINQ. Esta tecnología, LINQ (LINQ to Entities), nos permite escribir consultas flexibles mediante el uso de operadores de consulta estándar y de expresiones LINQ en el entorno de desarrollo. Las consultas se expresan en el propio lenguaje de programación y no como literales de consulta incrustados en el código permitiendo que el compilador detecte errores de sintaxis en tiempo de compilación.

Una operación de consulta LINQ consta de tres acciones: obtener el origen o los orígenes de datos, crear la consulta y luego ejecutarla. Las instancias de la clase ObjectQuery, que implementa la interfaz genérica IQueryable, actúan como origen de datos para las consultas LINQ to Entities. La clase ObjectQuery genérica representa una consulta que devuelve una instancia o colección de entidades tipadas. Primero debemos crear una instancia de ObjectContext, que es la clase principal para interactuar con un Entity Data Model (EDM) como objetos de CLR (Common Language Runtime). Luego en la consulta especificamos exactamente la información que deseamos recuperar del origen de datos. Una vez creada se debe ejecutar.

Las consultas de LINQ to Entities se pueden formular en dos sintaxis diferentes:

 

  • Sintaxis de expresiones de consulta
  • Sintaxis de consultas basadas en métodos

 

La sintaxis de expresiones de consulta consta de un conjunto de cláusulas escritas en una sintaxis declarativa similar a Transact-SQL. Al usar la sintaxis de consulta, se pueden realizar operaciones complejas de filtrado, clasificación y agrupación en orígenes de datos con una cantidad mínima de código. En el siguiente ejemplo se muestra la operación de consulta completa que incluye la creación de un origen de datos, la definición de la expresión de consulta y la ejecución de la consulta en una instrucción forech:

 

class LINQQueryExpressions
{
    static void Main()
    {
        // Especificar origen de datos.
        int[] numeros = new int[] { 97, 92, 81, 60 };
 
        // Definir la consulta mediante la sintaxis de expresiones.
        IEnumerable<int> listaNumeros =
        from nro in numeros
        where nro > 80
        select nro;
 
        // Ejecutar la consulta.
        foreach (int i in listaNumeros)
        {
            Console.Write(i + " ");
        }            
    }
}
// Salida: 97 92 81

 

Todas las variables utilizadas con este tipo de sintaxis tienen establecimiento inflexible de tipos, aunque no es necesario que proporcionemos el tipo explícitamente ya que el compilador puede deducirlo. Debido a que .NET CLR no reconoce la sintaxis de expresiones de consulta por sí mismo, en tiempo de compilación las expresiones de consulta se convierten en elementos que CLR reconoce: las llamadas a métodos. Estos métodos se conocen como operadores de consulta estándar y tienen nombres como Where, Select, GroupBy, Join, Max, Average, etc. Este tipo de sintaxis no es más que una secuencia de llamadas directas a los métodos de operador de LINQ a las que le debemos pasar como parámetros expresiones lambda. En el siguiente ejemplo use el Select para devolver todas las filas de Empleados que cumplan con la condición  de tener 33 años y mostrar sus nombres.

 

using (EmpresaSAEntities data= new EmpresaSAEntities())
{
    IQueryable<string> nombreEmpleados = data.EmpleadosSet
        .Where(e=> e.Edad == 33)
        .Select(p => p.Nombre);
 
    Console.WriteLine("Nombre Empleados:");
    foreach (var nombre in nombreEmpleados)
    {
        Console.WriteLine(nombre);
    }
}

 

Para ambas sintaxis es posible usar operadores estándar para confeccionar consultas de agrupación (GroupBy), de combinación (Join o GroupJoin), de ordenamiento (OrderBy combinado con ThenBy o ThenByDescending por ejemplo), etc. Algunas operaciones de consulta como Count o Max, no tienen ninguna cláusula de expresión equivalente y, por tanto, deben expresarse como una llamada a método. La sintaxis de método se puede combinar con la sintaxis de consulta de varias maneras.

Como programador, tenemos la posibilidad de escribir la consulta mediante sintaxis de consulta como también con sintaxis de método. Sin embargo, en la mayoría de los casos, la sintaxis de consulta es más fácil y concisa porque utiliza muchas construcciones de lenguaje de C# familiares. Si bien, no existe ninguna diferencia semántica o de rendimiento entre las dos formas, las expresiones de consulta suelen ser más legibles que las expresiones equivalentes escritas con sintaxis de método. Un claro ejemplo que nos llevo a cambiar por sintaxis de consulta en el proyecto del que formo parte, es que la sintaxis de método resulta casi ilegible al tener una consulta con una gran cantidad de Joins o GroupJoins en secuencia donde los datos son seleccionados mediante clases anónimas. Veamos este caso:

Ejemplo con sintaxis de consulta basada en método:

 

//Join entre la tabla PermisosSet con los EmpleadosSet 
//para obtener los ids de Empresa y Articulo.    
var permisos = data.PermisosSet
    .Join(data.EmpleadosSet,
    p => p.IdLegajo,
    e => e.IdLegajo,
    (p, e) => new
    {
        p.IdEmpresa,
        p.IdArticulo
    });
 
//Join entre el IQueryable de "permisos" con los 
//EmpresasSet para obtener información de la Empresa.
var resultadoConEmpresas = data.EmpresasSet
    .Join(permisos,
    em => em.IdEmpresa,
    p => p.IdEmpresa,
    (e, p) => new
    {
        p.IdArticulo,
 
        DatosEmpresas = new
        {
            em.CodEmpresa,
            em.Descripcion
        }
    });
 
//Join entre el IQueryable de " resultadoConEmpresas " con 
//los ArticulosSet para obtener información del Articulo.
var resultadoFinal = data.ArticulosSet
    .Join(resultadoConEmpresas,
    a => a.IdArticulo,
    r => r.IdArticulo,
    (a, r) => new
    {
        DatosArticulos = new
        {
            a.CodArticulo,      
                a.Descripcion,
                a.Vigencia
        }
    });

 

Ejemplo con sintaxis de expresiones de consulta:

 

var permisos = from r in data.PermisosSet
    //Realizar todos los Joins
    join e in data.EmpleadosSet on r.IdLegajo equals e.IdLegajo
    join em in data.EmpresasSet on r.IdEmpresa equals em.IdEmpresa
    join a in data.ArticulosSet on r.IdArticulo equals a.IdArticulo
    
    //El Select se realiza al final para obtner todos los datos que se retornaran.
    select new
    {
        DatosEmpresas = new
        {
            em.CodEmpresa,
            em.Descripcion
        },
 
        DatosArticulos = new
        {
            a.CodArticulo,      
            a.Descripcion,
            a.Vigencia
        }
    };

 

Si bien las consultas devuelven los mismos resultados puede verse claramente que la consulta confeccionada mediante sintaxis de expresiones es más legible y similar a T-SQL que la realizada son sintaxis de métodos.

 

Tags: ,

ADO.NET | Entity Framework

Acerca de los Autores

Este es el blog del equipo de VEMN SA 
Presentaremos temas que nos parezcan de interés sobre tecnología .NET, Procesos y Metodologías y todo aquello relacionado con el proceso de desarrollo de Software

Month List