Pídele a cualquiera que diseñe un acortador de URL y la primera respuesta llega en segundos: guardar code → url, buscar el código y redirigir. Esa respuesta es correcta, y también es la parte más pequeña del diseño. Un acortador es un buen ejercicio precisamente porque su núcleo es trivial; todas las decisiones que importan tienen que ver con lo que ocurre a su alrededor: qué ruta tiene que ser rápida, qué trabajo puede esperar y qué fallo puede llegar a la persona que hizo clic.
Este artículo construye una arquitectura en cuatro etapas. Cada etapa parte de un problema que la anterior no puede resolver, añade solo lo que ese problema exige y dice lo que cuesta. Si ya conoces el terreno, salta a la arquitectura final. Si estás aquí para aprender, léelo en orden: el diagrama final solo resulta obvio cuando has visto por qué llegó cada caja.
Los supuestos
Los números deciden la arquitectura, así que estos son los que usa este diseño. Son supuestos del ejercicio, redondeados a propósito, no mediciones de ningún servicio real:
- 50 millones de enlaces nuevos al mes, unas 20 creaciones por segundo de media.
- 100 redirecciones por cada enlace creado: unas 2000 redirecciones por segundo de media, con picos diez veces mayores cuando un enlace se propaga.
- Unos 500 bytes por enlace (la URL larga, el código, un propietario, marcas de tiempo): alrededor de 25 GB de datos nuevos al mes, 300 GB al año.
- Un evento de clic por redirección, que se guarda para analítica.
Ya destacan dos cosas. Las escrituras son mínimas: 20 por segundo no es nada para ninguna base de datos. Y la redirección es el producto: es la petición que espera gente real, miles de veces por segundo, y se interpone entre esas personas y la página que de verdad querían ver. Crear un enlace puede tardar unos cientos de milisegundos sin que nadie lo note; una redirección así de lenta hace que cualquier enlace parezca roto.
El diseño resuelto en Learn parte de un servicio más pequeño, con otros números. La forma de la respuesta es la misma, y de eso se trata: la estimación cambia los tamaños, no el razonamiento.
Etapa 1: que funcione
Empieza por lo más pequeño que sea correcto. Un cliente habla con un servicio sin estado, y el servicio guarda los enlaces en una base de datos relacional. Hay dos operaciones: POST /links guarda una URL larga y devuelve un código, y GET /{code} busca el código y responde con una redirección.
Incluso esta primera versión ejecuta al menos dos instancias del servicio detrás de un balanceador de carga. No es una cuestión de escala (una sola instancia podría atender este tráfico), sino el mínimo para la disponibilidad: con una sola instancia, cada despliegue y cada caída es una interrupción del servicio. El servicio no guarda estado propio, así que cualquier instancia puede responder a cualquier petición, y añadir instancias más adelante es un cambio de configuración, no un rediseño.
Más informaciónDiseño resuelto: un acortador de enlacesBalanceo de carga y escalado horizontalGeneración de ID únicos
El código corto en sí
El modelo de datos es una sola tabla (código, URL larga, propietario, fecha de creación) con el código como clave primaria. La única decisión real es cómo se generan los códigos, y hay dos respuestas razonables.
Códigos aleatorios. Toma siete caracteres de un alfabeto de 62 (base62: 26 letras minúsculas, 26 mayúsculas y 10 dígitos), inserta y reintenta si la restricción de unicidad rechaza la fila. Siete caracteres dan unos 3,5 billones de combinaciones, así que incluso tras un año con este tráfico una colisión es rara y el reintento casi nunca se ejecuta. Además, los códigos aleatorios son prácticamente imposibles de adivinar, y eso importa más de lo que parece: con códigos secuenciales, cualquiera que tenga un enlace puede probar los códigos contiguos, y la gente suele acortar enlaces que nunca pensó publicar.
Códigos basados en un contador. Toma el siguiente número de una secuencia que la base de datos ya ofrece y codifícalo en base62: sin colisiones ni reintentos. El coste es la previsibilidad, porque números consecutivos dan códigos consecutivos. La solución habitual es pasar el número por una mezcla reversible, una permutación con clave, antes de codificarlo: los códigos siguen siendo únicos, pero ya no están en orden.
Este diseño usa el contador mezclado: unicidad por construcción, códigos que nadie puede enumerar y ningún componente nuevo, porque el número sale de la base de datos que ya tenemos, en la misma transacción que la inserción. Los códigos aleatorios serían igual de defendibles. Lo que importa es que la decisión es pequeña, está acotada y es fácil de cambiar más adelante.
Etapa 2: la redirección se convierte en la ruta caliente
Con 2000 redirecciones por segundo, la primera etapa va bien. En el pico (20 000 por segundo mientras un enlace está en todas partes), cada redirección sigue siendo una consulta a la base de datos, y la base de datos tiene que dimensionarse para el peor minuto del mes. Es una forma cara de servir una lectura que casi nunca cambia.
El destino de un enlace se escribe una vez y se lee miles de veces, lo que lo convierte en el caso de manual para una caché. El servicio usa cache-aside: en una redirección, primero busca el código en la caché. Si hay acierto, responde de inmediato; si hay fallo, lee la base de datos, guarda el resultado en la caché con un tiempo de vida (TTL) y responde.
El servicio consulta primero la caché; la base de datos solo responde a los fallos de caché.
Más informaciónCapas de cachéClaves calientes y estampidas de caché
Tres detalles deciden si esta caché ayuda o perjudica:
- La base de datos sigue siendo la fuente de verdad. Crear un enlace escribe en la base de datos y solo responde después del commit; la caché se llena en la primera lectura. Si la caché desapareciera, todos los enlaces seguirían existiendo: las redirecciones simplemente serían más lentas.
- El TTL es una afirmación sobre el cambio. Los enlaces rara vez cambian, así que un TTL largo, de horas o días, es seguro. Cuando se borra un enlace o se edita su destino, el servicio borra también la entrada de la caché, y el TTL limita cuánto tiempo puede sobrevivir un borrado que se haya pasado por alto.
- Los códigos desconocidos también tienen una respuesta en caché. Los escáneres piden códigos aleatorios constantemente. Guardar en caché «no encontrado» durante un minuto los mantiene lejos de la base de datos. Solo se guarda así un «no encontrado» real, nunca un error de la base de datos, y crear un enlace borra cualquier entrada de «no encontrado» para su código.
La caché también cambia la forma en que falla el sistema. Si se cae, las redirecciones pasan a la base de datos (siguen siendo correctas, pero más lentas) y a la base de datos se le pide de repente toda la carga que absorbía la caché. O la base de datos está dimensionada para aguantarlo, o el servicio descarta carga (responde a algunas peticiones con un error de inmediato en lugar de encolarlas) hasta que la caché vuelve. La popularidad añade un segundo riesgo: un enlace viral es una única clave caliente, y si su entrada caduca en el peor momento, una avalancha de peticiones no encuentra la entrada en la caché a la vez y todas van a la base de datos a buscar la misma fila. Ese fallo tiene su propio artículo en este blog, enlazado al final.
Etapa 3: contar clics sin ralentizar las redirecciones
Ahora el producto quiere analítica: cuántas veces se hizo clic en cada enlace, desde dónde y cuándo. La implementación obvia escribe una fila en cada redirección. También es la equivocada, porque pone una escritura, más lenta que la lectura en caché que tiene al lado, en la ruta de latencia de cada clic.
La lección de esta etapa cabe en una frase: redirigir al lector y registrar el clic no necesitan el mismo contrato de latencia. El lector necesita una redirección en milisegundos. La analítica necesita que el clic llegue tarde o temprano, contado correctamente. Así que el servicio publica un pequeño evento de clic en una cola y responde a la redirección sin esperar nada más. Un worker consume la cola y escribe los eventos, por lotes, en un almacén pensado para analítica.
Más informaciónColas de mensajes y workers asíncronosIdempotencia y las ilusiones del exactamente-una-vez
Los supuestos explican por qué los clics tienen su propio almacén. Los enlaces crecen 25 GB al mes. Los clics llegan a 2000 por segundo (unos 170 millones de eventos al día) y, a unos 100 bytes cada uno, suman alrededor de 500 GB al mes, veinte veces lo que ocupan los enlaces. El mayor conjunto de datos de un acortador de URL no son los enlaces, y la base de datos que responde a las redirecciones no debería ser también la que cuenta los clics por país de la semana pasada.
La frontera asíncrona tiene costes, y conviene decirlos con claridad:
- La entrega es al menos una vez. Un worker que se cae después de escribir un lote, pero antes de confirmarlo, recibirá ese lote de nuevo. Cada evento lleva un ID y el lado de analítica ignora los ID que ya ha visto, así que el consumidor es idempotente y un reintento no puede duplicar un recuento.
- Los recuentos van con retraso. Un clic aparece en la analítica segundos después de la redirección, o más si el worker se queda atrás. Para analítica eso está bien, y es una decisión, no un accidente.
- La cola puede fallar. Si la publicación falla, la redirección sigue funcionando: el servicio puede retener localmente unos segundos de eventos y descartar el resto. Perder un puñado de clics durante una caída es el precio que paga este diseño para que un problema de analítica nunca se convierta en un problema de redirección.
301 Moved Permanently permite que los navegadores recuerden la redirección y se salten el acortador en la siguiente visita: es más barato, pero esos clics repetidos nunca se ven, y un destino editado nunca llega a un navegador que ya siguió el anterior. Un 302 Found mantiene cada clic observable y cada edición efectiva. Si la analítica forma parte del producto, 302 encaja mejor; la opción contraria es defendible, siempre que se tome a propósito.Etapa 4: cuando la base de datos es el punto único de fallo
Fíjate en lo que sigue estando solo. El servicio ejecuta varias instancias. La caché puede desaparecer sin perder datos. La cola absorbe un worker lento o averiado. La base de datos es una sola máquina que guarda la única copia de cada enlace: si falla, la creación se detiene y fallan todas las lecturas que no encuentran el código en la caché, y un disco perdido se lleva todo lo escrito desde la última copia de seguridad.
La solución es una réplica en espera que recibe una copia de cada escritura poco después de confirmarse y que puede promoverse para ocupar el lugar del primario. Observa para qué sirve: disponibilidad y durabilidad, no capacidad de lectura. La caché ya le quitó la carga de lectura a la base de datos, así que una réplica añadida «para lecturas» estaría casi siempre ociosa. Una réplica y una caché responden a preguntas distintas, y otro artículo de este blog explica esa diferencia.
Más informaciónReplicación y réplicas de lecturaSharding y particionado
Aquí la replicación es asíncrona, lo que es un compromiso en sí mismo: el primario confirma una escritura sin esperar a la réplica. Si el primario falla, las escrituras de los últimos instantes pueden no llegar nunca a la réplica, así que un enlace creado un segundo antes del fallo podría perderse. La replicación síncrona cierra ese hueco a cambio de escrituras más lentas y de una ruta de creación que depende de dos máquinas. Con 20 creaciones por segundo, cualquiera de las dos es asumible; la elección depende de si «tu enlace nuevo desapareció» es aceptable muy de vez en cuando.
Lo que esta etapa no añade, a propósito, es sharding. Repartir los enlaces entre varias bases de datos resuelve otro problema (más datos o más escrituras de las que puede soportar un solo primario) y, con 300 GB al año y 20 escrituras por segundo, ese problema queda a años vista. Cuando llegue, la clave de partición ya es obvia: toda búsqueda es por código, así que particionar por un hash del código hace que cada redirección toque exactamente una partición. Particionar, en cambio, por rangos del número de secuencia en bruto enviaría cada enlace nuevo a la misma partición, la más reciente; aplicar un hash a la clave es lo que reparte las escrituras.
La arquitectura final, leída a través de sus fallos
Cuatro etapas y nueve cajas, cada una ahí por una razón que dieron las etapas. La forma más útil de leer el diagrama final es por sus fallos, es decir, qué se rompe y quién se da cuenta:
- Muere una instancia del servicio → el balanceador de carga deja de enviarle tráfico. Nadie se da cuenta.
- Falla el balanceador de carga → falla todo. Se dibuja como una sola caja porque suele ser un servicio gestionado y redundante, no una máquina que operes tú; si es una máquina que operas tú, hacen falta dos.
- La caché está caída → las redirecciones se sirven desde la base de datos, más despacio, mientras la base de datos aguante la carga. Nada deja de ser correcto.
- La cola o el worker están caídos → las redirecciones no se ven afectadas; los clics se acumulan en búfer, se retrasan o, en una caída larga, se pierden en parte.
- El almacén de analítica va lento → el worker se queda atrás y los recuentos se retrasan. A las redirecciones les da igual.
- Falla la base de datos primaria → la creación de enlaces se detiene hasta que se promueve la réplica en espera. Los enlaces populares siguen redirigiendo desde la caché; el resto falla hasta que termina la promoción.
Esa lista es el diseño de verdad. Las cajas son el medio; las decisiones son a qué ruta puede llegar cada fallo. La redirección depende de lo mínimo posible (el balanceador de carga, el servicio, la caché y la base de datos solo cuando el código no está en la caché) y todo lo demás se le conecta de forma asíncrona.
Lo que este diseño deja abierto
Algunas decisiones pertenecen al producto y no a la arquitectura, y una revisión de diseño debería nombrarlas en lugar de fingir que están resueltas:
- Abuso. Un acortador oculta adónde lleva un enlace, lo que lo hace atractivo para el phishing y el malware. Los servicios reales revisan los destinos al crear un enlace y limitan el ritmo al que se pueden crear enlaces.
- Alias personalizados. Dejar que la gente elija su propio código lo convierte en una clave única aportada por el usuario, con palabras reservadas y acaparamiento de nombres que resolver.
- Caducidad y borrado. Los enlaces que caducan necesitan un proceso de limpieza y una caché que los olvide a tiempo.
- Regiones. Lectores en varios continentes querrían tener la caché, y quizá la propia redirección, más cerca. El tráfico de este ejercicio todavía no lo necesita.
El código corto ocupó dos párrafos. Todo lo demás (la ruta caliente, la frontera asíncrona, la fuente de verdad, el fallo que cada caja puede causar) es el diseño real, y es la parte que se traslada a cualquier otro sistema que dibujes.