7 min di letturaIngegneria

Budget di latenza dei redirect: come Elido punta a p95 <15ms

L'edge-redirect reale dietro un target p95 di 15ms su cache hit: LRU, cache condivisa, origin gRPC e clic asincroni, e cosa non abbiamo misurato.

Marius Voß
DevRel · edge infra

Un redirect blocca il visitatore. Clicca un link corto, il browser aspetta, e nient'altro si carica finché non torna il 302. Per questo services/edge-redirect ha un budget di latenza anziché una sensazione di "abbastanza veloce": p50 5ms e p95 15ms su cache hit.

Leggilo come un target, non come un risultato. Il budget è scritto nel README del servizio e nel runbook di reperibilità, e la nostra soglia di accettazione dei load test è costruita su di esso. Non abbiamo un benchmark committato né un file di risultati di load test che dimostri un percentile per regione, quindi questo articolo non ne stampa uno. Descrive, dal codice, cosa fa l'hot path e, altrettanto utile, cosa si rifiuta di fare.

In sintesi

  • L'edge è un servizio Go su una libreria HTTP Go a bassa allocazione, in esecuzione oggi su un singolo host di produzione dietro un reverse proxy, con un edge multi-regione pianificato.
  • La ricerca attraversa una LRU in-process (100.000 voci, TTL di 60 secondi), poi una cache condivisa in memoria (TTL di 5 minuti), poi api-core via gRPC con un timeout di 1,5 secondi.
  • L'evento del clic viene prodotto da una goroutine separata. Il visitatore non lo attende mai.
  • Budget: p50 5ms, p95 15ms su cache hit. Sono target, e i percentili misurati qui non sono pubblicati.

Perché esiste il budget

Il tempo del redirect si somma a tutto ciò che viene dopo. Se un link di campagna manda il traffico a una landing page, il redirect fa parte del time to first byte di quella pagina. Un budget impone una decisione ogni volta che qualcuno vuole aggiungere lavoro a questo percorso: ci sta, oppure passa a un worker.

I numeri in services/edge-redirect/README.md:

PercentileCache hitCache miss
p505ms40ms
p9515ms100ms
p9930ms200ms

Sono un target di progetto. Se ti servono numeri misurati per una revisione del fornitore, esegui lo scenario k6 descritto alla fine di questo articolo sui tuoi link e conserva l'output.

Il percorso della richiesta, passo per passo

internal/handler/redirect.go documenta il flusso in cima al file. In ordine:

  1. Converti l'host in minuscolo e prendi lo slug dal percorso.
  2. resolver.Resolve attraversa L1, poi L2, poi l'origin.
  3. Cortocircuito sullo stato: un workspace in pausa, un link scaduto, un link protetto da password (il visitatore va alla pagina di sblocco e non viene emesso alcun clic), un link che l'url-scanner ha marcato blocked (451) e un host di destinazione nella blocklist dell'admin.
  4. Emetti l'evento del clic senza attenderlo.
  5. Rispondi con 302 a meno che il link non indichi 301 o 307.

Tutto nel passo 3 è un campo del record del link in cache o una ricerca in un insieme in memoria. Il matcher della blocklist, per esempio, viene sincronizzato da api-core in background, quindi controllarlo costa un paio di ricerche in una mappa, non una chiamata al database.

Le anteprime dei link (uno slug che termina con +), le richieste di agenti AI che chiedono JSON e i link con cifratura end-to-end prendono rami diversi prima del redirect. Nessuno di essi tocca Postgres.

Hot path contro cold path: ricerca, controlli di stato e risposta del redirect avvengono prima che il visitatore riceva una risposta, mentre archiviazione dei clic, scansione e analytics girano dopo in worker separati

I tre livelli di ricerca

Ogni livello è piccolo di proposito, e ciascuno è best-effort tranne l'ultimo.

L1, LRU in-process. internal/cache/lru.go incapsula hashicorp/golang-lru/v2/expirable. La dimensione è L1Size: 100_000 voci e il TTL è L1TTL: 60s, entrambi impostati in internal/config/config.go. Il TTL esiste perché un link disabilitato o eliminato non possa essere servito per sempre da un nodo. La chiave è host + "/" + slug, così lo stesso slug su due domini non collide.

L2, cache condivisa. internal/cache conserva il link come JSON, con chiave host e slug, con L2TTL: 5m. È condivisa dalle istanze edge. Un client nil è legale: i metodi della cache restituiscono un miss e l'edge continua a funzionare.

Origin, gRPC verso api-core. internal/origin/origin.go supporta due trasporti. REST (GET /v1/lookup) è il predefinito quando non è configurato altro. gRPC (elido.v1.LookupService.Resolve) è usato una volta impostato GRPC_ORIGIN_ADDR, ed è la scelta di produzione. La chiamata è limitata da OriginTimeout, 1,5 secondi.

Il resolver è abbastanza breve da citarne la 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

Un hit in L2 riempie di nuovo L1. Un miss in entrambi va ad api-core e riempie entrambi. Solo l'origin può dire "non trovato"; se lo fa, il resolver riprova una volta con un host genitore wildcard (abc.example.com diventa *.example.com) per i domini personalizzati wildcard.

Ordine di ricerca dell'edge redirect: LRU in-process con 100.000 voci e TTL di 60 secondi, poi una cache condivisa in memoria con TTL di 5 minuti, poi api-core via gRPC con timeout di 1,5 secondi, ogni hit che riempie di nuovo i livelli superiori

Cosa questo design non ha

Una versione precedente di questo articolo descriveva cose che il codice non fa. Per essere espliciti:

  • Non esiste un gruppo singleflight. Due miss simultanei per lo stesso slug freddo raggiungono entrambi l'origin.
  • Non c'è alcuna sottoscrizione pub/sub sull'edge. Quando un link cambia, api-core elimina la chiave L2 (il purger in services/api-core/internal/linkmod/adapters.go). L1 non riceve push, quindi un link modificato può essere servito dalla memoria di un nodo per fino a 60 secondi.
  • L1 è una LRU limitata dal TTL, non una cache con controllo di ammissione, ed è dimensionata per numero di voci, non per byte.
  • Non esiste un endpoint cache-status, nessun header di risposta per livello e nessun istogramma di latenza esportato dall'edge.

La finestra di 60 secondi è il motivo per cui un link modificabile dovrebbe usare un redirect temporaneo, e per cui 301 vs 302 conta per il lato browser dello stesso problema. Il design della cache è trattato più a fondo in strategia di cache per i redirect di URL.

L'unica metrica Prometheus che possiamo indicare nel codice dell'edge è elido_edge_redirect_total con le label fleet e outcome. Il runbook cita altri nomi di metriche (edge_cache_*, edge_origin_grpc_duration) che non siamo riusciti a trovare registrati nel codice attuale, quindi non le documentiamo.

Cosa resta fuori dall'hot path

Archiviazione dei clic. click.Producer.Emit serializza l'evento e lo passa a un producer franz-go, e il gestore lo chiama come go h.clicks.Emit(...). Il callback di produzione registra solo un errore. Se il bus di eventi è lento, il visitatore non se ne accorge; il costo è che un clic può andare perso, che è il normale compromesso del fire-and-forget. Scrivere i clic nell'archivio analytics è compito di click-ingester, che alimenta l'API analytics dei clic.

Sfide ai bot. Non c'è alcuna sfida JavaScript sul redirect. Il rilevamento dei bot è un elenco di sottostringhe di user-agent che decide se un clic viene contato, quindi crawler e fetcher di anteprime dei link ricevono comunque il loro redirect ma non gonfiano gli analytics. È un filtro grossolano per progetto.

Scansione. url-scanner pubblica un verdetto per link al momento della creazione e a intervalli pianificati. L'edge legge soltanto il verdetto memorizzato (clean, suspect, blocked), e solo blocked ferma il redirect.

Valutazione delle regole. Gli smart link viaggiano con il record del link, quindi le regole per paese, dispositivo e simili girano sulla copia in cache, con la ricerca GeoIP eseguita in-process da un database locale. Un cache hit con regole non richiede una chiamata di rete ad api-core.

Le impostazioni del server sono modeste. cmd/server/main.go configura il server con un timeout di lettura e scrittura di 2 secondi, un timeout di inattività di 30 secondi e un limite di 64 KB sul corpo della richiesta. L'abbiamo scelto al posto di net/http per una minore allocazione per richiesta, come nota il README del servizio. Non abbiamo pubblicato un benchmark diretto per il nostro handler, quindi è una ragione di progetto, non un risultato misurato.

Come misurarlo tu stesso

Il repo ha un runbook di load testing (docs/runbooks/19-load-testing.md) basato su k6. La sua soglia di accettazione pre-lancio per l'hot path è 1,5k req/s per 5 minuti a 15ms p95 con zero 5xx, e 5k req/s per 1 minuto a 30ms p95 con meno dello 0,1% di 5xx. Sono soglie, non risultati.

Un test da un generatore di carico vicino misura il servizio. Un test da un host lontano misura la tua rete. Per la latenza lato visitatore, sonda dall'esterno con un monitor sintetico, come in monitorare i redirect con Sentry e Datadog.

Onestamente, non citerei ancora 15ms a un cliente come misurazione. Lacune che chiuderemmo prima di stampare una tabella per regione: un istogramma di latenza sull'edge, singleflight sui miss a freddo, invalidazione push di L1 e un benchmark committato dietro la cifra dei 15ms.

Marius Voß è DevRel e edge infra in Elido. Fonti: services/edge-redirect, services/api-core/internal/linkmod/adapters.go e docs/runbooks/19-load-testing.md; non si fanno affermazioni di fornitori esterni.

Correlati sul blog

Domande frequenti

15ms p95 è un numero misurato?

No. 15ms p95 (con p50 a 5ms) su una cache hit è il budget di latenza del servizio edge-redirect, ed è il parametro su cui sono scritti i nostri alert e la soglia di accettazione dei load test. Questo articolo non pubblica percentili misurati per regione perché non abbiamo un benchmark o un file di risultati committato da citare.

Come risolve uno slug un redirect di Elido?

L'edge guarda prima in una LRU in-process, poi in una cache condivisa in memoria, e solo dopo interroga api-core via gRPC. Ogni livello di cache è best-effort, quindi solo l'origin può restituire un not-found autorevole. Un hit in un livello inferiore viene copiato nei livelli superiori.

Cosa resta fuori dal percorso del redirect?

Archiviazione dei clic, analytics e i controlli dell'url-scanner. L'edge emette l'evento del clic sul bus di eventi in una goroutine separata e risponde al visitatore senza attenderlo. Il filtro dei bot è un controllo sull'user-agent che decide se un clic viene contato, non una sfida che il visitatore deve superare.

Perché Elido usa 302 per impostazione predefinita?

Un 302 dice ai browser di non trattare la destinazione come permanente, quindi un link modificato ha effetto alla visita successiva. Un link può essere impostato singolarmente su 301 o 307. Il compromesso è trattato nel nostro articolo su 301 vs 302.

Dove si trovano i POP edge?

Oggi la produzione è un singolo host che esegue il servizio edge dietro un reverse proxy. Un edge multi-regione (Europa, Nord America e Sud-Est asiatico, ciascuno con lo stesso binario edge) è pianificato, non attivo. I database principali e il bus di eventi restano nell'UE, e in quel design solo i dati della cache dei redirect starebbero accanto ai nodi edge, come descrive l'ADR-0014.

Un link modificato può essere servito in versione obsoleta?

Brevemente. Quando un link cambia, api-core elimina la sua voce nella cache condivisa, ma la copia in-process di un nodo edge vive finché non scade il suo TTL di 60 secondi. Quindi una modifica della destinazione può impiegare fino a un minuto per raggiungere ogni nodo, ed è uno dei motivi per cui i redirect temporanei si adattano ai link modificabili.

Prova Elido

Incolla un URL, ottieni un link breve

Senza registrazione. Il link vive 30 giorni. Iscriviti per conservarlo.

Senza registrazione. Crea un account gratuito per 50 link al mese.

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito - senza carta di credito.

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

Continua a leggere