Read in English · Lire en français · Auf Deutsch lesen
Sección 7.4 de 7.8 · Avanzado · 30 min de lectura
Replicación, particionamiento y sharding
Videolección
5 min · narrado en inglés, con subtítulosSolo escuchar (audio)
Leer la transcripción
Tarde o temprano, un servidor de base de datos se queda sin algo: disponibilidad, capacidad o almacenamiento. En esta lección aprenderás las dos formas de repartir los datos entre varias máquinas, y qué efecto tiene cada una en la corrección.
Imagina una biblioteca muy concurrida. Para seguir atendiendo a los lectores si uno de sus edificios se inunda, guarda copias idénticas de su catálogo en varias sucursales. Eso es la replicación. Las copias se actualizan por mensajero, así que una sucursal puede ir un poco atrasada. Cuando la colección es demasiado grande para un solo edificio, la biblioteca la divide, con la mitad del alfabeto aquí y la otra mitad allá. Eso es el particionamiento, a menudo llamado sharding. Dividir bien significa que ninguna sucursal queda desbordada y que los lectores rara vez necesitan visitar todas las sucursales para obtener una respuesta. Los sistemas reales suelen hacer ambas cosas: cada partición se replica.
El esquema más común es la replicación líder-seguidor. Todas las escrituras van a un solo nodo, el líder, que transmite su log de cambios a los seguidores. La decisión clave es cuándo comunica el líder un commit. Con un seguidor síncrono, el commit espera hasta que el seguidor tiene el cambio. Una escritura confirmada sobrevive a la pérdida del líder, pero cada commit paga un viaje de ida y vuelta por la red. Con un seguidor asíncrono, el commit no espera. Es rápido, pero si el líder muere, pueden perderse los últimos cambios, aunque al cliente se le dijo que estaban confirmados.
Los seguidores asíncronos suelen ir milisegundos por detrás, pero con carga el retardo puede crecer hasta segundos o minutos. Los usuarios lo notan. Una clienta guarda una nueva dirección de entrega, y el líder responde OK. Recarga la página. La lectura la atiende un seguidor, y ve su dirección antigua. Parece que el guardado ha fallado. Su cambio solo llega al seguidor tras el retardo. Esta garantía incumplida se llama leer tus propias escrituras. La solución es leer del líder los cambios recientes del propio usuario, o esperar a que el seguidor se ponga al día. Decide para cada consulta qué lecturas pueden estar obsoletas.
Cuando un líder falla, el failover lo sustituye por un seguidor: detectar el fallo, elegir el seguidor más actualizado y redirigir las escrituras. Cada paso puede salir mal. El fallo más peligroso es el split brain: dos nodos creen ser el líder, ambos aceptan escrituras y los datos divergen. La protección necesita un quórum, para que un nodo solo pueda liderar mientras una mayoría esté de acuerdo, y fencing, para que el antiguo líder no pueda seguir escribiendo. El incidente de GitHub de octubre de 2018 muestra lo difícil que es esto. Un fallo de red de cuarenta y tres segundos desencadenó un failover hacia la otra costa. Después, ambas costas tenían escrituras de las que carecía la otra, y el servicio estuvo degradado durante más de un día.
Ahora, el particionamiento. Cada registro va a una partición según su clave de partición, y el objetivo es repartir la carga de forma uniforme. El particionamiento por rango da a cada partición un rango de claves, como meses. Un recorrido de un mes toca una sola partición. Pero si la clave crece con el tiempo, cada pedido nuevo cae en la partición más reciente: un punto caliente. El particionamiento por hash reparte los pedidos nuevos de forma uniforme entre las particiones. El precio es que un recorrido de un mes debe preguntar a todas las particiones. Muchos almacenes combinan ambos. Aplica un hash al identificador del cliente para elegir la partición y ordena por tiempo dentro de ella, de modo que el historial de un cliente sea un único recorrido.
Elijamos una clave de sharding. La plataforma de reparto de comida recibe diez millones de pedidos al día, unas tres mil quinientas escrituras por segundo en el pico. Un único primario bien ajustado puede con eso, así que el throughput por sí solo no obliga a hacer sharding. El almacenamiento, sí. A unos cuarenta gigabytes al día, dos años en línea son veintinueve terabytes, y reconstruir una réplica en un solo nodo llevaría demasiado tiempo. Unos dos terabytes por shard dan dieciséis shards, cada uno con dos seguidores, con mil veinticuatro particiones lógicas por encima, de modo que crecer mueve particiones enteras en lugar de volver a aplicar el hash a las claves. Después, las claves candidatas. El identificador del pedido dispersaría el historial de cada cliente. La hora del pedido sería un punto caliente. El restaurante tiene claves calientes, y algunas ciudades son mucho más grandes que otras. Así que la decisión es aplicar un hash al identificador del cliente y ordenar por la hora del pedido. Las demás consultas reciben rutas independientes, como un modelo de lectura para la cola de cada restaurante. Fíjate: con solo noventa días en línea, no habría hecho falta ningún sharding.
Cuatro errores comunes. Primero, tratar las réplicas como copias de seguridad. Una réplica copia fielmente un borrado erróneo en milisegundos. Segundo, confiar en un failover automático que nunca se ha ensayado. Tercero, hacer sharding demasiado pronto, lo que complica los joins, las transacciones y los cambios de esquema. Prueba primero la indexación, la caché, las réplicas de lectura y el archivado. Y cuarto, hacer sharding por una clave que siempre aumenta, lo que envía cada escritura nueva a una sola partición. Una clave de sharding es muy difícil de cambiar más adelante, así que registra los volúmenes que se supusieron.
La sección completa también trata la replicación sin líder, el hashing consistente y los índices secundarios. A continuación, en la Sección siete punto cinco, veremos CQRS y event sourcing. Nos vemos allí.
Lo que necesitas saber
- La replicación mantiene copias de los mismos datos en varios nodos para lograr disponibilidad, escalado de lecturas y proximidad; el particionamiento (sharding) reparte los datos entre nodos para lograr throughput de escritura y almacenamiento; los sistemas reales suelen combinar ambos.
- En la replicación líder-seguidor todas las escrituras van a un único líder; los seguidores síncronos protegen las escrituras confirmadas a costa de latencia, mientras que los seguidores asíncronos son rápidos, pero pueden perder escrituras recientes en un failover.
- El retardo de replicación rompe las garantías de leer tus propias escrituras, de lecturas monótonas y de lecturas de prefijo consistente; entre las soluciones están leer del líder las escrituras propias recientes, esperar a una posición del log y fijar a cada usuario a una réplica.
- El failover debe detectar el fallo, elegir el nodo más actualizado y aislar (fencing) al antiguo líder; el split brain, en el que dos nodos aceptan escrituras, puede provocar recuperaciones largas y dolorosas, como mostró el incidente de GitHub de 2018.
- La replicación multilíder debe resolver los conflictos de escritura; la replicación sin líder (al estilo Dynamo) usa quórums con R + W > N, reparación en lectura y antientropía, pero los quórums por sí solos no garantizan la linealizabilidad.
- El particionamiento por rangos admite recorridos por rango, pero crea puntos calientes con claves ordenadas por tiempo; el particionamiento por hash reparte la carga, pero dispersa los rangos; el hashing consistente o las particiones lógicas fijas mantienen barato el reequilibrado.
- Las claves calientes, el desequilibrio (skew) y los índices secundarios (locales implica lecturas de dispersión-recolección; globales implica escrituras de índice remotas, normalmente asíncronas) deben diseñarse a partir de las consultas más importantes.
Dos motivos distintos para repartir los datos
Un único servidor de base de datos acaba quedándose sin algo: disponibilidad (es una sola máquina que puede fallar), capacidad de lectura, capacidad de escritura, almacenamiento o proximidad a usuarios de otros continentes. Hay dos formas complementarias de repartir los datos entre varias máquinas, y es importante no confundirlas. La replicación mantiene una copia de los mismos datos en varios nodos, para lograr disponibilidad, escalado de lecturas y menor latencia cerca de los usuarios. El particionamiento, llamado sharding en muchos productos, divide los datos para que cada nodo guarde un subconjunto, y así lograr throughput de escritura y almacenamiento más allá de una sola máquina. Los sistemas reales suelen hacer ambas cosas: cada partición se replica.
Ambas tienen consecuencias serias para la corrección. La replicación plantea la pregunta de qué ve un lector mientras las copias no coinciden, que es el tema de modelos de consistencia, CAP y PACELC. El particionamiento plantea la pregunta de qué ocurre con las consultas, los índices y las transacciones que abarcan varias particiones. Esta sección explica los mecanismos con la profundidad suficiente para juzgar lo que afirma una base de datos y para diseñar una clave de sharding.
Replicación líder-seguidor
El esquema más común es la replicación líder-seguidor (también llamada primario-réplica; la documentación antigua dice maestro-esclavo). Un nodo, el líder, acepta todas las escrituras. Registra cada cambio en su log (el write-ahead log de PostgreSQL, el binary log de MySQL) y transmite ese log a los seguidores, que aplican los cambios en el mismo orden. Las lecturas pueden ir al líder o a los seguidores. La replicación en streaming de PostgreSQL, la replicación de MySQL, los grupos de disponibilidad de SQL Server, los conjuntos de réplicas de MongoDB y la mayoría de las bases de datos gestionadas en la nube funcionan así.
La decisión de diseño clave es cuándo comunica el líder que un commit ha tenido éxito. Con la replicación síncrona, espera hasta que un seguidor confirma que tiene el cambio, de modo que una escritura confirmada sobrevive a la pérdida del líder; el precio es que cada commit incluye un viaje de ida y vuelta por la red y que, si el seguidor síncrono está caído, las escrituras se detienen. Con la replicación asíncrona, el líder confirma de inmediato y envía el cambio después; los commits son rápidos y los seguidores pueden estar lejos, pero si el líder muere, se pierden los últimos cambios que no habían llegado a ningún seguidor, aunque al cliente se le dijo que estaban confirmados. Un compromiso habitual, llamado semisíncrono en MySQL, espera a un seguidor y deja que el resto sea asíncrono. En el banco, perder un pago confirmado es inaceptable, así que lo normal es tener al menos una copia síncrona (o una base de datos basada en consenso); para la base de datos de la carta de la plataforma de reparto de comida, unos segundos de posible pérdida pueden ser un precio aceptable a cambio de velocidad. Este es, en forma concreta, el compromiso del RPO visto en disponibilidad y fiabilidad.
Líder con un seguidor síncrono y otro asíncrono
El retardo de replicación y las anomalías que causa
Los seguidores asíncronos suelen ir milisegundos por detrás, pero con carga, durante una actualización por lotes grande o a través de una red lenta, el retardo de replicación puede crecer hasta segundos o minutos. Leer de los seguidores produce entonces anomalías que los usuarios notan. Douglas Terry y sus colaboradores definieron las dos primeras garantías que siguen, leer tus propias escrituras y lecturas monótonas, como garantías de sesión en 1994; la tercera importa sobre todo cuando los datos también están particionados.
Leer tus propias escrituras, incumplido
Anomalías del retardo y sus soluciones
| Garantía | Qué sale mal sin ella | Soluciones habituales |
|---|---|---|
| Leer tus propias escrituras (read-your-writes, read-after-write) | Una clienta cambia su dirección de entrega, recarga y ve la antigua: parece que el guardado falló, así que lo vuelve a intentar. | Leer del líder los datos que el usuario puede haber acabado de cambiar (por ejemplo, durante un minuto después de su última escritura); o registrar la posición del log del líder en el momento de escribir y leer de un seguidor solo cuando haya aplicado esa posición. |
| Lecturas monótonas | La app de un repartidor se actualiza dos veces; la primera lectura llega a un seguidor al día y muestra un pedido como asignado, la segunda llega a uno retrasado y el pedido vuelve a parecer sin asignar. El tiempo parece ir hacia atrás. | Dirigir las lecturas de cada usuario al mismo seguidor (por ejemplo, aplicando un hash al id del usuario) y cambiar a otro solo cuando no esté disponible. |
| Lecturas de prefijo consistente | Los lectores ven una respuesta antes que la pregunta, o el evento entregadode un pedido antes que recogido, porque los dos se escribieron en particiones distintas que se retrasan de forma distinta. | Escribir los datos relacionados causalmente en la misma partición, o seguir explícitamente las dependencias causales. |
Ninguna de estas anomalías existe si todas las lecturas van al líder, así que la solución más sencilla suele ser enviar al líder las lecturas que deben estar actualizadas y usar los seguidores solo para las lecturas que toleran datos obsoletos: listados de productos, informes, feeds de búsqueda. Diseña esto de forma deliberada para cada consulta, en lugar de dirigir todas las lecturas a las réplicas
por defecto.
Failover y split brain
El failover (conmutación por error) sustituye a un líder que ha fallado por un seguidor. Tiene tres pasos: detectar que el líder ha fallado (normalmente mediante un timeout), elegir el nuevo líder (idealmente el seguidor más actualizado, a menudo mediante un protocolo de consenso como Raft entre los nodos o un coordinador externo como etcd o ZooKeeper) y redirigir las escrituras hacia él. Cada paso puede salir mal. Un timeout demasiado corto provoca un failover durante un breve corte de red o un pico de carga, lo que empeora las cosas; uno demasiado largo alarga la interrupción. Con la replicación asíncrona, se pierden las escrituras que el antiguo líder aún no había enviado y, si el antiguo líder vuelve y todavía las conserva, entran en conflicto con las escrituras nuevas.
El fallo más peligroso es el split brain (cerebro dividido): dos nodos creen ser el líder y ambos aceptan escrituras, de modo que los datos divergen de maneras que puede ser imposible fusionar automáticamente. Suele producirse tras una partición de red en la que el antiguo líder queda aislado pero sigue vivo. La protección necesita un quórum (un nodo solo puede liderar mientras una mayoría esté de acuerdo), fencing (la autoridad del nuevo líder se impone mediante un token siempre creciente que el almacenamiento comprueba, o se apaga a la fuerza el antiguo líder, una técnica apodada STONITH: shoot the other node in the head, es decir, dispara al otro nodo en la cabeza) y clientes que sigan de forma fiable al líder actual.
El incidente de GitHub del 21 de octubre de 2018 muestra lo difícil que es esto en la práctica. Un fallo de red cortó la conexión entre su nodo central de la Costa Este de EE. UU. y su centro de datos principal de la Costa Este durante 43 segundos. Sus herramientas de failover, funcionando según lo previsto, promovieron a primarios las bases de datos de la Costa Oeste. Cuando volvió la conectividad, ambas costas tenían escrituras de las que carecía la otra, así que GitHub no pudo simplemente volver atrás; para proteger la integridad de los datos decidió restaurar y conciliar, y el servicio estuvo degradado durante 24 horas y 11 minutos. El failover automático entre regiones había convertido un corte de 43 segundos en una recuperación de un día entero.
Replicación multilíder y sin líder
En la replicación multilíder, varios nodos aceptan escrituras y se replican entre sí. Se usa para varios centros de datos (cada región escribe localmente, así que los usuarios evitan la latencia de cruzar un océano), para clientes que funcionan sin conexión (una app de calendario o de notas es en la práctica un líder en cada dispositivo) y para la edición colaborativa. El problema inevitable son los conflictos de escritura: dos líderes modifican el mismo registro de forma concurrente. Entre las soluciones están last write wins (sencillo, pero descarta datos en silencio y depende de los relojes), fusionar los valores, conservar ambos y preguntar a una persona, o usar tipos de datos diseñados para fusionarse automáticamente (CRDT, tipos de datos replicados libres de conflictos). El enfoque más fiable es evitar los conflictos: asignar a cada registro una región de origen que sea la única que acepta sus escrituras, como el país de residencia de un cliente.
La replicación sin líder, popularizada por el artículo Dynamo de Amazon (DeCandia y colaboradores, 2007) y usada en Apache Cassandra, ScyllaDB y Riak, no tiene ningún líder. El cliente, o un nodo coordinador que actúa en su nombre, envía cada escritura a las N réplicas de una clave y la considera correcta cuando W de ellas la confirman; envía cada lectura a las réplicas y espera R respuestas, quedándose con la versión más reciente. Si R + W > N, cada conjunto de lectura se solapa con cada conjunto de escritura correcta en al menos una réplica, así que una lectura normalmente ve la última escritura confirmada. Las réplicas que se perdieron escrituras se reparan mediante la reparación en lectura (read repair: el coordinador vuelve a escribir la versión más reciente cuando ve una obsoleta) y una antientropía en segundo plano que compara las réplicas. Cassandra expone estas opciones en cada solicitud como niveles de consistencia, como ONE, QUORUM y ALL.
Una escritura y una lectura de quórum con N = 3
A menudo se confía demasiado en los quórums. R + W > N no hace por sí solo que el sistema sea linealizable: las escrituras concurrentes, las escrituras que fallaron en algunas réplicas pero tuvieron éxito en otras, los quórums laxos (sloppy quorums: escribir en nodos sustitutos durante los fallos) y last-write-wins con relojes desfasados permiten datos obsoletos o perdidos. El Capítulo 6 recorre la aritmética de los quórums y estos casos límite en CAP y PACELC. Los almacenes sin líder son excelentes para lograr una alta disponibilidad de escritura frente a fallos y entre centros de datos; encajan mal con invariantes como no vender nunca dos veces el mismo asiento
, salvo que ofrezcan una transacción ligera o una función de consenso para esas operaciones.
Particionamiento: por rango de claves o por hash
El particionamiento asigna cada registro a una partición según su clave de partición (o clave de sharding), y el objetivo es repartir los datos y la carga de forma uniforme. Una partición que recibe una carga desproporcionada es un punto caliente, y un reparto desigual se llama desequilibrio (skew). Hay dos esquemas básicos.
El particionamiento por rango de claves da a cada partición un rango contiguo de claves, como los tomos de una enciclopedia. Los recorridos por rango son eficientes porque las claves adyacentes se guardan juntas (todos los pedidos de octubre en una o dos particiones), y las particiones pueden dividirse cuando crecen, como en Bigtable, HBase, Spanner y el sharding por rangos de MongoDB. El peligro es una clave que aumenta con el tiempo: si la clave es una marca de tiempo o un número de pedido secuencial, cada escritura nueva cae en la última partición, que se convierte en un punto caliente mientras las demás están ociosas. El particionamiento por hash aplica una función hash a la clave y asigna rangos de valores hash a las particiones, de modo que reparte uniformemente incluso las claves secuenciales. El precio es que las consultas por rango sobre la clave tienen que preguntar ahora a todas las particiones. Muchos almacenes combinan ambos con una clave compuesta: Cassandra aplica un hash a la clave de partición para elegir el nodo y ordena las filas dentro de la partición por columnas de agrupamiento (clustering columns), de modo que (customer_id, placed_at) pone juntos los pedidos de un cliente, en orden temporal, en un solo nodo.
Particionamiento por rango frente a por hash para los pedidos
Hashing consistente y reequilibrado
La forma obvia de asignar hashes a nodos, hash(key) mod N, tiene un defecto fatal: cuando N cambia, casi todas las claves se mueven. Pasar de 4 a 5 nodos reubica alrededor del 80% de las claves (cualquier clave cuyo resto difiera para 4 y para 5), y pasar de 10 a 11, alrededor del 91%, lo que significa copiar casi todo el conjunto de datos solo para añadir una máquina. El hashing consistente, introducido por David Karger y sus colaboradores en 1997 para las cachés web, coloca tanto los nodos como las claves en un espacio de hash circular; cada clave pertenece al siguiente nodo en el sentido de las agujas del reloj. Un nodo nuevo asume solo las claves situadas entre él y su predecesor, alrededor de 1/N del total, y al retirar uno, este entrega sus claves a su vecino. Con pocos nodos los arcos son desiguales, así que a cada nodo físico se le asignan muchos nodos virtuales (tokens) en distintos puntos del anillo, lo que equilibra la carga y permite que un nodo nuevo tome un poco de cada nodo existente. Dynamo y Cassandra usan este esquema.
Hashing consistente en un anillo
Hay otras dos estrategias de reequilibrado habituales. Con un número fijo de particiones, se crean al principio muchas más particiones que nodos (por ejemplo, 1024 para 10 nodos) y se asignan particiones enteras a los nodos; añadir un nodo mueve unas pocas particiones enteras y nunca vuelve a aplicar el hash a las claves. Los shards de Elasticsearch, los vBuckets de Couchbase y muchos esquemas de sharding hechos a mano usan este enfoque, y la decisión principal es elegir un número de particiones lo bastante grande para el crecimiento futuro. Con el particionamiento dinámico, como en HBase y MongoDB, una partición se divide cuando supera un umbral de tamaño y las mitades pueden moverse a nodos distintos. Sea cual sea el esquema, el reequilibrado mueve grandes cantidades de datos por la red mientras el sistema atiende tráfico, así que muchos equipos prefieren que una persona apruebe cada movimiento, y lo limitan para proteger la latencia.
Las solicitudes también deben dirigirse a la partición correcta: o bien cualquier nodo acepta una solicitud y la reenvía (los nodos de Cassandra conocen el anillo mediante gossip), o bien una capa de enrutamiento conoce el mapa de particiones (el mongos de MongoDB), o bien la biblioteca cliente guarda el mapa. El propio mapa suele guardarse en un servicio de coordinación como etcd o ZooKeeper, o en el grupo de consenso de la propia base de datos.
Claves calientes y desequilibrio
El hash reparte uniformemente las claves, no la carga. Si una clave es mucho más activa que el resto, su partición está caliente sea cual sea el esquema: la cuenta de una celebridad en una red social, el restaurante insignia de una cadena nacional durante una promoción o la propia cuenta de liquidación de un banco, que interviene en cada pago. Los almacenes gestionados hacen explícitos los límites; Amazon DynamoDB, por ejemplo, documenta un máximo por partición de 3000 unidades de lectura y 1000 unidades de escritura por segundo. Entre las mitigaciones están almacenar en caché las lecturas calientes, dividir una clave caliente añadiéndole un pequeño sufijo aleatorio (escrituras de restaurant:88#0 a restaurant:88#9, a costa de leer las diez y combinarlas), mover un cliente (tenant) muy grande a particiones dedicadas y agregar contadores en memoria antes de escribirlos. Todas añaden complejidad, así que primero mide qué claves están realmente calientes.
Índices secundarios entre particiones
El particionamiento funciona sin problemas para las búsquedas por la clave de partición. Las consultas por cualquier otro atributo necesitan un índice secundario, y hay dos formas de particionarlo. Un índice local (particionado por documento) vive con cada partición y cubre solo las filas de esa partición. Las escrituras actualizan una sola partición, pero una consulta por el atributo indexado debe enviarse a todas las particiones y fusionar los resultados (dispersión-recolección, scatter-gather), así que su latencia la marca la partición más lenta y su costo crece con el número de particiones. Un índice global (particionado por término) está a su vez particionado por el valor indexado, así que una consulta lee una sola partición del índice; pero una sola escritura puede necesitar ahora actualizar particiones del índice en otros nodos, lo que normalmente se hace de forma asíncrona, así que el índice puede ir brevemente por detrás de los datos. DynamoDB muestra ambos: un índice secundario local comparte la clave de partición de la tabla, mientras que un índice secundario global tiene la suya propia y es eventualmente consistente.
Índices secundarios locales y globales
Ejemplo práctico
Ejemplo práctico: dimensionar y elegir una clave de sharding para los pedidos
La plataforma de reparto de comida ha crecido hasta 30 países y 10 millones de pedidos al día, con un pico vespertino del triple de la tasa media. Cada pedido genera unas 10 escrituras a lo largo de su vida (creación, líneas, cambios de estado, referencias de pago) y ocupa unos 4 KB, índices incluidos. Los clientes pueden ver todo su historial de pedidos; la normativa exige que los pedidos puedan recuperarse durante seis años, pero el personal de soporte solo necesita acceso en línea a los dos últimos.
| Tasa de escritura | 10 000 000 / 86 400 s = unos 116 pedidos por segundo de media; en el pico, unos 350 por segundo; con 10 escrituras cada uno, unas 3500 escrituras por segundo en el pico. Un único primario bien ajustado puede con esto; el throughput por sí solo no obliga a hacer sharding. |
|---|---|
| Crecimiento del almacenamiento | 10 000 000 x 4 KB = 40 GB al día, unos 14.6 TB al año, unos 29 TB para dos años en línea. |
| ¿Necesitamos hacer sharding? | Si solo tuvieran que estar en línea 90 días (3.6 TB), un primario más réplicas, con particionamiento de tablas por tiempo y archivado en el almacén de datos, sería la respuesta aburrida y correcta. Con dos años en línea y 29 TB, los tiempos de restauración y de reconstrucción de réplicas en un solo nodo se vuelven inaceptables, así que el equipo hace sharding. |
| Dimensionar los shards | Objetivo de unos 2 TB por shard para que una réplica pueda reconstruirse en horas: 29 TB / 2 TB = unos 15, así que 16 shards, cada uno con un líder y dos seguidores. Se crean 1024 particiones lógicas asignadas a los 16 shards, para que crecer signifique mover particiones enteras, no volver a aplicar el hash. Escrituras en el pico por shard: 3500 / 16 = unas 220 por segundo. |
| Candidata: order_id (hash) | Escrituras perfectamente uniformes, pero tanto el historial de un cliente como la cola en vivo de un restaurante pasan a ser de dispersión-recolección entre 16 shards. Descartada como clave principal de ubicación. |
| Candidata: placed_at (rango) | Muy buena para informes por rango de tiempo, pero cada pedido nuevo escribe en la partición más reciente: un punto caliente garantizado. Descartada. |
| Candidata: restaurant_id | La cola de un restaurante es local, pero las grandes cadenas y las promociones crean claves calientes, y el historial del cliente se dispersa. Descartada. |
| Candidata: país o ciudad | Facilita la residencia de datos y las consultas locales, pero una sola ciudad grande puede concentrar el 15% de todos los pedidos: desequilibrio. Se conserva solo como primer nivel grueso para la residencia (clústeres de la UE y de fuera de la UE), no como clave de sharding. |
| Decisión | Sharding por hash(customer_id), con las filas ordenadas por placed_at dentro de cada partición. El id del pedido incorpora el número de partición del cliente, de modo que una búsqueda por id de pedido va directamente a un solo shard. |
| Cubrir las demás consultas | La cola en vivo del restaurante la atiende un modelo de lectura independiente con clave de restaurante, construido a partir de los eventos de pedido (un índice global deliberado y eventualmente consistente). La analítica se ejecuta en el almacén de datos. No se necesitan transacciones entre clientes: cada pedido afecta a un solo cliente. |
Las cifras cambiaron la pregunta. El throughput nunca obligó a hacer sharding; lo hicieron los datos conservados en línea. Reducir la ventana en línea habría eliminado por completo la necesidad, algo que vale la pena plantear al negocio antes de construir un sistema con sharding.
La clave de sharding se eligió a partir de las consultas más importantes y de sus necesidades de consistencia, no solo a partir del modelo de datos. A cada una de las demás consultas se le dio una ruta explícita, diseñada por separado.
Elegir una clave de sharding es una de las decisiones más difíciles de revertir: cambiarla más adelante significa mover todas las filas. Regístrala en un ADR con los volúmenes y las consultas que se supusieron.
Errores comunes
Tratar las réplicas como copias de seguridad. Una réplica copia fielmente un DELETE erróneo en milisegundos; siguen haciendo falta copias de seguridad y recuperación a un momento dado (Capítulo 0). Leer de las réplicas por defecto sin decidir qué consultas toleran el retardo. Confiar en un failover automático que nunca se ha ensayado, o que puede promover un nodo mientras el antiguo líder sigue aceptando escrituras. Hacer sharding demasiado pronto: una base de datos con sharding complica los joins, las transacciones, los cambios de esquema y las operaciones, y los nodos individuales modernos son muy grandes; agota primero la indexación, la caché, las réplicas de lectura, el archivado y el escalado vertical. Hacer sharding por una clave que crece de forma monótona, lo que concentra cada inserción en una sola partición. Olvidar las rutas de acceso secundarias y descubrir después de la división que una consulta importante debe preguntar ahora a todos los shards. Como muestra el resto de este capítulo, las buenas alternativas suelen implicar datos derivados: modelos de lectura, cachés y almacenes analíticos, cada uno construido para un tipo de consulta.
Comprueba lo que has entendido
¿Por qué la replicación asíncrona corre el riesgo de perder escrituras que a los clientes se les dijo que estaban confirmadas?
El líder confirma antes de que ningún seguidor tenga el cambio; si falla antes de enviarlo, el seguidor promovido nunca recibió esas escrituras.
Un usuario guarda un cambio y ve de inmediato el valor antiguo. ¿Qué garantía se incumple y cuál es una solución?
Leer tus propias escrituras (read-your-writes). Atiende desde el líder las lecturas que ese usuario haga de datos modificados recientemente, o espera hasta que el seguidor haya aplicado la posición del log de la escritura.
¿Por qué hash mod N es una mala forma de asignar claves a nodos?
Cambiar N mueve la mayoría de las claves (alrededor del 80% al pasar de 4 a 5 nodos), mientras que el hashing consistente o las particiones lógicas fijas mueven solo alrededor de 1/N.
¿Cuál es el compromiso entre los índices secundarios locales y los globales?
Los índices locales mantienen las escrituras en una sola partición, pero obligan a las consultas a preguntar a todas las particiones; los índices globales permiten que las consultas vayan a una sola partición del índice, pero exigen actualizaciones del índice entre particiones, normalmente asíncronas.
Fuentes y lecturas recomendadas
- DeCandia et al.: Dynamo: Amazon's Highly Available Key-value Store (SOSP 2007)
- GitHub: October 21 post-incident analysis (2018)
- PostgreSQL documentation: High Availability, Load Balancing, and Replication (log-shipping standby servers)
- Amazon DynamoDB Developer Guide: Best practices for designing and using partition keys effectively
- Microsoft Azure Architecture Center: Sharding pattern
¿Tienes una pregunta sobre esta sección? ArchitectBot, el asistente de estudio con IA del curso, responde primero a partir del curso.
Pregunta: “Nuestra tabla de pedidos se acerca a los 5 TB en un único servidor PostgreSQL. ¿Deberíamos hacer sharding y, en ese caso, cómo elegimos la clave de sharding?”