Usuarios aleatorios únicos

title

Hola a todos, después de un tiempo sin escribir entradas en el blog estamos de vuelta, en esta ocasión les voy a compartir una solución a uno de los retos más comunes al ejecutar pruebas de carga o estrés evitando colisiones de los datos de prueba por usuario concurrente, déjenme explicarles a detalle la problemática y nuestra elegante solución, dado que generalmente lo que buscamos es tener una buena rotación de datos de prueba para evitar el cache interno.

Ejemplo:

  1. Tenemos una prueba de carga de 100 usuarios concurrentes.
  2. Necesitamos de 2 a 3 veces datos de prueba para evitar el caching, de 200 a 300 usuarios diferentes.
  3. Los nombre de usuarios y contraseñas deberán preferiblemente tener un patrón.

Para este ejemplo vamos a realizar un prueba de carga de 100 usuarios concurrentes, que puede ser sencillamente extrapolado a 1,000 o 10,000. Los datos de prueba requeridos para evitar el caching en el servidor de aplicación o en la base de datos, es de 2 a 3 veces, por lo tanto necesitamos como mínimo 200 o 300 datos de prueba o nombres de usuario. Finalmente para que este esquema pueda funcionar a la perfección requerimos que el nombre de los usuarios tenga un patrón y de ser posible que la contraseña sea la misma para todos ellos, tendríamos algo como usuario001,usuario002,...,usuario299,usuario300. Por lo tanto el patrón sería usuario + numero aleatorio, claro que este número aleatorio tendría que ser un número entre 1 y 300 con una máscara para agregar los ceros a la izquierda para completar el formato del patrón.

Entonces, tenemos un experimento aleatorio para obtener 100 números aleatorios, con un espacio muestral de 300 (s={1,2,...,299,300}) y con un evento o suceso de obtener estos valores para firmar a los usuarios. Si la aplicación permite la multi-sesión pudiéramos tener menos problemas, porque es válido utilizar la misma cuenta para firmarla más de una vez. En caso contrario si no está permitida la multi-sesión, esto se tornaría un problema. Porque al generar 100 números aleatorios, la probabilidad de obtener valores únicos del espacio muestral disminuye a medida que avanzamos en la sucesión de eventos. Por lo siguiente es necesario implementar un mecanismo para evitar está colisión o problemática de carrera para nuestro prueba.

Si realizamos un número aleatorio entre 1 y 300, la probabilidad de obtener dicho valor es P(1/300) o 0.33%, pero para el segundo evento existe la probabilidad de repetir este valor que se va incrementando a medida que avanzamos, la probabilidad de obtener valores únicos disminuye dado que los números pseudo-aleatorios generados, son uniformemente distribuidos y por lo tanto se trata de mantener ese uniformidad, en alguna iteración cercana tendremos un número aleatorio repetido, por ejemplo, el primer aleatorio es igual a 344, pero en la iteración 450, volvimos a obtener este valor, por lo tanto no son únicos e irrepetibles.

¿Cómo podemos evitar este problema?

La solución es más fácil de lo que parece y fácilmente escalable si estás ocupando varios generadores de carga, primero vamos a resolver el problema inicial. La solución es generar un estructura de datos en la cual podemos almacenar estos valores aleatorios y continuar generando hasta obtener la cantidad deseada. Una vez que obtengamos los valores subsecuentes, vamos a verificar si este valor existe en la estructura o no, si no existe lo introducimos, en caso de existir pues lo eliminamos y continuamos hasta obtener la cantidad necesaria. Ahora bien, esto se debe hacer entre cada iteración del grupo de hilos, para que podamos garantizar que en la primera iteración del grupo de hilos se utilizaron ciertos usuarios aleatorios, y en la segunda iteración o subsecuentes se utilice otro conjunto totalmente fresco de valores aleatorios.

El siguiente código está documentado para facilidad de lectura, se debería colocar en un JSR223 pre-procesador en la primera petición de nuestro script.

ArrayList<Integer> Users = new ArrayList<Integer>()  //Lista de usuarios
int getIteration = vars.getIteration().toInteger()   //Iteración actual del grupo de hilos
Random randomGenerator = new Random()                 
def iteration = props.get("iteration")	             //Propiedad para almacenar las iteraciones
def getUsers = props.get("Users")                    //Propiedad para almacenar la lista de usuarios
                                                     //Utilizamos def ante la posibilidad de obtener valores nulos
int random                                           //Valor aleatorio actual

if ( iteration == null || iteration < getIteration || getUsers.size() < 1){	//Validamos si la iteracion es nula
                                                                            //Que la lista tenga al menos 1 elemento
	while (Users.size() <= n) {                                             //Tamaño mínimo de la lista
                                                                            // de n elementos
	    RandUser = randomGenerator.nextInt(n)								//Generamos el numero aleatorio del 
                                                                        	// 1 al n
	    if (!Users.contains(RandUser)) {                                    //Si el valor no esta en el arreglo
	        Users.add(RandUser)												// lo introducimos
	    }
	}
	iteration = getIteration                                                //Hacemos la iteración actual
	props.put("iteration",iteration)                                        //Guardamos la iteración global
	props.put("Users",Users)                                                //Guardamos la lista global
log.info(" Users: "+props.get("Users").toString())						  	//Imprimir la lista en la bitácora
}

Una vez que obtengamos la lista de valores, lo único restante es asignar a cada hilo su valor correspondiente, esto también puede ser por medio otro JSR223 pre-procesador en el cual asignemos el valor de la lista a una variable local, porque recordemos que esta lista esta guardada en un propiedad que es una variable global.

int threadNum = ctx.getThreadNum().toInteger()
def getUsers = props.get("Users")
vars.put("User",getUsers[threadNum].toString())

Listo en la variable Users, a la cual se puede acceder por medio de un ${User}, podemos substituir el valor por un número aleatorio que no está repetido. Esta solución es mucho mejor que utilizar archivos CSV, porque el método de acceso a la información no siempre es secuencial, si no aleatoria sobre todo si hablamos de accesos a base de datos. Y le otorga una simplicidad y elegancia a nuestros scripts.

También les dejo una liga para que puedan descargar un script de ejemplo.

script

Conclusión y Escalabilidad

Para finalizar, este mecanismo es fácilmente escalable a 1,000 usuarios por generador de carga, pero para evitar colisiones entre varios generadores de carga todo es tan simple como asignarles un valor de indice a cada uno de ellos. De tal forma que el primer generador de carga sería el 0 y así hasta el valor n. Para que el generador de carga 0 se haga cargo de los primeros 2,000 o 3,000 valores ejecutando solamente 1,000 usuarios concurrentes. Como se muestra a continuación:

Generador Usuarios Concurrentes Rango
0 1,000 0 - 3,000
1 1,000 3k - 6k
2 1,000 6k - 9k
3 1,000 9k - 12k
n 1,000 nk

Esto es posible mediante una ejecución distribuida centralizada o no centralizada, por ambos medios mi recomendación es utilizar una propiedad en el archivo users.properties de cada generador de carga con la finalidad de manualmente asignar estos valores, y dentro del código JSR223 en groovy, utilizar este valor de propiedad como multiplicador de rangos.

Espero esta información les sea de utilidad.

-Antonio