- Partiendo de un almacén clave-valor sencillo en Bash, muestra paso a paso por qué una base de datos real debe tratar como problemas de diseño separados la durabilidad, la atomicidad, el aislamiento y el rendimiento
fsync/fdatasync,flocky WAL son herramientas básicas para proteger los datos ante fallas y concurrencia, pero cuanto mayores son las garantías, mayor es el costo de rendimiento- Los motores de almacenamiento usan estructuras como B-tree y LSM tree para reducir el I/O de disco y el costo de búsqueda, y cada una viene acompañada de mantenimiento como vacuum o compaction
- Las bases de datos distribuidas obtienen disponibilidad y escalado horizontal, pero a cambio asumen la complejidad de los sistemas distribuidos: teorema CAP, particiones de red, ajuste de consistencia y resolución de conflictos
- Al elegir o implementar una base de datos, hay que ajustar a la carga de trabajo las garantías ACID, el nivel de aislamiento, la estructura de almacenamiento, el método de replicación y los requisitos de consistencia
Los problemas fundamentales de las bases de datos que se hacen visibles con bashdb
bashdbes un almacén clave-valor simple hecho con dos funciones de Bashdb_sethace append a un archivo en formatokey,valuedb_getlee el último valor combinandogrep,sedytail
- Aunque es simple para fines de aprendizaje, esta implementación por sí sola ya deja ver los problemas que una base de datos de producción debe resolver
- Durability: si la máquina falla después de que
db_settuvo éxito, los datos que no se hayan vaciado al disco pueden perderse - Atomicity: si ocurre una falla durante la escritura, los datos pueden quedar registrados solo parcialmente y corromperse
- Isolation: si lectura y escritura acceden al mismo elemento al mismo tiempo, la lectura puede ver solo una parte de los datos
- Performance:
db_getbusca línea por línea en todo el archivo, así que esO(n)
- Durability: si la máquina falla después de que
ACID e intentos de mejorar bashdb
- ACID es un acrónimo que agrupa propiedades que muchas bases de datos intentan garantizar
- Atomicity: si ocurre una falla durante una escritura, se cancela o revierte toda la transacción para no dejar un estado de escritura parcial
- Consistency: una transacción inválida no debe dañar la base de datos
- Isolation: no debe haber race conditions en accesos concurrentes al mismo dato
- Durability: una escritura exitosa debe mantenerse incluso después de una falla de energía
- No todas las transacciones de base de datos tienen que garantizar necesariamente ACID, y en algunos casos de uso se pueden reducir esas garantías para ganar rendimiento
-
Durabilidad y
fsync- La llamada al sistema
writeescribe el búfer en el archivo, pero eso no significa que se registre de inmediato en un dispositivo de almacenamiento no volátil - El kernel puede guardar el búfer como dirty page en el page cache y vaciarlo al disco más tarde
- El dispositivo de disco o un sistema RAID también pueden tener su propio write cache
fsyncyfdatasyncson llamadas al sistema para vaciar dirty pages al almacenamiento persistentefdatasyncvacía el raw buffer entregado conwritefsyncvacía no solo los datos, sino también metadatos del archivo comomtime- Si se añade
sync -d databasedespués dedb_set, se puede mejorar la durabilidad con un comportamiento parecido afdatasync, pero normalmente sync es más lento que la propia escritura, así que perjudica el rendimiento - Que
fsync()tenga éxito significa que “todas las escrituras desde el último fsync llegaron al disco”, y no solo “las escrituras posteriores al último fsync exitoso” - PostgreSQL tuvo este problema en 2018 y cambió su comportamiento para entrar en pánico en vez de reintentar cuando falla fsync
- Este incidente se conoce como fsyncgate, y se enlaza el paper sobre fallas de fsync como material relacionado
- MongoDB, por defecto, hace sync de las escrituras cada 100 ms, por lo que no es 100% duradero
- La llamada al sistema
-
Aislamiento y
flock- En
bashdb, la forma más simple de aislamiento entre múltiples procesos es poner un lock al archivo de almacenamiento antes de leer o escribir flocken Linux bloquea archivos, y con la bandera-spermite que varios readers lean al mismo tiempo mediante un shared lock- La versión mejorada de
bashdbusa exclusive lock para escritura y shared lock para lectura - La desventaja es que bloquea toda la base de datos en cada escritura
- No es fácil garantizar atomicidad de forma simple solo con Bash; existe la posibilidad de usar
mv -Torename, pero no se completa bashdbsigue sin resolver el problema de consultasO(n)
- En
El papel del motor de almacenamiento y sus cuellos de botella
- El motor de almacenamiento proporciona una abstracción para leer y escribir datos en almacenamiento persistente, y su objetivo principal es lograr alto throughput y baja latencia
- La mayor limitación proviene de la diferencia de velocidad del propio disco
- En la tabla de latencias de ejemplo, una referencia a L1 cache se presenta como alrededor de
0.5ns, una lectura aleatoria de 4 KB en SSD como150,000nsy una búsqueda en disco como10,000,000ns - Si una referencia a L1 cache fuera como un latido de unos 0.5 segundos, una lectura secuencial de 1 MB en SSD equivaldría a unos 12 días y una lectura secuencial de 1 MB en disco a unos 8 meses
- En la tabla de latencias de ejemplo, una referencia a L1 cache se presenta como alrededor de
- Por eso, el diseño de motores de almacenamiento ha evolucionado para reducir al máximo el I/O de disco y los disk seeks
- Los elementos de diseño típicos de un motor de almacenamiento son los siguientes
- La estructura de datos básica para guardar elementos en disco
- Transacciones ACID
- Cache para reducir lecturas de disco
- Una capa de API como SQL, document o graph
- Las estructuras de datos de los motores de almacenamiento pueden dividirse a grandes rasgos en estructuras mutables e inmutables
- Las estructuras mutables pueden sobrescribir más adelante los datos ya escritos en el archivo
- Las estructuras inmutables solo vuelven a leer los datos escritos en el archivo
B-tree mutable
- Para mantener buen rendimiento aunque aumenten los datos, no basta una búsqueda lineal como en
bashdb; hay que poder encontrar elementos en tiempo logarítmico como máximo - Un BST permite consultas
O(log n), pero si los nodos están muy separados entre sí en disco, durante la búsqueda puede haber muchos disk seeks - Un B-tree es una generalización del BST donde un nodo puede tener más de dos hijos, y aprovecha la spatial locality
- Normalmente se lee una page de 4 KB u 8 KB desde disco y luego se comparan secuencialmente varios nodos dentro de ella en memoria y en la CPU cache
- Como el acceso a memoria y a la CPU cache es varios órdenes de magnitud más rápido que el disco, es importante aprovechar al máximo los bytes leídos desde el disco
- El acceso secuencial a memoria puede ser muy potente gracias a SIMD, instruction pipelining y prefetching
- Un B+ tree guarda valores solo en los leaf nodes y deja solo las keys en los demás nodos, lo que permite comparar más keys dentro de una sola page de disco
-
Recuperación de espacio y vacuum
- Un B-tree necesita recuperar el espacio vacío generado por la fragmentación de datos para optimizar el uso del espacio
- Si se actualiza con un valor más grande, puede sobrescribir los datos del siguiente nodo, por lo que el elemento se mueve a otra ubicación y quedan huecos en la page original
- Si se actualiza con un valor más pequeño, queda un hueco al final
- Las eliminaciones crean huecos en el lugar donde estaba el valor borrado
- Este proceso de recuperación de espacio y reescritura de pages puede llamarse vacuum, compaction, page defragmentation o maintenance
- Normalmente se realiza en segundo plano para evitar picos de latencia en las solicitudes de los usuarios
- PostgreSQL permite configurar el auto vacuum daemon
- Los B-tree se usan con frecuencia como estructura base de índices, como el índice predeterminado de PostgreSQL, y hubo casos en que se llamó en broma a DynamoDB un “distributed B-tree”
Árbol LSM inmutable
- El árbol LSM es una estructura de datos append-only que parte de la idea de que el disk seek es costoso.
- Si los datos solo se agregan al final del archivo, el cabezal del disco no necesita moverse mucho hasta la siguiente posición de escritura, lo que favorece las cargas con muchas escrituras.
Log Structured Merge tree, abreviado como LSM tree, se usa en motores de almacenamiento de bases de datos modernos como RocksDB, Cassandra y ScyllaDB.- Su funcionamiento básico es el siguiente:
- Las escrituras se almacenan en búfer en una estructura de datos ordenable en memoria.
- Algunos ejemplos son
AVL tree,Red Black treeySkip List. - Al alcanzar cierta capacidad, se hace flush a un archivo ordenado llamado
Sorted String Table, es decir, una SSTable.
- Una SSTable almacena datos ordenados, por lo que puede reducir el I/O de disco con binary search y un índice disperso.
- Para garantizar la durabilidad, las operaciones escritas en memoria también se registran en el Write-Ahead Log, o WAL.
- Al iniciar el programa, se lee el WAL para restaurar el estado previo al cierre o al crash.
- Las eliminaciones también se agregan como una escritura normal, y en lugar del valor se almacena un tombstone.
- El tombstone se elimina durante el proceso de compaction.
-
Lectura y compaction del árbol LSM
- La lectura en un árbol LSM primero busca en la estructura de datos en memoria y, si no está ahí, recorre las SSTables en disco desde los archivos más recientes hasta los más antiguos.
- Cuantas más escrituras haya, mayor será el número de SSTables que se deben revisar.
- Aunque cada archivo esté ordenado, revisar muchos archivos pequeños puede ser más lento que consultar un solo archivo grande.
- La comparación es
log(num_files * table_size) < num_files * log(table_size). - Compaction es una tarea en segundo plano que fusiona varias SSTables pequeñas en una SSTable grande y elimina los tombstones.
- RocksDB implementa Leveled Compaction.
- La SSTable recién volcada se ubica en el nivel 0.
- Cuando se acumula la cantidad configurada de archivos en un nivel, después de la compaction el nuevo archivo se promueve al siguiente nivel.
- La eliminación de tombstones debe hacerse con cuidado.
- Puede ocurrir el problema de data resurrection, donde un elemento eliminado reaparece durante la compaction con archivos más antiguos.
- RocksDB conserva los tombstones hasta la compaction que los promueve al último nivel.
- Un ejemplo real en Rust se enlaza en el código del árbol LSM de dbeel.
-
Bloom filter
- Un Bloom filter es una estructura de datos probabilística de conjuntos que permite verificar de forma eficiente que un elemento no está en un conjunto.
- El resultado de la consulta tiene dos posibilidades:
false: el elemento definitivamente no está en el conjunto.true: el elemento podría estar en el conjunto.
- Un Bloom filter mapea los resultados de varias hash functions a posiciones de bits en un bitmap y las establece en 1.
- Su complejidad espacial se presenta como
O(log n), a diferencia delO(n)de un conjunto normal. - Se puede ajustar la “probabilidad de estar seguro de que no está” asignando más memoria al bitmap y aumentando la cantidad de hash functions; también hay una calculadora.
- Los árboles LSM almacenan un Bloom filter por cada SSTable, para poder omitir la búsqueda en SSTables donde se confirmó que una clave específica no existe.
WAL y garantías de transacción
- El WAL es una forma de registrar todas las operaciones de una transacción en un archivo especial para sobrevivir a un crash repentino.
- Cuando se inicia el proceso de la base de datos, se lee el archivo WAL y se reconstruye el estado de los datos.
- Las transacciones sin commit log se omiten, con lo que se obtiene atomicidad.
- Si antes de responder al usuario los datos de la solicitud de escritura se registran y se hacen flush en el WAL, entonces seguro podrán leerse al iniciar, con lo que se obtiene durabilidad.
- El WAL puede verse como una forma de event sourcing para eventos de transacción.
Niveles de aislamiento y control de concurrencia
- Los métodos para lograr aislamiento se dividen, en términos generales, en tres:
- Bloqueo pesimista: impide el acceso a los datos que se están escribiendo en ese momento.
- Bloqueo optimista: modifica una copia de los datos y solo hace commit si el original no cambió durante la transacción; de lo contrario, hace retry.
- MVCC: en lugar de sobrescribir los datos, crea una nueva version, para que cada usuario vea un snapshot de un momento específico.
- No todas las aplicaciones necesitan aislamiento completo, es decir, serializable isolation.
- ANSI/ISO SQL 92 clasifica en tres tipos los resultados que pueden ocurrir cuando otra transacción cambia los mismos datos durante una transacción.
- Dirty read: se lee una actualización de otra transacción que todavía no ha hecho commit.
- Non-repeatable read: entre dos lecturas de la misma fila, otra transacción hace commit y el valor cambia.
- Phantom read: entre dos lecturas del conjunto de filas con la misma condición, se agregan o eliminan filas.
- Los niveles de aislamiento de ANSI/SQL 92, de mayor a menor, son los siguientes:
- Serializable: solo lee datos confirmados y evita phantom read, incluso en escrituras de múltiples filas basadas en rangos.
- Repeatable reads: se permite phantom read.
- Read committed: se permite non-repeatable read.
- Read uncommitted: se permite dirty read.
- Un mayor nivel de aislamiento normalmente implica sacrificar rendimiento.
- Los niveles de aislamiento de ANSI/SQL 92 han sido criticados por no ser completos.
- Muchas implementaciones de MVCC no ofrecen serializable isolation, sino snapshot isolation.
- Como algoritmo rápido de MVCC serializable, se recomienda HyPer.
Por qué se necesitan sistemas distribuidos y CAP
- Los sistemas distribuidos agregan mucha complejidad, así que conviene evitarlos cuando una solución no distribuida sea suficiente.
- Hay dos razones comunes para distribuir datos entre varias máquinas:
- Disponibilidad (Availability): aunque la máquina de base de datos falle o se pierda la conexión con el usuario, se pueden enviar solicitudes a otra máquina.
- Escalado horizontal (Horizontal Scaling): en lugar de crecer mediante vertical scaling con una sola máquina más grande, varias máquinas conectadas por red actúan como si fueran una sola.
- Los sistemas distribuidos introducen complejidad operativa y el problema de las particiones de red.
- El teorema CAP dice que un sistema solo puede garantizar dos de las siguientes tres propiedades:
- Consistency: las lecturas reciben la escritura más reciente.
- Availability: todas las solicitudes tienen éxito sin importar las fallas.
- Partition Tolerance: el sistema sigue funcionando aunque haya pérdida o retraso de mensajes entre nodos.
- Una base de datos de una sola máquina no tiene particiones de red y es consistente, pero si la máquina falla, las nuevas solicitudes fallan, por lo que viola availability.
- Si dos máquinas con CPU, memoria y disco separados están conectadas por cable, en una situación de falla las opciones se bifurcan.
- Si se cancelan las solicitudes, se sacrifica availability y se mantiene consistency.
- Si solo la máquina que sigue funcionando continúa procesando solicitudes, se sacrifica consistency y se mantiene availability.
- A los sistemas que sacrifican consistency y se corrigen después se les llama eventually consistent.
- Las particiones de red también dificultan los
JOINeficientes, porque hay que reunir datos dispersos por el clúster; para aliviar esto, el mundo NoSQL recomienda la desnormalización.
Replicación y el caso de Amazon Dynamo
- El paper original de Dynamo de Amazon se presenta como un caso en el que, para el carrito de compras de amazon.com, la availability se consideró más importante que la consistency
- Si un usuario ve dos veces el mismo producto en el carrito, puede eliminar uno
- Se consideró mejor que una situación en la que ni siquiera sea posible completar la compra
- Para obtener availability, no basta con que varios nodos se repartan los datos; cada elemento debe tener al menos una copia adicional
- Los nodos que almacenan copias de un elemento son replicas, y el proceso de copiarlo es replication
- Si aumenta la cantidad de replicas, también aumenta la availability, pero se necesitan más recursos para almacenar las copias
- Las copias de los datos no necesariamente se almacenan completas; también pueden dividirse con erasure coding y dispersarse entre varios nodos, y las características de latencia relacionadas se explican en este artículo sobre erasure coding
Consistent Hashing y distribución de datos
- Cuando hay varios nodos, se necesita un método de load balancing o particionado de datos para decidir qué nodo procesa una solicitud de almacenamiento
- Un método simple es aplicar hash a la primary key y luego usar módulo con la cantidad de nodos
- Si se agrega o elimina un nodo, cambia
len(nodes)y la misma key pasa a apuntar a otro nodo - En ese caso, hay que migrar casi todos los elementos, lo que resulta costoso
- Si se agrega o elimina un nodo, cambia
- Consistent Hashing coloca los nodos en un ring en lugar de un arreglo, para reducir la cantidad de elementos que deben moverse cuando se agregan o eliminan nodos
- Se usa en bases de datos como Dynamo y Cassandra
- En consistent hashing, el hash del nombre del nodo se ubica en el ring, y el nodo que aparece después del hash de la key solicitada se convierte en el propietario
- La selección de replicas puede hacerse recorriendo el ring en sentido antihorario y guardando copias en los siguientes nodos
- Si el nodo propietario cae, un nodo replica puede procesar la solicitud y así mantener la availability
- Este método se conoce como Leaderless Replication y se usa en bases de datos estilo Dynamo como Cassandra
- La cantidad de keys que deben moverse al agregar un nodo es, en promedio,
num_keys / num_nodes - Un virtual node coloca un mismo nodo físico varias veces en el ring para reducir la posibilidad de que algunos nodos terminen siendo dueños de muchos más elementos
- Un ejemplo es agregar un índice como sufijo al nombre del nodo, como
"half-0","half-1"
- Un ejemplo es agregar un índice como sufijo al nombre del nodo, como
- Existe otro método para elegir el leader node y los replica nodes mediante leader election, pero aquí no se trata
Leaderless Replication y ajuste de consistency
- Una configuración leaderless obtiene alta availability a cambio de sacrificar consistency
- Si el nodo propietario está caído cuando llega una solicitud de write, la escritura se hace en una replica, y cuando el nodo propietario vuelve a estar activo, una solicitud de read puede devolver datos antiguos
- Si se necesita consistency en una solicitud específica, el read se puede enviar en paralelo al nodo propietario y a varias replicas, y el cliente elige el dato más reciente
- Las solicitudes de write normalmente se envían en paralelo a todas las replicas, pero solo se espera el acknowledgement de algunos nodos
- Para ajustar la consistency a nivel de solicitud, se verifica
R + W > N/2 + 1N: cantidad de nodos que tienen una copia de los datosW: cantidad de nodos que deben hacer acknowledgement para que el write se considere exitosoR: cantidad de nodos que deben responder para que el read sea exitoso
- Una solicitud a mayoría de nodos, donde
WoResN/2 + 1, se llama quorum -
Resolución de conflictos
- El proceso de elegir el write más reciente se llama Conflict Resolution
- Comparar timestamps sin más no es confiable en sistemas distribuidos
- Cada máquina tiene su propio hardware clock, y como el clock no es perfectamente preciso, se produce drift
- NTP obtiene la hora desde una fuente más precisa, pero como la propia solicitud viaja por la red, no se puede conocer con exactitud cuánto tardó en llegar la respuesta
- Cassandra usa timestamps, y la documentación relacionada está en Cassandra data versioning
- Google Spanner logró garantías de consistency basadas en clock mediante hardware de tiempo de alta precisión especializado y una API que expone un rango de incertidumbre del timestamp; el paper relacionado es el paper de Spanner
- Sistemas como Dynamo reducen algunos conflictos con Version Vectors
- A cada versión del elemento se le adjunta un par
(node, counter)para encontrar relaciones causales entre versiones - Así se pueden identificar versiones definitivamente más nuevas y eliminar algunos valores antiguos
- Como material más detallado, se enlaza Dotted Version Vectors
- También es posible, como en Riak KV, devolver todos los valores en conflicto a la aplicación para que esta los resuelva con base en su conocimiento de los datos
- En sistemas eventually consistent, varias técnicas para reducir conflictos suelen agruparse bajo el término Anti Entropy
Técnicas de Anti Entropy
-
Read Repair
- El cliente elige el valor más reciente entre los resultados de read de varios nodos y luego lo reenvía a los nodos que todavía no lo tienen, para hacer repair
-
Hinted Handoff
- Si una solicitud de write no puede llegar al nodo de destino, se guarda como hint en otro nodo
- Cuando el nodo de destino vuelve a estar available, se le entrega el hint almacenado
- En un quorum write, este método también se conoce como
Sloppy Quorumy eleva aún más la availability de las solicitudes con quorum
-
Merkle Trees
- Read repair solo corrige los datos que se consultaron, por lo que muchos datos pueden permanecer desincronizados durante mucho tiempo
- Sincronizar nodos buscando todas las diferencias completas cuesta
O(n)cuando hay muchos datos - Un Merkle tree es una estructura jerárquica donde los leaf guardan hashes de rangos de datos y cada padre guarda un hash combinado de los hashes de sus hijos
- Si el hash raíz es igual, los datos de dos nodos son iguales; si no, se comparan recursivamente los hashes inferiores para encontrar los datos inconsistentes, lo que permite acelerar la sincronización a
O(log n)
-
Gossip Dissemination
- Es una forma simple y confiable de propagar eventos a todo el clúster
- Un nodo envía mensajes a una cantidad configurada de nodos aleatorios, es decir, el fanout, y los nodos que los reciben vuelven a enviarlos a
Nnodos aleatorios - Si un nodo ve el mismo mensaje de gossip la cantidad de veces configurada, deja de hacer broadcast
- Se enlaza un simulador con el que se puede percibir la convergencia de los datos
- Los mensajes de gossip suelen transmitirse por UDP
Áreas que se pueden estudiar más a fondo
- En bases de datos hay muchos otros temas además de lo que se trató aquí
- Al elegir o implementar una base de datos, también hay que revisar cómo el storage engine, ACID, los niveles de aislamiento, la replicación distribuida y el método de resolución de conflictos se ajustan a los requisitos reales
1 comentarios
Opiniones de Hacker News
Hay un bug en el método
compact: las tombstones solo deberían omitirse al compactar el nivel final, es decir, el más grande, y no eliminarse entre todos los niveles.De lo contrario, una tombstone de un nivel superior desaparece durante la compactación, y una entrada que estaba en un nivel inferior vuelve a quedar expuesta.
En las bases de datos basadas en LSM, una de las características es que los registros de borrado/tombstone permanecen durante mucho tiempo, y algunas bases de datos como RocksDB incorporan optimizaciones para evitarlo.
Conozco la funcionalidad de borrados por rango, pero no recuerdo haber leído mucho sobre borrados de claves individuales.
Mucha gente aprende bases de datos aprendiendo SQL, pero recomiendo tomar una clase así y aprenderlas entendiendo los árboles B.
La mayoría de las ventajas y desventajas de un RDBMS se entienden al conocer los árboles B y cómo afectan la inserción, búsqueda y ordenamiento de claves.
Mucha gente intenta acelerar una base de datos agregando índices, pero al final eso no es más que poner otro árbol encima del árbol, lo que termina tapando el problema de fondo.
Algunos problemas encajan bien con los árboles B, pero muchos otros no.
SQL no es más que una interfaz de consulta para un sistema remoto de árboles B.
Los árboles B no son la única estrategia de indexación, y también es bien sabido que los índices son un mecanismo para mejorar el rendimiento de lectura a costa del rendimiento de escritura.
En general, las bases de datos procesan muchas más lecturas que escrituras.
Me pregunto cuál es exactamente el problema que se estaría ocultando con “poner otro árbol encima del árbol”, y cómo se supone que se resolvería sin tocar los índices.
En tablas de tamaño razonable, los índices son prácticamente indispensables.
Hay que aprender cosas como árboles B e índices hash, la capa de entrada/salida y los modelos de procesos.
Hoy también vale la pena aprender las estrategias generales de las bases de datos orientadas a columnas: materialización tardía de tuplas, ejecución diferida, escaneos lineales y búsqueda binaria, pipelining de instrucciones y demás.
Cuando te familiarizas con estas cosas, te das cuenta de que en la práctica a veces basta con simples archivos planos o con una base de datos embebida como RocksDB, no con un DBMS.
Por supuesto, también podría haber índices de cobertura.
Sobre el consejo “si una solución no distribuida es suficiente, evita los sistemas distribuidos”, diría lo contrario.
Todo sistema operativo no trivial es un sistema distribuido.
Como mínimo, si la base de datos es un conjunto de réplicas, ya es un sistema distribuido, así que no aprender sistemas distribuidos es asumir un riesgo.
Vale la pena ver https://jepsen.io/ y https://raft.github.io/.
Eso no significa que esté bien introducirlos en todas partes; hacerlo aumenta mucho la complejidad más de lo necesario.
Dicho así, no refuta el consejo de evitar complejidad innecesaria. El punto no es si técnicamente es distribuido, sino si realmente hace falta.
Aprender sistemas distribuidos no es lo mismo que usarlos.
Lo importante es si, aun después de aprenderlos, puedes tener la moderación de aplicarlos solo donde corresponde.
Hoy se suele invertir mucho esfuerzo en mover sistemas simples y que funcionan bien a un modelo distribuido más fuerte, tratándolo como si casi no tuviera costo.
Pero al ver el problema que se quería resolver y la escala, queda claro que en muchos casos una sola instancia de Postgres y un monolito habrían sido suficientes.
El consejo del texto original parece ir en ese sentido.
Al menos no necesariamente.
Yo seguiría aconsejando elegir una solución simple.
Muchos sistemas ni siquiera logran almacenar, respaldar y restaurar correctamente el estado persistente en “almacenamientos triviales y simples”.
Intentar restaurar el estado de un almacenamiento distribuido en un escenario de recuperación ante desastres es aún más difícil.
Primero hay que tener una solución de backups que funcione, y después se puede adoptar una solución distribuida.
Una configuración con un master y réplicas de solo lectura tampoco es lo que la gente normalmente llama “distribuido”, porque las escrituras no están distribuidas.
En la práctica, distribuido suele significar que los datos están shardeados, y eso es algo que definitivamente quieres evitar si no es realmente necesario.
Me pareció una lectura interesante porque recorre bien varios conceptos involucrados al crear una base de datos.
Cubre desde SIMD para exprimir rendimiento en una sola máquina hasta algoritmos de consenso.
Ya que se habla de bases de datos, confiabilidad y sistemas distribuidos, también vale la pena leer sobre métodos formales que pueden aplicarse a estas situaciones y a la implementación interna de bases de datos.
Hay un paper interesante del equipo de S3 modelado con TLA+.
[0] Use of Formal Methods at Amazon Web Services
https://lamport.azurewebsites.net/tla/formal-methods-amazon....
[1] How Amazon Web Services uses formal methods
https://www.amazon.science/publications/how-amazon-web-servi...
En consistencia existen la consistencia de base de datos y la consistencia de la aplicación.
Por ejemplo, a nivel de una sola tabla se pueden lograr atomicidad, aislamiento y durabilidad, pero las escrituras que abarcan varias tablas pueden fallar.
La consistencia se vuelve importante cuando empiezas a manejar transacciones que actualizan varias tablas a la vez.
Todas las tablas deben actualizarse al mismo tiempo, o no debe actualizarse ninguna.
Es muy genial un diseño que tenga “una API de documentos como MongoDB, replicación sin líder como Cassandra y una arquitectura de un thread por core como ScyllaDB”.
Y además está todo hecho en Rust.
La etapa de “los libros me despertaron la curiosidad, así que hice una pequeña base de datos por mi cuenta” parece ser algo por lo que muchos desarrolladores pasan al menos una vez.
No intentaría impedirlo. Al hacerlo uno mismo se aprende muchísimo sobre qué cosas no funcionan.
Si puedes hacerte el tiempo, es una lección extremadamente valiosa.
Haber creado una base de datos por mi cuenta fue lo que más aumentó mi respeto por las soluciones existentes.
Escribir y leer bytes rápidamente en disco no es la parte difícil.
Lo realmente difícil es hacer que funcione de forma estable durante años mientras soporta casos de uso que ni siquiera habías imaginado.
¿Qué eficiencias se podrían obtener si diseñáramos un DBMS especializado por dominio bajo la premisa de que se pueden prohibir e ignorar los casos de uso fuera de ese dominio?
Por ejemplo, hoy usamos bases de datos de propósito general incluso para datasets que, en esencia, son de solo anexado.
¿Cómo sería una base de datos en la que no existiera el concepto de actualizar o borrar filas existentes, y solo hubiera inserciones y eliminación completa de tablas/datasets?
¿Una base de datos así podría no implementar transacciones MVCC? ¿Podría evitar un write-ahead log separado porque cada tabla ya sería el write-ahead log? ¿Podría almacenar de forma más eficiente? ¿Podría reducir los bloqueos haciendo que la indexación tenga atomicidad por chunks en lugar de atomicidad a nivel de tabla completa?
¿La atomicidad de la versión en Bash no se puede lograr “fácilmente” copiando el archivo a un archivo temporal, modificándolo y luego usando
sync; mv; sync?grepinverso.Ya que estás copiando, también podrías garantizar el ordenamiento, pero hacer eso solo con “bash” y utilidades básicas no parece tener mucho sentido.
Para ese propósito existe CDB de DJB, es decir, cdbget, cdbmake, etc.:
https://cr.yp.to/cdb.html
Excelente artículo.
El libro Database Internals se ve bueno; ¿hay otros libros similares que profundicen en la implementación interna?
https://www.youtube.com/c/cmudatabasegroup
Tanto el curso introductorio como el avanzado están en línea, y también hay presentaciones y clases sobre productos de la industria.
Es muy útil.
Como material desde una perspectiva más de ciencias de la computación teórica de alto nivel y menos enfocada en la implementación física, el libro “Alice”, es decir, “Foundations of Databases”, es excelente.
Es muy denso y matemático, pero cubre álgebra relacional y Datalog, además de cómo convertir Datalog a álgebra relacional.
El libro en papel ya es difícil de conseguir, y el ejemplar usado que compré llegó con la encuadernación dañada y páginas sueltas, pero el libro completo está en línea: http://webdam.inria.fr/Alice/
https://dsf.berkeley.edu/papers/fntdb07-architecture.pdf
Aunque Database Internals es más moderno.
Me gusta que el artículo no mistifique las “bases de datos” y empiece mostrando una implementación trivial con una línea de Bash.
Es una gran introducción.