Escalado: más carga, más máquinas, más problemas
Escalado vertical y horizontal, por qué los servicios sin estado se multiplican con facilidad y el estado no, el retraso de réplica y por qué la clave de partición se elige una sola vez.
En architecture:1 construiste un sistema con un cliente, un servidor, un contrato de API, una caché y un monolito. Funcionaba. Esta lección trata del día en que funciona para diez mil personas en lugar de diez. Aquí no hay ningún concepto nuevo: aquí está lo que cuestan los conceptos de architecture:1 y os:2 cuando te los tomas en serio.
Hay exactamente dos direcciones para crecer. Vertical significa una máquina más grande: más núcleos, más RAM, discos más rápidos. Horizontal significa más máquinas del mismo tamaño. Lo vertical está injustamente infravalorado. No exige cambiar código, mantiene cierta toda suposición que ya hiciste, y un solo servidor moderno con 64 núcleos y 512 GB de RAM atiende una cantidad de tráfico asombrosa. Un blog, una tienda pequeña, una herramienta interna y la mayoría de productos B2B nunca necesitan otra cosa. El problema de lo vertical es que es una escalera con un último peldaño: en algún punto la máquina más grande que puedes alquilar no basta, el precio por núcleo deja de ser lineal y empieza a duplicarse, y sustituir esa única máquina es una ventana de mantenimiento y no una actualización gradual.
VERTICAL HORIZONTAL
1 x 32 cores 32 x 1 core
limit: silicon limit: coordination
cost: superlinear cost: linear plus a tax
failure: one box failure: 1 of 32 lost
restart: minutes of downtime restart: drain and replace
ceiling: ~ a few hundred GB ceiling: none in practiceAmbas direcciones hacen crecer el mismo sistema. Fallan de forma distinta, y esa diferencia suele ser la que decide.Aquí está la parte que lo decide todo, y es la misma idea del HTTP sin estado de architecture:1, vista ahora desde el otro lado. Un servicio sin estado no guarda nada entre peticiones: cada una llega, recibe respuesta y el proceso la olvida. Ese servicio escala cambiando un número. Ejecuta tres copias o treinta, y cualquier copia responderá a cualquier petición. Un servicio con estado recuerda algo que pertenece a un usuario concreto, y esa memoria está fijada a una máquina. Ahora añadir una máquina no ayuda por sí solo: además hay que decidir dónde vive el estado, cómo se entera de él la máquina nueva y qué le pasa cuando muere la máquina que lo guarda. Esa es toda la dificultad del escalado en una frase: las computadoras se multiplican con facilidad, la memoria de una conversación no.
El ejemplo clásico es el carrito de la compra. Si el carrito vive en la memoria del proceso del servidor, el usuario tiene que seguir dando al mismo proceso o el carrito se vacía. El arreglo es sacar el estado del proceso hacia algo compartido: una fila de base de datos, una clave de Redis o una cookie firmada que lleva el cliente. En cuanto el carrito vive en almacenamiento compartido, el servicio que tiene delante vuelve a no tener estado y se multiplica como cualquier otro. Fíjate en la forma de ese movimiento: no hiciste desaparecer el estado, lo trasladaste al único sitio al que se le permite tener estado. Para eso está la base de datos, y por eso se convierte en el siguiente cuello de botella.
STATELESS SERVICE STATEFUL SERVICE
any replica answers only the owner answers
add a pod: instant add a node: rebalance keys
kill a pod: no loss kill a node: lose its shard
scale by count scale by partition
cart lives in a shared store, not in the processEl mismo servicio, después de sacar su estado del proceso hacia un almacén compartido.Cuando tienes varias réplicas idénticas, algo tiene que elegir cuál recibe cada petición. Ese algo es un balanceador de carga, y lo interesante no es el algoritmo sino sobre qué se balancea. Round robin manda la petición uno a la réplica uno, la dos a la dos, y así: sencillo y correcto cuando todas las peticiones cuestan más o menos lo mismo. Least connections manda la siguiente petición a la réplica que tiene menos en vuelo: mejor cuando las peticiones varían mucho en coste, porque una petición lenta no seguirá atrayendo tráfico hacia una réplica ya ocupada. El hashing toma un valor de la petición y lo asigna a una réplica, y es el único de los tres que te da adherencia.
Un balanceador es tan útil como su idea de qué réplicas están sanas. La comprobación superficial es un connect TCP: si el puerto acepta, la réplica se considera viva. Eso no vale casi nada, porque un proceso puede aceptar conexiones mientras su pool de base de datos está agotado y cada petición acaba en timeout. La comprobación útil es un endpoint real que verifica las dependencias que la réplica necesita para trabajar y falla cuando no están. Aun así, cuidado con el orden: una comprobación que falla con demasiada facilidad saca una réplica sana durante un parpadeo breve de la base de datos, y si falla en todas a la vez has convertido una ralentización en una caída total.
Hay una distinción que reorganiza todo el problema: las lecturas y las escrituras no escalan igual, y no tienen por qué escalar juntas. La mayoría de los sistemas leen mucho más de lo que escriben. Un sitio de noticias puede servir cien mil visitas por cada artículo publicado; una tienda puede mostrar un producto diez mil veces por cada cambio de existencias. Además las lecturas son paralelas por naturaleza: diez usuarios distintos leyendo la misma fila no tienen motivo para entrar en conflicto. Las escrituras son lo contrario, porque cambian estado compartido y algo tiene que arbitrar el orden. Así que el primer movimiento habitual es separarlas: un primary que acepta escrituras y varias réplicas que lo copian y sirven lecturas. Fíjate en que esto es un intercambio y no una comida gratis. Acabas de comprar capacidad de lectura, y el precio es que una lectura puede devolver ahora un valor ligeramente desactualizado.
-- writes go to the primary
INSERT INTO orders (id, user_id, total)
VALUES (9001, 42, 12.90);
-- reads may go to a replica, up to 200 ms behind
SELECT id, total
FROM orders
WHERE user_id = 42
ORDER BY id DESC
LIMIT 20;
-- one connection per role, chosen explicitly
-- write pool -> primary.internal:5432
-- read pool -> replica.internal:5432Separar lecturas de escrituras son unas líneas de configuración y una nueva clase de fallo.El retraso de réplica es el tiempo entre que una escritura llega al primary y se hace visible en una réplica. Con poca carga son una o dos milésimas y nadie lo nota. Con mucha carga crece, porque el primary está ocupado sirviendo escrituras y la réplica tiene que aplicarlas una a una y en orden. Números que conviene llevar encima: una réplica de streaming sana de PostgreSQL está por debajo de unos 10 ms; una ocupada puede llegar a varios cientos de milisegundos en un pico de escrituras; una réplica que se ha quedado atrás con un lote grande o un cambio de esquema puede ir minutos por detrás y aun así reportarse como viva. El retraso no es un fallo que se arregla, es una propiedad de copiar datos por la red a una segunda máquina que también está trabajando.
El fallo que crea el retraso se llama leer tus propias escrituras, y es la forma más común en que una réplica de lectura genera un ticket de soporte. Un usuario guarda un cambio, la escritura va al primary y tiene éxito, la página recarga, la lectura va a una réplica que no se ha puesto al día, y el usuario ve sus propios datos antiguos. Desde su lado el guardado simplemente no funcionó, así que lo hace otra vez. El arreglo honesto es darse cuenta de que el usuario no necesita los datos más recientes en general, necesita que sus propias escrituras le sean visibles a él mismo. Así que encamina las lecturas al primary durante una ventana corta tras la escritura de ese usuario: unos segundos, o hasta que la réplica informe de que ha superado la posición de esa escritura. Fíjate en lo que cuesta: renuncias a algo de capacidad de lectura, para el subconjunto de lecturas que siguen a una escritura del mismo usuario. Ese subconjunto suele ser pequeño, y el intercambio merece la pena.
La replicación copia todo el conjunto de datos en cada réplica, así que escala las lecturas. Para las escrituras no hace nada, porque cada réplica sigue teniendo que aplicar cada escritura. Cuando el cuello de botella son las escrituras, solo queda un movimiento: dejar de poner todos los datos en un sitio y partir los datos mismos en trozos, llamados shards, cada uno en una máquina distinta, y cada uno atendiendo las escrituras de su propio trozo. Ahora una escritura toca un shard, y diez shards absorben diez veces más tráfico de escritura. Este es el punto en que una sola base de datos lógica se convierte en una flota por la que tú mismo tienes que enrutar, y en que la aplicación empieza a contener código que responde a la pregunta de qué shard guarda esta fila.
KEY DISTRIBUTION HOT SPOT CROSS-SHARD QUERY
user_id even none joins on user_id only
created_at even over time today only every range scan
country skewed one big country any global report
auto id even at write none no locality at allCuatro claves de partición candidatas para una tabla de pedidos y el precio que cobra cada una.La clave de partición es la decisión que es casi irreversible, y merece la pena entender por qué antes de elegir una. Cada consulta de la aplicación tiene ahora que nombrar la clave de partición o enviarse a todos los shards y fusionar sus resultados, lo cual es drásticamente más lento y pronto se convierte en el coste dominante. Así que la clave de partición decide en silencio qué consultas seguirán siendo rápidas los próximos cinco años. Elige user id para una tabla de pedidos y cada consulta por usuario es una búsqueda en un solo shard, mientras que un informe de todos los pedidos tiene que dispersarse y recogerse. Elige created_at y cada informe por mes es rápido, pero todas las escrituras de hoy caen en un shard, justo el que ya está más caliente. Cambiar la clave después significa leer cada fila de cada shard y reescribirla en otra disposición, con el sistema en vivo y sirviendo tráfico todo el tiempo.
Práctica
Un equipo mantiene un primary de PostgreSQL y tres réplicas de lectura. Los usuarios informan de que tras editar su perfil recargan la página y ven los valores antiguos, y un segundo después todo es correcto. ¿Qué está pasando en realidad?
Pistas
- La escritura tuvo éxito; pregunta qué servidor respondió a la recarga.
- Compara el estado de las copias, no la velocidad de la red.
- La cuestión es quién lee, no hacer que las copias vayan más rápido.
Empareja cada mecanismo con el problema que resuelve de verdad y el coste que cobra.
Pistas
- Uno copia todo el conjunto de datos; el otro lo divide.
- Una caché cambia lo que lees, no dónde está la verdad.
- Solo uno de los cuatro te cuesta una decisión permanente sobre la forma de las consultas.
Una tienda tiene 8 000 usuarios diarios, una base de datos y ninguna caché. La carga punta es de 40 peticiones por segundo. Ordena las respuestas tal como las intentaría un ingeniero cuidadoso.
- 1Poner una caché delante de las lecturas que se repiten
- 2Añadir réplicas de lectura cuando sea justo la lectura lo que ya no cabe
- 3Añadir los índices que faltan y arreglar la consulta más lenta
- 4Particionar los datos solo si el cuello de botella siguen siendo las escrituras y no las lecturas
- 5Pasar a una máquina más grande si la medición sigue diciendo que la base está llena
- 6Medir en qué gastan el tiempo realmente esas 40 peticiones por segundo
Pistas
- Empieza por el paso que no cambia nada estructuralmente.
- Mide antes de moverte, y divide los datos solo cuando copiar deje de ayudar.
- Las réplicas y los shards son los dos últimos peldaños, no los primeros.
Preguntas sobre esta lección
¿Es gratis?
Sí. El primer nivel de cada materia es gratis y no necesita cuenta. Los niveles más profundos y los grados superiores son de pago: eso es lo que mantiene vivo el proyecto.
¿Para qué edad es?
Está escrito para adolescentes curiosos a partir de unos 12 años y para adultos que empiezan de cero. No se supone experiencia previa en programación; las lecciones de redes y física no necesitan matemáticas más allá del colegio.
¿Necesito saber programar ya?
No. Las rutas de lógica, física y redes empiezan desde cero. El código lo escribes en la sección de lenguajes, que también arranca desde el principio.
¿Hay que instalar algo?
No. Todo funciona en el navegador: teoría, práctica y las simulaciones interactivas. No hay nada que descargar ni que configurar.
¿Cuánto se tarda en una materia?
Una lección dura entre 10 y 15 minutos. Un grado junior son unas 12 a 20 lecciones, así que son unas semanas de práctica constante, no una tarde.
¿Hay que seguir el orden?
Dentro de un grado los niveles se abren uno tras otro, porque cada uno se apoya en el anterior. Entre materias eres libre: empieza por lógica, física o redes.