Quando o ensaio aprova mil requisições por segundo e produção cai em novecentas, a tentação é concluir que o gerador estava errado sobre o número. Em geral não estava: o número é o mesmo, e o que difere é o que acompanha o número. Seis diferenças respondem pela maior parte dos casos, e todas elas são verificáveis antes do próximo ensaio.
- Distribuição de chegada. O gerador emite uma requisição a cada milissegundo com régua; produção chega em processo de Poisson, onde intervalos curtos se agrupam. Com a mesma média de mil por segundo, o pico instantâneo de um segundo qualquer em Poisson passa de mil e cem com frequência, e é o pico que satura a fila, não a média.
- Cardinalidade dos dados. O ensaio repete cem identificadores de cliente e aquece o cache a ponto de a taxa de acerto chegar a noventa e nove por cento. Produção tem duzentos mil clientes distintos e a taxa de acerto real é de sessenta por cento, o que multiplica por quatro as consultas que chegam ao banco com o mesmo número de requisições na borda.
- Distribuição do trabalho por requisição. O ensaio usa o mesmo corpo de requisição sempre, quase sempre o caso pequeno. Produção tem uma cauda de requisições que custam trinta vezes mais, e é essa cauda que ocupa os trabalhadores e forma a fila para todo o resto.
- Estado acumulado. O ensaio roda em base recém-restaurada, com índices compactos, estatísticas recentes e nenhuma linha morta. Produção tem seis meses de crescimento, e o mesmo plano de consulta escolhido no ensaio pode nem ser o plano escolhido lá.
- Concorrência com o resto do mundo. Durante o ensaio o serviço tem o banco só para si. Em produção ele divide com o processo de relatório noturno, com a replicação, com o trabalho em lote das oito da manhã e com o backup, e a capacidade disponível é o que sobra, não o total.
- Ausência dos vizinhos degradados. O ensaio usa dependências saudáveis e locais. Produção tem um provedor externo que oscila, um serviço interno que reinicia, uma resolução de nome que ocasionalmente demora, e cada um deles transforma capacidade em fila.
Das seis, a primeira é a que se corrige com menos esforço e tem o maior retorno imediato, porque é uma mudança de três linhas no gerador. Emitir com intervalo constante é um erro silencioso: ele reduz a variância da chegada a zero e, com isso, remove o principal mecanismo de formação de fila. Trocar o intervalo constante por um intervalo exponencial, que é o que gera um processo de Poisson, mantém a mesma taxa média e devolve ao ensaio o agrupamento que produção tem.
// Gerador de laco aberto com chegada de Poisson e correcao de
// omissao coordenada. O ponto central e o campo previsto: a medicao
// comeca no instante agendado, nao no instante do envio.
/**
* @param {object} opcoes
* @param {string} opcoes.url endpoint sob teste
* @param {number} opcoes.taxaPorSeg taxa media de chegada desejada
* @param {number} opcoes.duracaoSeg duracao total do ensaio
* @param {number} opcoes.timeoutMs timeout por requisicao
*/
export async function ensaioLacoAberto({ url, taxaPorSeg, duracaoSeg, timeoutMs = 2000 }) {
const medicoes = [];
const emVoo = new Set();
const inicio = performance.now();
const fim = inicio + duracaoSeg * 1000;
// Intervalo exponencial: reproduz chegada de Poisson, onde a media e
// 1/taxa mas os intervalos se agrupam. Intervalo constante remove a
// variancia da chegada e, com ela, a formacao de fila que se quer medir.
const proximoIntervaloMs = () => (-Math.log(1 - Math.random()) / taxaPorSeg) * 1000;
let previsto = inicio;
while (previsto < fim) {
previsto += proximoIntervaloMs();
const agendadoPara = previsto;
const esperaMs = agendadoPara - performance.now();
// Espera apenas quando o gerador esta adiantado. Quando esta atrasado
// o valor e negativo e a requisicao sai imediatamente, ja devendo
// tempo: esse debito e a omissao coordenada tornada visivel.
if (esperaMs > 0) {
await new Promise((resolve) => setTimeout(resolve, esperaMs));
}
const promessa = medirUma({ url, agendadoPara, timeoutMs })
.then((medicao) => medicoes.push(medicao))
.finally(() => emVoo.delete(promessa));
emVoo.add(promessa);
}
await Promise.all(emVoo);
return resumir(medicoes, duracaoSeg);
}
async function medirUma({ url, agendadoPara, timeoutMs }) {
const enviadoEm = performance.now();
const controlador = new AbortController();
const alarme = setTimeout(() => controlador.abort(), timeoutMs);
try {
const resposta = await fetch(url, { signal: controlador.signal });
const concluidoEm = performance.now();
return {
// Servico: o que o painel do servidor enxerga.
servicoMs: concluidoEm - enviadoEm,
// Resposta: o que o usuario sente, incluindo a espera na fila do
// proprio gerador. A diferenca entre os dois e a omissao coordenada.
respostaMs: concluidoEm - agendadoPara,
status: resposta.status,
ok: resposta.ok,
};
} catch {
const concluidoEm = performance.now();
return {
servicoMs: concluidoEm - enviadoEm,
respostaMs: concluidoEm - agendadoPara,
status: 0,
ok: false,
};
} finally {
clearTimeout(alarme);
}
}
function percentil(valores, p) {
if (!valores.length) return 0;
const ordenado = [...valores].sort((a, b) => a - b);
const indice = Math.min(ordenado.length - 1, Math.ceil((p / 100) * ordenado.length) - 1);
return ordenado[indice];
}
function resumir(medicoes, duracaoSeg) {
const servico = medicoes.map((m) => m.servicoMs);
const resposta = medicoes.map((m) => m.respostaMs);
const erros = medicoes.filter((m) => !m.ok).length;
return {
amostras: medicoes.length,
taxaEfetiva: medicoes.length / duracaoSeg,
taxaErro: erros / Math.max(1, medicoes.length),
servicoP99: percentil(servico, 99),
respostaP99: percentil(resposta, 99),
// A razao abaixo e o indicador mais importante do relatorio inteiro.
// Perto de 1 o gerador acompanhou o cronograma. Acima de 2 existe
// fila no gerador, e qualquer numero de latencia do servidor esta
// subestimando o que o usuario sofre por esse mesmo fator.
fatorDeOmissao: percentil(resposta, 99) / Math.max(1, percentil(servico, 99)),
};
}
O campo mais valioso do resumo é o último. A razão entre o percentil noventa e nove da resposta e o percentil noventa e nove do serviço responde, em um único número, se o ensaio pode ser levado a sério. Um valor próximo de um significa que o gerador conseguiu manter o cronograma e que o número de latência do servidor é confiável. Um valor de cinco significa que o gerador acumulou fila, que o servidor já estava saturado durante boa parte do ensaio, e que o percentil bonito do painel do servidor está subestimando a experiência real por um fator de cinco.