8 min de lecturaIngeniería

Presupuesto de latencia: cómo Elido apunta a un p95 bajo 15 ms

La arquitectura real del edge-redirect tras un objetivo de p95 de 15 ms con caché: LRU, caché L2, origen gRPC y clics asíncronos, y lo que no hemos medido.

Marius Voß
DevRel · edge infra

Una redirección bloquea al visitante. Hace clic en un enlace corto, el navegador espera y no se carga nada más hasta que vuelve el 302. Por eso services/edge-redirect tiene un presupuesto de latencia en lugar de una sensación de "suficientemente rápido": p50 de 5 ms y p95 de 15 ms con acierto de caché.

Léelo como un objetivo, no como un resultado. El presupuesto está escrito en el README del servicio y en el runbook de guardia, y nuestro umbral de aceptación de las pruebas de carga se construye sobre él. No tenemos un benchmark ni un archivo de resultados de pruebas de carga confirmado que demuestre un percentil por región, así que este artículo no publica ninguno. Describe, a partir del código, lo que hace la ruta crítica y, igual de útil, lo que se niega a hacer.

En resumen

  • El edge es un servicio en Go sobre una librería HTTP de Go de pocas asignaciones, que hoy corre en un único host de producción detrás de un proxy inverso, con un edge multirregión planificado.
  • La búsqueda recorre una LRU en proceso (100.000 entradas, TTL de 60 segundos), luego una caché compartida en memoria (TTL de 5 minutos), luego api-core por gRPC con un timeout de 1,5 segundos.
  • El evento de clic se produce desde una goroutine aparte. El visitante nunca lo espera.
  • Presupuesto: p50 de 5 ms, p95 de 15 ms con acierto de caché. Son objetivos, y aquí no se publican percentiles medidos.

Por qué existe el presupuesto

El tiempo de redirección se suma a todo lo que viene después. Si un enlace de campaña envía tráfico a una página de destino, la redirección forma parte del tiempo hasta el primer byte de esa página. Un presupuesto obliga a decidir cada vez que alguien quiere añadir trabajo a esta ruta: cabe, o se mueve a un worker.

Los números de services/edge-redirect/README.md:

PercentilAcierto de cachéFallo de caché
p505 ms40 ms
p9515 ms100 ms
p9930 ms200 ms

Son un objetivo de diseño. Si necesitas números medidos para una revisión de proveedor, ejecuta el escenario de k6 descrito al final de este artículo contra tus propios enlaces y guarda la salida.

La ruta de la petición, paso a paso

internal/handler/redirect.go documenta el flujo al principio del archivo. En orden:

  1. Pasar el host a minúsculas y tomar el slug de la ruta.
  2. resolver.Resolve recorre L1, luego L2, luego el origen.
  3. Cortocircuito según el estado: un espacio de trabajo en pausa, un enlace caducado, un enlace protegido con contraseña (el visitante va a la página de desbloqueo y no se emite ningún clic), un enlace que el url-scanner marcó como blocked (451) y un host de destino en la lista de bloqueo del administrador.
  4. Disparar el evento de clic sin esperarlo.
  5. Responder con 302, salvo que el enlace indique 301 o 307.

Todo lo del paso 3 es un campo del registro de enlace en caché o una consulta a un conjunto en memoria. El comprobador de la lista de bloqueo, por ejemplo, se sincroniza desde api-core en segundo plano, así que consultarlo cuesta un par de búsquedas en mapas, no una llamada a la base de datos.

Las vistas previas de enlaces (un slug que termina en +), las peticiones de agentes de IA que piden JSON y los enlaces cifrados de extremo a extremo toman ramas distintas antes de la redirección. Ninguna toca Postgres.

Ruta crítica frente a ruta fría: la búsqueda, las comprobaciones de estado y la respuesta de redirección ocurren antes de que el visitante reciba una respuesta, mientras que el almacenamiento de clics, el análisis y la analítica se ejecutan después en workers aparte

Los tres niveles de búsqueda

Cada nivel es pequeño a propósito, y todos son de mejor esfuerzo salvo el último.

L1, LRU en proceso. internal/cache/lru.go envuelve hashicorp/golang-lru/v2/expirable. El tamaño es L1Size: 100_000 entradas y el TTL es L1TTL: 60s, ambos definidos en internal/config/config.go. El TTL existe para que un enlace desactivado o eliminado no pueda servirlo un nodo para siempre. La clave es host + "/" + slug, así que el mismo slug en dos dominios no colisiona.

L2, caché compartida. internal/cache guarda el enlace como JSON, con clave de host y slug, con L2TTL: 5m. Lo comparten las instancias edge. Un cliente nulo es legal: los métodos de caché devuelven un fallo y el edge sigue funcionando.

Origen, gRPC a api-core. internal/origin/origin.go admite dos transportes. REST (GET /v1/lookup) es el predeterminado cuando no hay nada más configurado. gRPC (elido.v1.LookupService.Resolve) se usa una vez definido GRPC_ORIGIN_ADDR, y es la opción de producción. La llamada está limitada por OriginTimeout, 1,5 segundos.

El resolver es lo bastante corto como para citar 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 acierto en L2 rellena L1. Un fallo en ambas va a api-core y rellena las dos. Solo el origen puede decir "no encontrado"; si lo dice, el resolver reintenta una vez con un host padre comodín (abc.example.com pasa a *.example.com) para los dominios personalizados con comodín.

Orden de búsqueda del edge redirect: LRU en proceso con 100.000 entradas y TTL de 60 segundos, luego una caché compartida en memoria con TTL de 5 minutos, luego api-core por gRPC con un timeout de 1,5 segundos, y cada acierto rellena las capas superiores

Lo que este diseño no tiene

Una versión anterior de este artículo describía cosas que el código no hace. Para ser explícitos:

  • No hay un grupo singleflight. Dos fallos simultáneos del mismo slug frío llegan ambos al origen.
  • No hay suscripción pub/sub en el edge. Cuando un enlace cambia, api-core borra la clave de L2 (el purger en services/api-core/internal/linkmod/adapters.go). A L1 no se le empuja nada, así que un enlace editado puede servirse desde la memoria de un nodo hasta 60 segundos.
  • L1 es una LRU acotada por TTL, no una caché con control de admisión, y su tamaño se mide en número de entradas, no en bytes.
  • No hay endpoint cache-status, ni cabecera de respuesta por nivel, ni histograma de latencia exportado por el edge.

La ventana de 60 segundos es la razón por la que un enlace editable debe usar una redirección temporal, y por la que 301 vs 302 importa para el lado del navegador del mismo problema. El diseño de la caché se trata con más profundidad en estrategia de caché para redirecciones de URL.

La única métrica de Prometheus a la que podemos señalar en el código del edge es elido_edge_redirect_total con las etiquetas fleet y outcome. El runbook menciona otros nombres de métricas (edge_cache_*, edge_origin_grpc_duration) que no pudimos encontrar registrados en el código actual, así que no los documentamos.

Lo que se queda fuera de la ruta crítica

Almacenamiento de clics. click.Producer.Emit serializa el evento y se lo entrega a un productor franz-go, y el handler lo llama como go h.clicks.Emit(...). El callback de producción solo registra un error. Si el bus de eventos va lento, el visitante no lo nota; el coste es que un clic puede perderse, la disyuntiva habitual de disparar y olvidar. Escribir los clics en el almacén de analítica es trabajo de click-ingester, que alimenta la API de analítica de clics.

Desafíos para bots. No hay desafío de JavaScript en la redirección. La detección de bots es una lista de subcadenas de user-agent que decide si un clic se cuenta, así que los rastreadores y los descargadores de vistas previas de enlaces siguen recibiendo su redirección pero no inflan la analítica. Es un filtro tosco por diseño.

Análisis. url-scanner publica un veredicto por enlace en el momento de la creación y de forma programada. El edge solo lee el veredicto almacenado (clean, suspect, blocked), y solo blocked detiene la redirección.

Evaluación de reglas. Los enlaces inteligentes viajan con el registro del enlace, así que las reglas de país, dispositivo y similares se ejecutan contra la copia en caché, con la búsqueda GeoIP hecha en proceso desde una base de datos local. Un acierto de caché con reglas no necesita una llamada de red a api-core.

La configuración del servidor es modesta. cmd/server/main.go configura el servidor con un timeout de lectura y escritura de 2 segundos, un timeout de inactividad de 30 segundos y un límite de cuerpo de petición de 64 KB. Lo elegimos frente a net/http por la menor asignación por petición, como indica el README del servicio. No hemos publicado un benchmark comparativo para nuestro handler, así que es una razón de diseño, no un resultado medido.

Cómo medirlo tú mismo

El repositorio tiene un runbook de pruebas de carga (docs/runbooks/19-load-testing.md) basado en k6. Su umbral de aceptación previo al lanzamiento para la ruta crítica es 1,5k req/s durante 5 minutos con p95 de 15 ms y cero 5xx, y 5k req/s durante 1 minuto con p95 de 30 ms y menos del 0,1% de 5xx. Son umbrales, no resultados.

Una prueba desde un generador de carga cercano mide el servicio. Una prueba desde un host lejano mide tu red. Para la latencia del lado del visitante, sondea desde fuera con un monitor sintético, como en monitorizar redirecciones con Sentry y Datadog.

Siendo honesto, todavía no citaría 15 ms a un cliente como una medición. Lagunas que cerraríamos antes de imprimir una tabla por región: un histograma de latencia en el edge, singleflight en los fallos fríos, invalidación por empuje de L1 y un benchmark confirmado detrás de la cifra de 15 ms.

Marius Voß es DevRel e infraestructura edge en Elido. Fuentes: services/edge-redirect, services/api-core/internal/linkmod/adapters.go y docs/runbooks/19-load-testing.md; no se hacen afirmaciones de proveedores externos.

Relacionado en el blog

Preguntas frecuentes

¿Es el p95 de 15 ms un número medido?

No. Un p95 de 15 ms (con p50 en 5 ms) con acierto de caché es el presupuesto de latencia del servicio edge-redirect, y es la vara con la que están escritas nuestras alertas y el umbral de aceptación de las pruebas de carga. Este artículo no publica percentiles medidos por región porque no tenemos un benchmark ni un archivo de resultados confirmado que citar.

¿Cómo resuelve una redirección de Elido un slug?

El edge busca primero en una LRU en proceso, luego en una caché compartida en memoria y solo después consulta a api-core por gRPC. Cada capa de caché es de mejor esfuerzo, así que solo el origen puede devolver un no encontrado con autoridad. Un acierto en una capa inferior se copia hacia las capas superiores.

¿Qué se queda fuera de la ruta de redirección?

El almacenamiento de clics, la analítica y las comprobaciones del url-scanner. El edge emite el evento de clic al bus de eventos en una goroutine aparte y responde al visitante sin esperarlo. El filtrado de bots es una comprobación del user-agent que decide si un clic se cuenta, no un desafío que el visitante deba superar.

¿Por qué usa Elido 302 por defecto?

Un 302 indica a los navegadores que no traten el destino como permanente, así que un enlace editado surte efecto en la siguiente visita. Un enlace se puede configurar individualmente como 301 o 307. La disyuntiva se trata en nuestro artículo sobre 301 vs 302.

¿Dónde están los POP edge?

La producción hoy es un único host que ejecuta el servicio edge detrás de un proxy inverso. Un edge multirregión (Europa, Norteamérica y el Sudeste Asiático, cada una con el mismo binario edge) está planificado, no activo. Las bases de datos principales y el bus de eventos permanecen en la UE, y en ese diseño solo los datos de caché de redirecciones estarían junto a los nodos edge, como describe el ADR-0014.

¿Se puede servir un enlace editado con datos obsoletos?

Brevemente. Cuando un enlace cambia, api-core borra su entrada de la caché compartida, pero la copia en proceso de un nodo edge vive hasta que se agota su TTL de 60 segundos. Así que una edición de destino puede tardar hasta un minuto en llegar a todos los nodos, una de las razones por las que las redirecciones temporales convienen a los enlaces editables.

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Sin registro. Crea una cuenta gratis y obtén 50 enlaces al mes.

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

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

Seguir leyendo