8 min de leituraEngenharia

Orçamento de Latência: Como o Elido Mira p95 Abaixo de 15ms

A arquitetura do edge-redirect por trás da meta de p95 de 15ms em cache hit: LRU, cache compartilhado, origem gRPC, cliques assíncronos e o que falta medir.

Marius Voß
DevRel · edge infra

Um redirecionamento bloqueia o visitante. Ele clica em um link curto, o navegador espera e nada mais carrega até o 302 voltar. É por isso que o services/edge-redirect tem um orçamento de latência em vez de uma sensação de "rápido o bastante": p50 de 5ms e p95 de 15ms em um cache hit.

Leia isso como uma meta, não como um resultado. O orçamento está escrito no README do serviço e no runbook de plantão, e o nosso critério de aceitação de teste de carga é construído sobre ele. Não temos um benchmark nem um arquivo de resultado de teste de carga versionado que comprove um percentil por região, então este post não imprime nenhum. Ele descreve, a partir do código, o que o caminho quente faz e, igualmente útil, o que ele se recusa a fazer.

Resumo

  • O edge é um serviço em Go sobre uma biblioteca HTTP de Go com poucas alocações, rodando hoje em um único host de produção atrás de um proxy reverso, com um edge em várias regiões planejado.
  • A busca percorre um LRU em processo (100.000 entradas, TTL de 60 segundos), depois um cache compartilhado em memória (TTL de 5 minutos), depois o api-core por gRPC com timeout de 1,5 segundo.
  • O evento de clique é produzido por uma goroutine separada. O visitante nunca espera por ele.
  • Orçamento: p50 de 5ms, p95 de 15ms em cache hit. São metas, e os percentis medidos não são publicados aqui.

Por que o orçamento existe

O tempo do redirecionamento se soma a tudo o que vem depois. Se um link de campanha leva o tráfego a uma landing page, o redirecionamento faz parte do tempo até o primeiro byte dessa página. Um orçamento força uma decisão sempre que alguém quer adicionar trabalho a esse caminho: ele cabe, ou vai para um worker.

Os números em services/edge-redirect/README.md:

PercentilCache hitCache miss
p505ms40ms
p9515ms100ms
p9930ms200ms

São uma meta de projeto. Se você precisa de números medidos para uma avaliação de fornecedor, execute o cenário k6 descrito no fim deste post contra os seus próprios links e guarde a saída.

O caminho da requisição, passo a passo

O internal/handler/redirect.go documenta o fluxo no topo do arquivo. Em ordem:

  1. Converter o host para minúsculas e tirar o slug do caminho.
  2. resolver.Resolve percorre o L1, depois o L2, depois a origem.
  3. Interromper por estado: um workspace pausado, um link expirado, um link protegido por senha (o visitante vai para a página de desbloqueio e nenhum clique é emitido), um link que o url-scanner marcou como blocked (451) e um host de destino na lista de bloqueio do admin.
  4. Disparar o evento de clique sem esperar por ele.
  5. Responder com 302, a menos que o link diga 301 ou 307.

Tudo no passo 3 é um campo do registro de link em cache ou uma consulta em um conjunto em memória. O verificador da lista de bloqueio, por exemplo, é sincronizado do api-core em segundo plano, então consultá-lo custa duas ou três buscas em mapa, não uma chamada ao banco de dados.

Prévias de link (um slug terminado em +), requisições de agentes de IA que pedem JSON e links com criptografia de ponta a ponta tomam ramos diferentes antes do redirecionamento. Nenhum deles toca o Postgres.

Caminho quente versus caminho frio: a busca, as verificações de estado e a resposta do redirecionamento acontecem antes de o visitante receber uma resposta, enquanto o armazenamento de cliques, a varredura e as análises rodam depois, em workers separados

As três camadas de busca

Cada camada é pequena de propósito, e cada uma é de melhor esforço, exceto a última.

L1, LRU em processo. O internal/cache/lru.go envolve o hashicorp/golang-lru/v2/expirable. O tamanho é L1Size: 100_000 entradas e o TTL é L1TTL: 60s, ambos definidos em internal/config/config.go. O TTL existe para que um link desativado ou apagado não possa ser servido para sempre por um nó. A chave é host + "/" + slug, então o mesmo slug em dois domínios não colide.

L2, cache compartilhado. O internal/cache guarda o link como JSON, com chave de host e slug, com L2TTL: 5m. Ele é compartilhado pelas instâncias de edge. Um cliente nulo é válido: os métodos de cache devolvem um miss e o edge continua funcionando.

Origem, gRPC para o api-core. O internal/origin/origin.go suporta dois transportes. REST (GET /v1/lookup) é o padrão quando nada mais está configurado. O gRPC (elido.v1.LookupService.Resolve) é usado quando GRPC_ORIGIN_ADDR está definido, e é a escolha de produção. A chamada é limitada por OriginTimeout, de 1,5 segundo.

O resolvedor é curto o bastante para citar a parte importante:

if v, ok := r.l1.Get(key); ok {
    return v, nil
}
if v, ok := r.l2.Get(ctx, key); ok {
    r.l1.Set(key, v)
    return v, nil
}
link, err := r.origin.Resolve(ctx, host, slug)
if err != nil {
    return nil, err
}
r.l1.Set(key, link)
r.l2.Set(ctx, key, link)
return link, nil

Um acerto no L2 reabastece o L1. Um miss nos dois vai ao api-core e reabastece ambos. Só a origem pode dizer "não encontrado"; se disser, o resolvedor tenta mais uma vez com um host pai curinga (abc.example.com vira *.example.com) para domínios personalizados com curinga.

Ordem de busca do edge redirect: LRU em processo com 100.000 entradas e TTL de 60 segundos, depois um cache compartilhado em memória com TTL de 5 minutos, depois api-core por gRPC com timeout de 1,5 segundo, cada acerto reabastecendo as camadas acima

O que este projeto não tem

Uma versão anterior deste post descrevia coisas que o código não faz. Para ser explícito:

  • Não existe um grupo singleflight. Dois misses simultâneos para o mesmo slug frio chegam ambos à origem.
  • Não existe assinatura de pub/sub no edge. Quando um link muda, o api-core apaga a chave do L2 (o purger em services/api-core/internal/linkmod/adapters.go). O L1 não recebe push, então um link editado pode ser servido da memória de um nó por até 60 segundos.
  • O L1 é um LRU limitado por TTL, não um cache com controle de admissão, e é dimensionado por número de entradas, não por bytes.
  • Não existe endpoint cache-status, nem cabeçalho de resposta por camada, nem histograma de latência exportado pelo edge.

A janela de 60 segundos é o motivo para um link editável usar um redirecionamento temporário, e o motivo pelo qual 301 vs 302 importa para o lado do navegador do mesmo problema. O projeto do cache é tratado com mais profundidade em estratégia de cache para redirecionamentos de URL.

A única métrica Prometheus que conseguimos apontar no código do edge é elido_edge_redirect_total com os rótulos fleet e outcome. O runbook menciona outros nomes de métrica (edge_cache_*, edge_origin_grpc_duration) que não encontramos registrados no código atual, então não os documentamos.

O que fica fora do caminho quente

Comece pelo armazenamento de cliques. O click.Producer.Emit serializa o evento e o entrega a um produtor franz-go, e o handler o chama como go h.clicks.Emit(...). O callback de produção apenas registra um erro em log. Se o barramento de eventos estiver lento, o visitante não percebe; o custo é que um clique pode ser perdido, a troca habitual do fire-and-forget. Gravar os cliques no armazenamento de análises é trabalho do click-ingester, que alimenta a API de análises de cliques.

Os desafios para bots também ficam de fora: não há desafio de JavaScript no redirecionamento. A detecção de bots é uma lista de substrings de user-agent que decide se um clique é contado, então crawlers e buscadores de prévia de link ainda recebem o redirecionamento, mas não inflam as análises. É um filtro grosseiro por projeto.

A varredura também é assíncrona. O url-scanner publica um veredito por link na criação e em uma programação. O edge apenas lê o veredito armazenado (clean, suspect, blocked), e só blocked interrompe o redirecionamento.

A avaliação de regras é a que parece arriscada e não é. Os smart links viajam com o registro do link, então regras de país, dispositivo e semelhantes rodam contra a cópia em cache, com a consulta de GeoIP feita em processo a partir de um banco de dados local. Um cache hit com regras não precisa de uma chamada de rede ao api-core.

As configurações do servidor são modestas. O cmd/server/main.go configura o servidor com timeout de leitura e escrita de 2 segundos, timeout de ociosidade de 30 segundos e limite de 64 KB para o corpo da requisição. Nós o escolhemos em vez do net/http por causa da menor alocação por requisição, como observa o README do serviço. Não publicamos um benchmark direto do nosso handler, então esse é um motivo de projeto, não um resultado medido.

Como medir você mesmo

O repositório tem um runbook de teste de carga (docs/runbooks/19-load-testing.md) construído sobre o k6. O critério de aceitação pré-lançamento para o caminho quente é 1,5k req/s por 5 minutos com p95 de 15ms e zero 5xx, e 5k req/s por 1 minuto com p95 de 30ms e menos de 0,1% de 5xx. São limiares, não resultados.

Um teste de um gerador de carga próximo mede o serviço. Um teste de um host distante mede a sua rede. Para a latência do lado do visitante, sonde de fora com um monitor sintético, como em monitorando redirecionamentos com Sentry e Datadog.

Sinceramente, eu ainda não citaria 15ms a um cliente como uma medição. Lacunas que fecharíamos antes de imprimir uma tabela por região: um histograma de latência no edge, singleflight nos misses frios, invalidação por push do L1 e um benchmark versionado por trás do número de 15ms.

Marius Voß é DevRel e edge infra no Elido. Fontes: services/edge-redirect, services/api-core/internal/linkmod/adapters.go e docs/runbooks/19-load-testing.md; nenhuma afirmação de fornecedor externo é feita.

Relacionados no Blog

Perguntas frequentes

15ms de p95 é um número medido?

Não. 15ms de p95 (com p50 em 5ms) em um cache hit é o orçamento de latência do serviço edge-redirect, e é contra ele que estão escritos os nossos alertas e o critério de aceitação do teste de carga. Este post não publica percentis medidos por região porque não temos um benchmark nem um arquivo de resultados versionado para citar.

Como um redirecionamento do Elido resolve um slug?

O edge consulta primeiro um LRU em processo, depois um cache compartilhado em memória, e só então pergunta ao api-core por gRPC. Cada camada de cache é de melhor esforço, então só a origem pode devolver um not-found autoritativo. Um acerto em uma camada inferior é copiado para as camadas acima dela.

O que fica fora do caminho do redirecionamento?

O armazenamento de cliques, as análises e as verificações do url-scanner. O edge emite o evento de clique para o barramento de eventos em uma goroutine separada e responde ao visitante sem esperar por ele. O filtro de bots é uma checagem de user-agent que decide se um clique é contado, não um desafio que o visitante precise passar.

Por que o Elido usa 302 por padrão?

Um 302 diz aos navegadores para não tratar o destino como permanente, então um link editado vale na próxima visita. Um link pode ser configurado individualmente como 301 ou 307. A troca é tratada no nosso post sobre 301 vs 302.

Onde ficam os POPs de edge?

Hoje a produção roda em um único host executando o serviço de edge atrás de um proxy reverso. Um edge em várias regiões (Europa, América do Norte e Sudeste Asiático, cada uma executando o mesmo binário de edge) está planejado, ainda não ativo. Os bancos de dados principais e o barramento de eventos ficam na UE, e, nesse desenho, apenas os dados de cache do redirecionamento ficariam ao lado dos nós de edge, como descreve o ADR-0014.

Um link editado pode ser servido desatualizado?

Por pouco tempo. Quando um link muda, o api-core apaga a entrada dele no cache compartilhado, mas a cópia em processo de um nó de edge vive até o TTL de 60 segundos acabar. Então uma edição de destino pode levar até um minuto para chegar a todos os nós, um dos motivos pelos quais redirecionamentos temporários combinam com links editáveis.

Experimente Elido

Cole uma URL, obtenha um link curto

Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.

Sem registro. Crie uma conta grátis e obtenha 50 links por mês.

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito - sem cartão de crédito.

Tags
url shortener performance
edge redirect latency
multi-region url shortener
redirect cache strategy
go redirect service
redirect latency budget

Continuar lendo