Ao testar alterações em uma aplicação, há alguns fatores que só podem ser medidos em um ambiente de produção, e não em um ambiente de testes de desenvolvimento. Alguns exemplos são o efeito das alterações na interface do usuário sobre o comportamento dos usuários e o impacto no desempenho geral. Um método de teste comum é o teste A/B — também conhecido como teste dividido — no qual uma parcela (geralmente pequena) dos usuários é direcionada para a nova versão de uma aplicação, enquanto a maioria continua usando a versão atual.
Nesta publicação do blog, exploraremos por que é importante realizar testes A/B ao implantar novas versões da sua aplicação web e como usar o NGINX e o NGINX Plus para controlar qual versão de uma aplicação os usuários veem. Os exemplos de configuração mostram como usar diretivas, parâmetros e variáveis do NGINX e do NGINX Plus para realizar testes A/B precisos e mensuráveis.
Por que fazer testes A/B?
Como mencionamos, os testes A/B permitem medir a diferença de desempenho ou eficácia do aplicativo entre duas versões. Talvez sua equipe de desenvolvimento queira alterar a disposição visual dos botões na interface do usuário ou reformular todo o processo do carrinho de compras, mas queira comparar a taxa de conclusão das transações para garantir que a mudança tenha o impacto desejado nos negócios. Com os testes A/B, você pode direcionar uma porcentagem definida do tráfego para a nova versão e o restante para a versão antiga, além de medir a eficácia de ambas as versões do aplicativo.
Ou talvez sua preocupação seja menos com o efeito sobre o comportamento dos usuários e mais com o impacto no desempenho. Digamos que você planeje implantar um grande conjunto de alterações em sua aplicação web, mas não acredite que os testes realizados em seu ambiente de garantia de qualidade reflitam de fato o possível impacto no desempenho em produção. Nesse caso, uma implantação A/B permite disponibilizar a nova versão para uma porcentagem pequena e definida de visitantes a fim de medir o impacto das alterações no desempenho e aumentar gradualmente essa porcentagem até que, por fim, a aplicação modificada seja disponibilizada para todos os usuários.
Como usar o NGINX e o NGINX Plus para testes A/B
O NGINX e o NGINX Plus oferecem dois métodos para controlar para onde o tráfego de aplicações web é direcionado. O primeiro método está disponível em ambos os produtos, enquanto o segundo está disponível apenas no NGINX Plus.
Ambos os métodos escolhem o destino de uma solicitação com base nos valores de uma ou mais variáveis do NGINX que capturam características do cliente (como seu endereço IP) ou do URI da solicitação (como um argumento nomeado), mas as diferenças entre eles os tornam adequados para diferentes casos de uso de testes A/B:
- O método
split_clientsescolhe o destino de uma solicitação com base em um hash dos valores das variáveis extraídos da solicitação. O conjunto de todos os valores de hash possíveis é dividido entre as versões do aplicativo, e você pode atribuir uma proporção diferente do conjunto a cada aplicativo. A escolha do destino acaba sendo aleatória. - O método
stickyrouteoferece muito mais controle sobre o destino de cada solicitação. A escolha da aplicação é baseada nos próprios valores das variáveis (não em um hash), permitindo definir explicitamente qual aplicação recebe as solicitações que contêm determinados valores de variáveis. Você também pode usar expressões regulares para basear a decisão apenas em partes do valor de uma variável e escolher preferencialmente uma variável em vez de outra como base para a decisão.
Como usar o método split_clients Method
Nesse método, o bloco de configuração split_clients define uma variável para cada solicitação que determina para qual grupo upstream a diretiva proxy_pass envia a solicitação. No exemplo de configuração abaixo, o valor da variável $appversion determina para onde a diretiva proxy_pass envia uma solicitação. O bloco split_clients usa uma função hash para definir dinamicamente o valor da variável como um dos dois nomes de grupos upstream: version_1a ou version_1b.
http { # ...
# application version 1a
upstream version_1a {
server 10.0.0.100:3001;
server 10.0.0.101:3001;
}
# application version 1b
upstream version_1b {
server 10.0.0.104:6002;
server 10.0.0.105:6002;
}
split_clients "${arg_token}" $appversion {
95% version_1a;
* version_1b;
}
server {
# ...
listen 80;
location / {
proxy_set_header Host $host;
proxy_pass http://$appversion;
}
}
}O primeiro parâmetro da diretiva split_clients é a string ("${arg_token}" em nosso exemplo), que passa por hash usando uma função MurmurHash2 durante cada solicitação. Os argumentos de URI ficam disponíveis para o NGINX como variáveis chamadas $arg_name – em nosso exemplo, a variável $arg_token captura o argumento de URI chamado token. Você pode usar qualquer variável do NGINX ou string de variáveis como a string a ser submetida a hash. Por exemplo, você pode gerar o hash do endereço IP do cliente (a variável $remote_addr), da porta ($remote_port) ou da combinação de ambos. É recomendável usar uma variável que seja gerada antes de a solicitação ser processada pelo NGINX. Variáveis que contêm informações sobre a solicitação inicial do cliente são ideais; alguns exemplos incluem o endereço IP e a porta do cliente, como já mencionado, o URI da solicitação ou até mesmo os cabeçalhos da solicitação HTTP.
O segundo parâmetro da diretiva split_clients ($appversion em nosso exemplo) é a variável definida dinamicamente de acordo com o hash do primeiro parâmetro. As instruções entre chaves dividem a tabela de hash em “grupos”, cada um contendo uma porcentagem dos hashes possíveis. Você pode criar quantos grupos quiser, e eles não precisam ter todos o mesmo tamanho. Observe que a porcentagem do último grupo é sempre representada pelo asterisco (*), em vez de um número específico, pois a quantidade de hashes pode não ser divisível de maneira uniforme entre as porcentagens especificadas.
Em nosso exemplo, colocamos 95% dos hashes em um bucket associado ao grupo upstream version_1a e o restante em um segundo bucket associado ao grupo version_1b. O intervalo de valores de hash possíveis vai de 0 a 4.294.967.295, portanto, o primeiro bucket contém valores de 0 a aproximadamente 4.080.218.930 (95% do total). A variável $appversion é definida como o upstream associado ao bucket que contém o hash da variável $arg_token. Como exemplo específico, o valor de hash 100.000.000 está no primeiro bucket, portanto, $appversion é definida dinamicamente como version_1a.
Testando a configuração split_clients Configuration
Para verificar se o bloco de configuração split_clients funciona conforme o esperado, criamos uma configuração de teste que divide as solicitações entre dois grupos de upstream na mesma proporção acima (95% e o restante). Configuramos os servidores virtuais dos grupos para retornar uma string indicando qual grupo — version_1a ou version_1b — processou a solicitação (você pode ver a configuração de teste aqui). Em seguida, usamos o curl para gerar 20 solicitações, definindo aleatoriamente o valor do argumento de URI token por meio da execução do comando cat no arquivo urandom. Isso serve apenas para fins de demonstração e aleatoriedade. Como pretendíamos, 1 em cada 20 solicitações (95%) foi atendida pelo version_1b (para sermos breves, mostramos apenas 10 das solicitações).
# for x in {1..20}; do curl 127.0.0.1?token=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1); doneToken: p3Fsa86HfJDFwZ9ZnYz4QbaZLzcb70Ka Served from site version_1a.
Token: a7z7afUz7zQijXwsLGfj6mWyPU8sCRIZ Served from site version_1a.
Token: CjIr6W57NtPzChf4qbYMzD1Estif7jOH Served from site version_1a.
... output for 10 requests omitted ...
Token: gXq8cbG3jhl1LuYmICPTfDQT855gaO5y Served from site version_1a.
Token: VVqGuNN3cRU2slsl5AWOR93aQX8nXOpk Served from site version_1a.
Token: z7KnewxTX5Sp6wscT0fgmTDohTsQuCmy Served from site version_1b!!
Token: fWOgH9WRrR0kLJZcIaYchpLhceaQgPD1 Served from site version_1a.
Token: mTADMXrVnwnr1cd5JE6QCSkgTwfWUnDk Served from site version_1a.
Token: w7AzSNmNJtxWZaH6cXe2PWIFqst2o3oP Served from site version_1a.
Token: QR7ay0dA39MmVlXtzgOVsj6SBTPi8ECC Served from site version_1a.Como usar o método sticky route Method
Em alguns casos, talvez você queira definir uma rota estática tomando decisões de roteamento do cliente com base no valor total ou parcial de uma variável do NGINX. Isso pode ser feito com a diretiva sticky route, disponível apenas no NGINX Plus. A diretiva recebe uma lista de um ou mais parâmetros e define a rota com o valor do primeiro parâmetro não vazio da lista. Podemos usar esse recurso para priorizar qual variável da solicitação controla a escolha do destino e, assim, permitir mais de um método de divisão de tráfego em uma única configuração.
Há duas abordagens diferentes para usar esse método.
- Usando a abordagem do lado do cliente, você pode escolher a rota com base em variáveis do NGINX que contêm valores enviados inicialmente diretamente pelo cliente, como o endereço IP do cliente ou cabeçalhos de solicitação HTTP específicos do navegador, como o
User-Agentdo cliente. - Com a abordagem do lado do servidor ou da aplicação, sua aplicação decide a qual grupo de teste um usuário iniciante será atribuído e envia a ele um cookie ou um URI de redirecionamento que inclui um indicador de rota representando o grupo escolhido. Na próxima vez que o cliente enviar uma solicitação, ele apresentará o cookie ou usará o URI de redirecionamento; a diretiva
stickyrouteextrairá o indicador de rota e encaminhará a solicitação ao servidor apropriado.
Em nosso exemplo, usamos a abordagem no lado da aplicação: a diretiva sticky route no grupo upstream define preferencialmente a rota como o valor especificado no cookie fornecido por um servidor (capturado em $route_from_cookie). Se o cliente não tiver um cookie, a rota será definida como um valor de um argumento do URI da solicitação ($route_from_uri). O valor da rota determina então qual server do grupo upstream recebe a solicitação – o primeiro servidor se a rota for a, e o segundo servidor se a rota for b (os dois servidores correspondem às duas versões da nossa aplicação).
upstream backend { zone backend 64k;
server 10.0.0.200:8098 route=a;
server 10.0.0.201:8099 route=b;
sticky route $route_from_cookie $route_from_uri;
}Mas o caractere a ou b está incorporado a uma cadeia de caracteres muito mais longa no cookie ou URI real. Para extrair apenas a letra, configuramos um bloco de configuração map para o cookie e outro para o URI:
map $cookie_route $route_from_cookie { ~.(?P<route>w+)$ $route;
}
map $arg_route $route_from_uri {
~.(?P<route>w+)$ $route;
}No primeiro bloco map, a variável $cookie_route representa o valor de um cookie chamado ROUTE. A expressão regular na segunda linha, que usa a sintaxe de Expressão Regular Compatível com Perl (PCRE), extrai parte do valor — neste caso, a cadeia de caracteres (w+) após o ponto — para o grupo de captura nomeado route e a atribui à variável interna com esse nome. O valor também é atribuído à variável $route_from_cookie na primeira linha, o que o disponibiliza para ser passado à diretiva sticky route.
Por exemplo, o primeiro bloco map extrai o valor “a” desse cookie e o atribui a $route_from_cookie:
ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.aNo segundo bloco map, a variável $arg_route representa um argumento chamado route no URI da solicitação. Assim como ocorre com o cookie, a expressão regular na segunda linha extrai parte do URI – neste caso, a sequência de caracteres (w+) após o ponto no argumento route. O valor é lido no grupo de captura nomeado, atribuído a uma variável interna e também atribuído à variável $route_from_uri.
Por exemplo, o segundo bloco map extrai o valor b deste URI e o atribui a $route_from_uri:
www.example.com/shopping/my-cart?route=iLbLr35AeAET39GvWK2Xd2GI5c24y5go.bAqui está a configuração completa de exemplo.
http { # ...
map $cookie_route $route_from_cookie {
~.(?P<route>w+)$ $route;
}
map $arg_route $route_from_uri {
~.(?P<route>w+)$ $route;
}
upstream backend {
zone backend 64k;
server 10.0.0.200:8098 route=a;
server 10.0.0.201:8099 route=b;
sticky route $route_from_cookie $route_from_uri;
}
server {
listen 80;
location / {
# ...
proxy_pass http://backend;
}
}
}Testando a configuração sticky route Configuration
Quanto ao método split_clients, criamos uma configuração de teste, que você pode acessar aqui. Usamos o curl para enviar um cookie chamado ROUTE ou incluir um argumento route no URI. O valor do cookie ou argumento é uma string aleatória gerada executando o comando cat no arquivo urandom, com .a ou .b acrescentado ao final.
Primeiro, testamos com um cookie que termina em .a:
# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).a" 127.0.0.1Cookie Value: R0TdyJOJvxBkLC3f75Coa29I1pPySOeQ.a
Request URI: /
Results: Site A - Running on port 8089Em seguida, testamos com um cookie que termina em .b.
# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).b" 127.0.0.1Cookie Value: JhdJZrScTnPBLhqmzK3podNRcJAIc8ST.b
Request URI: /
Results: Site B - Running on port 8099Por fim, fazemos o teste sem um cookie e, em vez disso, usamos um argumento de rota no URI da solicitação que termina em .a. A saída confirma que, quando não há cookie (o campo Cookie Value está vazio), o NGINX Plus usa o valor da rota derivado do URI.
# curl 127.0.0.1?route=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).aCookie Value:
Request URI: /?route=yNp8pHskvukXK6XqbWefhVUcOBjbJv4v.a
Results: Site A - Running on port 8089Registrando e analisando seus resultados
O tipo de teste que descrevemos aqui é suficiente para verificar se uma configuração distribui as solicitações conforme o esperado, mas interpretar os resultados de testes A/B reais exige registros e análises muito mais detalhados sobre como as solicitações são processadas. A maneira correta de realizar o registro e a análise depende de muitos fatores e está além do escopo deste post, mas o NGINX e o NGINX Plus oferecem recursos sofisticados e integrados de registro e monitoramento do processamento de solicitações.
Você pode usar a diretiva log_format para definir um formato de log personalizado que inclua qualquer variável do NGINX. Os valores das variáveis registrados nos logs do NGINX ficam disponíveis para análise posterior. Para obter detalhes sobre logs personalizados e monitoramento em tempo de execução, consulte o Guia de Administração do NGINX Plus.
Algumas considerações finais
Ao elaborar um experimento ou plano de teste A/B, certifique-se de que a forma como você distribui as solicitações entre as versões do seu aplicativo não predetermine os resultados. Se quiser um experimento totalmente aleatório, usar o método split_clients e aplicar hash a uma combinação de várias variáveis proporciona os melhores resultados. Por exemplo, gerar um token experimental exclusivo com base na combinação do cookie e do ID do usuário de uma solicitação proporciona um padrão de teste mais aleatório do que aplicar hash apenas ao tipo e à versão do navegador do cliente, pois é bem provável que muitos usuários tenham o mesmo tipo e a mesma versão do navegador e, portanto, sejam todos direcionados para a mesma versão do aplicativo.
Você também precisa levar em conta que muitos usuários pertencem ao chamado grupo misto. Eles acessam aplicações web de vários dispositivos — talvez dos computadores do trabalho e de casa, além de um dispositivo móvel, como um tablet ou smartphone. Esses usuários têm vários endereços IP de cliente. Portanto, se você usar o endereço IP do cliente como base para escolher a versão da aplicação, eles poderão ver ambas as versões, comprometendo os resultados do experimento.
Provavelmente, a solução mais fácil é exigir que os usuários façam login para que você possa rastrear o cookie de sessão deles, como no nosso exemplo do método sticky route. Dessa forma, você pode rastreá-los e sempre direcioná-los para a mesma versão que viram quando acessaram o teste pela primeira vez. Se isso não for possível, às vezes faz sentido dividir os usuários em grupos que provavelmente não mudarão durante o teste, por exemplo, usando geolocalização para mostrar uma versão aos usuários de Los Angeles e outra aos usuários de São Francisco.
Conclusão
O teste A/B é uma maneira eficaz de analisar e acompanhar alterações no seu aplicativo e monitorar seu desempenho, distribuindo diferentes volumes de tráfego entre servidores alternativos. Tanto o NGINX quanto o NGINX Plus oferecem diretivas, parâmetros e variáveis que podem ser usados para criar uma estrutura sólida para testes A/B. Eles também permitem registrar detalhes importantes sobre cada solicitação. Bons testes!
Experimente o NGINX Plus e o método sticky route por conta própria — comece hoje mesmo sua avaliação gratuita de 30 dias ou entre em contato conosco para discutir seus casos de uso.
Sobre o autor

Publicações relacionadas no blog

Como o F5 NGINX One aprimora o gerenciamento e a observabilidade
Descubra como o F5 NGINX One aprimora o gerenciamento e a observabilidade de ambientes híbridos e multicloud ao oferecer um console web centralizado baseado em SaaS, com insights em tempo real, conformidade automatizada e gerenciamento simplificado de configurações.

Do OpenAPI ao NGINX como gateway de API usando uma API declarativa
Descubra como transformar uma definição de esquema OpenAPI em uma configuração NGINX totalmente funcional, operando como um API Gateway com a segurança de um Web Application Firewall e um Portal do Desenvolvedor, usando uma abordagem declarativa de API.

Como gerenciar picos repentinos de tráfego e sobrecarga do servidor
Picos de tráfego e sobrecarga de servidores são preocupações sérias para varejistas online em qualquer época, mas especialmente durante a “Cyber Season” de compras de fim de ano. Detalhamos como você pode aproveitar as soluções de entrega de aplicações de nível de produção da NGINX para se preparar para aumentos repentinos de tráfego.

Distribuição segura de chaves privadas SSL com o NGINX
Descrevemos três maneiras progressivamente mais seguras de proteger chaves privadas SSL ao configurar o NGINX para processar tráfego HTTPS: permitir acesso de leitura apenas ao usuário root, criptografar as chaves com senhas armazenadas separadamente e distribuir as senhas a partir de um repositório central.
Agrupando solicitações de API em lotes com NGINX Plus e o módulo JavaScript do NGINX
O módulo JavaScript do NGINX lançado com o NGINX Plus R15 oferece suporte a subsolicitações, possibilitando uma classe totalmente nova de casos de uso para o NGINX Plus. Este exemplo completo também usa a API do NGINX Plus e os módulos de chave-valor para converter solicitações de API dos clientes em solicitações separadas para as APIs dos microsserviços que compõem um aplicativo.
Como superar o esgotamento de portas efêmeras no NGINX e no NGINX Plus
Confira dicas para evitar o esgotamento de portas efêmeras com ajustes no kernel do Linux, conexões keepalive e conexões vinculadas dinamicamente a endereços IP locais.