Be Software Architecture Wise
Be Wise Apps

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ítulos
Solo 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

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

Los clientes envían las escrituras al líder, que transmite su log de cambios a un seguidor síncrono y a otro asíncrono; las lecturas pueden ir a los seguidoresServ. de appescrit.Líderacepta escriturassínc.: commit esperaasínc.: el commitno esperaSeguidor 1síncronosiempre al díaSeguidor 2asíncronose atrasalect.lecturas (¿obsoletas?)envía su log de cambios(WAL o binlog)
Todas las escrituras van al líder; su log de cambios fluye hacia los seguidores, y el commit solo espera al sí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

Una usuaria actualiza su dirección de entrega en el líder, lee de inmediato de un seguidor retrasado y ve la dirección antiguaUsuar.LíderSeguidor1 guarda dirección2 OK3 recarga4 ¡la antigua!5 el cambio llegatras el retardoSolución: leer del líder los cambios recientes del usuario, o esperar a que el seguidor se ponga al día
El líder confirma el cambio de la propia usuaria, pero aún no se ha aplicado en el seguidor que atiende su siguiente lectura.

Anomalías del retardo y sus soluciones

GarantíaQué sale mal sin ellaSoluciones 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ótonasLa 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 consistenteLos lectores ven una respuesta antes que la pregunta, o el evento entregado de 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

En un almacén sin líder con tres réplicas, una escritura va a las tres y vale si dos confirman; una lectura pregunta a las tres, espera a dos y toma la versión más reciente: los conjuntos de lectura y escritura se solapanEscrit.: N = 3, W = 2Lect.: R = 2ClienteRéplica Av2Réplica Bv2Réplica Cv1 (caída)2 confirmaciones: éxitoClienteRéplica Av2Réplica BlentaRéplica Cv1responden v2 y v1:gana la nueva (v2);C reparada a v2R + W = 4 > N = 3: cada lecturase solapa con última escr.
Con W = 2 y R = 2, al menos una réplica de cada lectura ha visto la última escritura confirmada.

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

El particionamiento por rango agrupa claves adyacentes, así que las claves ordenadas por tiempo caen en la partición más nueva; el hash reparte las claves, pero dispersa los rangosPor rangoPor hash (clave)P1ene. a abr.P2may. a ago.P3sep. a hoyrecorrido de un mes:una partición (bien)los pedidos nuevos a P3:hotspot (mal)P1hash 0 a 33P2hash 34 a 66P3hash 67 a 99pedidos nuevos repartidosentre P1, P2, P3 (bien)recorrido de un mes:preguntar a todas (mal)Clave compuesta: hash(customer_id) elige la partición,placed_at ordena las filas: el historial de un cliente es un recorrido
El particionamiento por rango mantiene juntos los rangos, pero concentra las escrituras ordenadas por tiempo; el hash reparte las escrituras, pero dispersa los rangos.

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

Nodos y claves se sitúan en un anillo por hash; cada clave es del siguiente nodo en sentido horario, así que añadir el nodo D solo le pasa las claves entre el predecesor de C y DNodo ANodo BNodo CNuevo nodo Danillo hashclaves: s. horarioAñadir nodo Dhash mod N, de 4 a 5 nodos:se mueve ~80% de claveshashing consistente:solo se mueven las clavesentre B y D (C → D),aprox. 1/N del totalnodos virtuales: un servidorocupa muchos puntos del anilloy equilibra la carga
Las claves pertenecen al siguiente nodo en sentido horario, así que un nodo nuevo asume un arco en lugar de redistribuirlo todo.

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

Un índice local vive con cada partición: escrituras locales, pero una consulta por restaurante pregunta a todas las particiones; un índice global se particiona por el valor indexado: la consulta va a una partición del índice, pero cada escritura puede actualizar un índice en otro nodoÍndice local (por partición)Índice global (por término)Partición 1pedidos por clienteíndice: restaurante(solo sus filas)Partición 2pedidos por clienteíndice: restaurante(solo sus filas)Buscarrestaurante = 88dispersión a todas y recolecciónPartición 1pedidos por clientePartición 2pedidos por clienteÍndice parte arestaurantes 1-50Índice parte brestaurantes 51-99asínc.Buscarrestaurante = 881 partición del índice;quizá algo desfasada
Un índice local abarata las escrituras y dispersa las consultas; un índice global hace que las consultas sean dirigidas y que las actualizaciones del índice sean remotas y normalmente asíncronas.

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 escritura10 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 almacenamiento10 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 shardsObjetivo 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_idLa 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 ciudadFacilita 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ónSharding 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 consultasLa 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.

Siguiente · 7.5CQRS y event sourcingSeparar el modelo que modifica los datos de los modelos que los leen, almacenar el estado como una secuencia de eventos de solo anexado (append-only) y los costos reales: consistencia eventual, versionado de eventos y datos personales.Continuar →

Fuentes y lecturas recomendadas

¿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?”