Usuários Únicos Aleatórios
Tabela de conteúdos
intermediário - This article is part of a series.
Olá todos, após um tempo sem escrever entradas no blog no blog, estamos de volta. Esta vez eu vou compartilhar com você uma solução para um dos problemas mais comuns quando executar testes de carga ou de estresse: evitar colisões de dados por sessões de usuário concurrente. Vou explicar detalhadamente o problema e nossa elegante solução, pois geralmente o que buscamos é a rotação adequada de dados de teste para evitar cache interno.
Premissas: #
- Temos uma carga de teste com 100 usuários concurrentes.
- Precisamos entre 2 e 3 vezes o volume de dados para evitar cacheamento, entre 200 e 300 diferentes usuários.
- Os nomes de usuário e senhas devem preferencialmente seguir um padrão.
Para este exemplo, realizaremos uma carga de teste com 100 usuários em simultâneo, que pode ser facilmente escalado para até 1.000 ou 10.000. O dados necessários para evitar o caching no servidor aplicativo ou banco de dados são 2 a 3 vezes, então precisamos ter pelo menos 200 ou 300 dados de teste ou nomes de usuário. Finalmente, para que este esquema funcione perfeitamente, é necessário que os nomes de usuários tenham um padrão e seria bom se seus senhas fossem as mesmas para todos eles, algo como user001, user002,…,user299,user300. Portanto, o padrão seria user + número aleatório, claro que esse número aleatório deve ser um número entre 1 e 300 com uma máscara para adicionar zeros à esquerda para completar a forma do padrão.
Então temos uma experiência experimental aleatória para obter 100 números aleatórios, com um espaço amostral de 300 (S={1,2,…,299,300}) e um evento ou resultado de obter esses valores para os usuários para assinar. Se a aplicação permite múltiplas sessões, poderíamos ter menos problemas, porque é válido usar o mesmo usuário múltiplas vezes para assinar. Alternando, se não permitir múltiplas sessões, isso tornaria-se um problema. Porque quando gerar 100 números aleatórios, a probabilidade de obter valores únicos do espaço amostral diminui conforme avançamos na sequência dos eventos. Portanto, é necessário implementar uma mecanismo para evitar colisões ou problema de carrera para nosso teste.
Se escolhemos um número aleatório entre 1 e 300, a probabilidade de obter esse valor é P(1/300) ou 0.33%, mas para o segundo evento há uma probabilidade crescente de repetir essa valor ao avançarmos, a probabilidade de obter valores únicos diminui, já que números pseudo-aleatórios gerados são uniformemente distribuídos e portanto é mantido que haverá um número aleatório selecionado repetidamente em algum iterativo próximo, por exemplo, o primeiro número aleatório é igual a 344, mas na iteração 450 obtemos esse valor novamente, então não são únicos e irreprodutíveis.
Como podemos evitar este problema? #
A solução é mais fácil do que você imagina e pode ser escalável se estiver usando múltiplos geradores de carga, primeiro resolveremos o problema inicial. A solução é gerar uma estrutura de dados em que podemos armazenar esses valores aleatórios e continuar gerando até obter a quantidade desejada. Quando obtivermos os valores subsequentes, verificaremos se este valor existe na estrutura ou não; se não existir, o introduziremos, caso contrário, removê-loemos e continuemos até obtermos a quantidade necessária. Agora, isso deve ser feito entre cada iteração da fila de threads para garantirmos que no primeiro iterador da fila de threads certas usuários aleatórias tenham sido usadas, e nas iterações subsequentes ou em futuras, uma vez ou outra, um conjunto completamente novo de valores aleatórios será usado.
O código abaixo está documentado para facilitar o leitura; ele deve ser colocado em um pré-processador JSR223 na primeira requisição do nosso script.
ArrayList<Integer> Usuarios = new ArrayList<Integer>() //Lista de usuários
int getIteration = vars.getIteration().toInteger() //Atualização da iteração do grupo de threads
Random randomGenerator = new Random()
def iteration = props.get("iteration") //Propriedade para armazenar as iterações
def getUsers = props.get("Users") //Propriedade para armazenar a lista de usuários
//Usamos def se podemos obter valores nulos
int random //Atualização do valor aleatório
if (iteracao == null || iteracao < getIteration || getUsers.size() < 1) {
//Validamos se a iteração é nula
//Que a lista tenha pelo menos um elemento
while (getUsers.size() <= n) {
//Nº de elementos da lista mínima
RandUser = randomGenerator.nextInt(n);
//Gerar número aleatório entre 1 e n
if (!getUsers.contains(RandUser)) {
getUsers.add(RandUser);
}
}
iteracao = getIteration;
props.put("iteration", iteracao);
props.put("Users", getUsers);
log.info("Users: " + props.get("Users").toString());
}
Quando obtermos a lista de valores, o que restará é atribuir cada thread à sua correspondente valor. Isso também pode ser feito através de um pre-processador JSR223 anterior em que atribuímos o valor da lista a uma variável local porque lembramos que essa lista está armazenada em uma propriedade que é uma variável global.
int threadNum = ctx.getThreadNum().toInteger()
def getUsers = props.get("Users")
vars.put("User",getUsers[threadNum].toString())
Pronto na variável Users, que pode ser acessada via ${User}
, podemos substituir o valor por um número aleatório não repetido. Esta solução é muito melhor do que usar arquivos CSV, porque a maneira de se obter informações nunca é sequencial, mesmo se estamos falando sobre acesso à base de dados. E dá simplicidade e beleza para nossos scripts.
Você pode deixar uma ligação para baixar o script de exemplo aqui.
Conclusão e Escalabilidade #
Para finalização, essa mecanismos é fácil escalável para até generador de carga com 1.000 usuários por generador de carga, mas para evitar colisões entre múltiplos generadores de carga, basta atribuir a cada um um valor único índice. Portanto, o primeiro generador de carga seria atribuído o valor de 0 e assim sucessivamente até o valor de n. Para garantir que o generador de carga com índice 0 lidere os primeiros 2.000 ou 3.000 valores por executar apenas 1.000 usuários concorrentes, conforme mostrado abaixo:
Gerador | Usuários Concorrentes | 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 |
Isso é possível através de uma execução centralizada ou descentralizada, ambas as formas minha recomendação é usar um propriedade no arquivo users.properties do cada gerador de carga com o objetivo de manualmente atribuir esses valores, e dentro do JSR223 em groovy use esse valor de propriedade como multiplicador para ranges.