Backends y UX consistentes: ¿qué puede salir mal?
Serie de artículos
- ¿Por qué debería importarte?
- ¿Qué puede ser malo?
- ¿Cuáles son las barreras para la adopción?
- ¿Cómo ayudan los nuevos algoritmos?
- 1. Conferencias obsoletas
- 2. Escrituras perdidas
- 3. Escribe sesgado
- 4. Escrituras fuera de servicio
- Todas las anomalías pueden regresar cuando la coherencia está limitada
En el artículo anterior, explicamos qué es la coherencia fuerte (frente a la eventual). Este artículo es la segunda parte de una serie en la que explicamos cómo la falta de una coherencia sólida dificulta ofrecer una buena experiencia al usuario final, puede generar importantes gastos de ingeniería y abre la puerta a vulnerabilidades. Esta parte es más larga ya que explicaremos diferentes anomalías de bases de datos, analizaremos varios escenarios de ejemplo y resaltaremos brevemente qué tipo de base de datos sufre cada anomalía.
La experiencia del usuario es el factor determinante del éxito de cualquier aplicación, y depender de un backend inconsistente puede aumentar el desafío de ofrecer una buena experiencia. Más importante aún, crear una lógica de aplicación sobre datos inconsistentes puede generar vulnerabilidades. Un artículo llama a este tipo de ataques “ACIDrain”. Investigaron 12 de las aplicaciones de comercio electrónico autohospedadas más populares y se identificaron al menos 22 posibles ataques críticos. Un sitio web era un servicio de billetera Bitcoin que tuvo que cerrar debido a estos ataques. Cuando eliges una base de datos distribuida que no es 100% ACID, habrá dragones. Como se explica en uno de nuestros ejemplos anteriores, debido a malas interpretaciones, terminología mal definida y marketing agresivo, es muy difícil para un ingeniero determinar qué garantías ofrece una base de datos específica.
¿Qué dragones? Su aplicación puede presentar problemas como saldos de cuenta incorrectos, recompensas de usuario no recibidas, transacciones comerciales que se ejecutan dos veces, mensajes que aparecen fuera de orden o reglas de la aplicación que se violan. Para obtener una introducción rápida por qué las bases de datos distribuidas son necesarias y difíciles, consulte nuestro primer artículo o esta excelente explicación en video. En resumen, una base de datos distribuida es una base de datos que contiene copias de sus datos en múltiples ubicaciones por razones de escala, latencia y disponibilidad.
Analizaremos cuatro de estos problemas potenciales (hay más) y los ilustraremos con ejemplos del desarrollo de juegos. El desarrollo de juegos es complejo y los desarrolladores se enfrentan a muchos problemas que se parecen mucho a problemas graves de la vida real. Un juego tiene sistemas de intercambio, sistemas de mensajería, premios que requieren que se cumplan condiciones, etc. Recuerda lo enojados (o felices) que pueden estar los jugadores si las cosas van mal o parecen ir mal. En los juegos, la experiencia del usuario lo es todo, por lo que los desarrolladores de juegos suelen estar bajo una enorme presión para asegurarse de que sus sistemas sean tolerantes a fallos.
¿Listo? ¡Vamos a sumergirnos en el primer problema potencial!
1. Conferencias obsoletas
Las lecturas obsoletas son lecturas que devuelven datos antiguos o, en otras palabras, datos que devuelven valores que aún no se han actualizado según las últimas escrituras. Muchas bases de datos distribuidas, incluidas las bases de datos tradicionales que se amplían con réplicas (lea la Parte 1 para saber cómo funcionan), sufren de lecturas obsoletas.
Impacto en los usuarios finales
En primer lugar, las lecturas obsoletas pueden afectar a los usuarios finales. Y no es un solo impacto.
Experiencias frustrantes y ventajas injustas
Imagine un escenario en el que dos usuarios de un juego encuentran un cofre con oro. El primer usuario recibe los datos de un servidor de base de datos mientras que el segundo está conectado a un segundo servidor de base de datos. El orden de los acontecimientos es el siguiente:
- El usuario 1 (a través del servidor de base de datos 1) ve y abre el cofre, recupera el oro.
- El usuario 2 (a través del servidor de base de datos 2) ve un cofre lleno, lo abre y falla.
- El usuario 2 todavía ve un cofre lleno y no entiende por qué falla.
Aunque esto parece un problema menor, el resultado es una experiencia frustrante para el segundo jugador. No sólo tenía una desventaja, sino que a menudo verá situaciones en el juego en las que las cosas parecen estar ahí, pero no lo están. A continuación, veamos un ejemplo en el que el jugador actúa en una lectura obsoleta.
Lecturas obsoletas que conducen a escrituras duplicadas
Imagina una situación en la que un personaje del juego intenta comprar un escudo y una espada en una tienda. Si hay varias ubicaciones que contienen los datos y no existe un sistema inteligente que proporcione coherencia, entonces un nodo contendrá datos más antiguos que otro. En ese caso, el usuario puede comprar los artículos (que contacta con el primer nodo) y luego verificar su inventario (que contacta con el segundo nodo), solo para ver que no están allí. El probablemente estará confundido y podría pensar que la transacción no se realizó por el usuario. ¿Qué haría la mayoría de la gente en ese caso? Bueno, intenta comprar el artículo nuevamente. Una vez que el segundo nodo se pone al día, el usuario ya ha comprado un duplicado, y una vez que la réplica se pone al día, de repente ve que no le queda dinero y tiene dos artículos de cada uno. Se queda con la percepción de que nuestro juego está roto.
En este caso, el usuario ha gastado recursos que no quería gastar. Si escribimos un cliente de correo electrónico sobre dicha base de datos, un usuario podría intentar enviar un correo electrónico, luego actualizar el navegador y no poder recuperar el correo electrónico que acaba de enviar y, por lo tanto, enviarlo nuevamente. Ofrecer una buena experiencia de usuario e implementar transacciones seguras, como transacciones bancarias, además de un sistema de este tipo, es muy difícil.
Impacto en los desarrolladores
Al codificar, siempre hay que esperar que algo no esté allí (todavía) y codificar en consecuencia. Cuando las lecturas finalmente son consistentes, escribir código a prueba de fallas se vuelve muy desafiante y es probable que los usuarios encuentren problemas en su aplicación. Cuando las lecturas finalmente sean consistentes, estos problemas desaparecerán cuando pueda investigarlos. Básicamente, terminas persiguiendo fantasmas. Los desarrolladores todavía suelen elegir bases de datos o enfoques de distribución que eventualmente sean consistentes, ya que a menudo lleva tiempo notar los problemas. Luego, una vez que surgen los problemas en su aplicación, intentan ser creativos y crear soluciones (1, 2) sobre su base de datos tradicional para corregir las lecturas obsoletas. El hecho de que existan muchas guías como esta y que bases de datos como Cassandra hayan implementado algunas características de coherencia muestra que estos problemas son reales y causan problemas en los sistemas de producción con más frecuencia de lo que imagina. Las soluciones personalizadas sobre un sistema que no está diseñado para ser consistente son muy complejas y frágiles. ¿Por qué alguien pasaría por semejante problema si existen bases de datos que ofrecen una gran coherencia desde el primer momento?
Bases de datos que presentan esta anomalía
Las bases de datos tradicionales (PostgreSQL, MySQL, SQL Server, etc.) que utilizan replicación de lectura maestra generalmente sufren lecturas obsoletas. Muchas bases de datos distribuidas más nuevas también comenzaron siendo eventualmente consistentes, o en otras palabras, sin protección contra lecturas obsoletas. Esto se debió a la fuerte creencia en la comunidad de desarrolladores de que esto era necesario para escalar. La base de datos más famosa que comenzó así es Cassandra, pero Cassandra reconoció cómo sus usuarios luchaban para lidiar con esta anomalía y desde entonces ha proporcionado medidas adicionales para evitarlo. Las bases de datos más antiguas o las bases de datos que no están diseñadas para proporcionar una coherencia sólida de forma eficiente, como Cassandra, CouchDB y DynamoDB, son finalmente coherentes de forma predeterminada. Otros enfoques, como Riak, también son eventualmente consistentes, pero toman un camino diferente al implementar un sistema de resolución de conflictos para reducir las probabilidades de valores obsoletos. Sin embargo, esto no garantiza que sus datos estén seguros ya que la resolución de conflictos no es a prueba de errores.
2. Escrituras perdidas
En el ámbito de las bases de datos distribuidas, hay que tomar una decisión importante cuando las escrituras ocurren al mismo tiempo. Una opción (la segura) es asegurarse de que todos los nodos de la base de datos puedan ponerse de acuerdo sobre el orden de estas escrituras. Esto está lejos de ser trivial ya que requiere relojes sincronizados, para los cuales se necesita hardware específico, o un algoritmo inteligente como Calvin que no depende de relojes. La segunda opción, menos segura, es permitir que cada nodo escriba localmente y luego decida qué hacer con los conflictos más adelante. Las bases de datos que eligen la segunda opción pueden perder sus escrituras.
Impacto en los usuarios finales
Considere dos transacciones comerciales en un juego en el que comenzamos con 11 piezas de oro y compramos dos artículos. Primero, compramos una espada por 5 piezas de oro y luego compramos un escudo por cinco piezas de oro, y ambas transacciones se dirigen a diferentes nodos de nuestra base de datos distribuida. Cada nodo lee el valor, que en este caso sigue siendo 11 para ambos nodos. Ambos nodos decidirán escribir 6 como resultado (11-5) ya que no tienen conocimiento de ninguna replicación. Dado que la segunda transacción aún no pudo ver el valor de la primera escritura, el jugador termina comprando tanto la espada como el escudo por cinco piezas de oro en total en lugar de 10. ¡Bueno para el usuario, pero no tanto para el sistema! Para remediar este comportamiento, las bases de datos distribuidas tienen varias estrategias, algunas mejores que otras.
Las estrategias de resolución incluyen “victorias de la última escritura” (LWW) o victorias del “historial de versiones más largo” (LVH). LWW ha sido durante mucho tiempo la estrategia de Cassandra y sigue siendo el comportamiento predeterminado si no lo configura de manera diferente.
Si aplicamos la resolución de conflictos LWW a nuestro ejemplo anterior, el jugador aún se quedará con 6 de oro, pero solo habrá comprado un artículo. Esta es una mala experiencia para el usuario porque la aplicación confirmó la compra del segundo artículo, aunque la base de datos no lo reconoce como existente en su inventario.
Seguridad impredecible
Como se puede imaginar, no es seguro escribir reglas de seguridad sobre un sistema de este tipo. Muchas aplicaciones se basan en reglas de seguridad complejas en el backend (o directamente en la base de datos cuando sea posible) para determinar si un usuario puede o no acceder a un recurso. Cuando estas reglas se basan en datos obsoletos que se actualizan de manera poco confiable, ¿cómo podemos estar seguros de que nunca habrá una infracción? Imagine que un usuario de una aplicación PaaS llama a su administrador y le pregunta: "¿Podría hacer que este grupo público sea privado para que podamos reutilizarlo para datos internos?" El administrador aplica la acción y le dice que ya está hecho. Sin embargo, debido a que el administrador y el usuario pueden estar en nodos diferentes, el usuario puede comenzar a agregar datos confidenciales a un grupo que técnicamente sigue siendo público.
Impacto en los desarrolladores
Cuando se pierden las escrituras, depurar los problemas de los usuarios será una pesadilla. Imagine que un usuario informa que ha perdido datos en su aplicación y luego pasa un día antes de que tenga tiempo de responder. ¿Cómo intentará averiguar si el problema fue causado por su base de datos o por una lógica de aplicación defectuosa? En una base de datos que permite rastrear el historial de datos como FaunaDB o Datomic, podría viajar en el tiempo para ver cómo se manipularon los datos. Ninguno de estos es vulnerable a la pérdida de escrituras, y las bases de datos que sufren esta anomalía normalmente no tienen la función de viaje en el tiempo.
Bases de datos que sufren pérdidas de escritura
Todas las bases de datos que utilizan la resolución de conflictos en lugar de evitarlos perderán escrituras. Cassandra y DynamoDB utilizan las victorias de última escritura (LWW) de forma predeterminada; MongoDB solía usar LWW pero desde entonces lo ha abandonado. Los enfoques de distribución maestro-maestro en bases de datos tradicionales como MySQL ofrecen diferentes estrategias de resolución de conflictos. Muchas bases de datos distribuidas que no se crearon para lograr coherencia sufren pérdidas de escritura. La resolución de conflictos más simple de Riak está impulsada por LWW, pero también implementan sistemas más inteligentes. Pero incluso con sistemas inteligentes, a veces simplemente no existe una forma obvia de resolver un conflicto. Riak y CouchDB asignan la responsabilidad de elegir la escritura correcta al cliente o la aplicación, lo que les permite elegir manualmente qué versión conservar.
Dado que la distribución es compleja y la mayoría de las bases de datos utilizan algoritmos imperfectos, las escrituras perdidas son comunes en muchas bases de datos cuando los nodos fallan o cuando surgen particiones de red. Incluso MongoDB, que no distribuye escrituras (las escrituras van a un nodo), puede tener conflictos de escritura en el raro caso de que un nodo caiga inmediatamente después de una escritura.
3. Escribe sesgado
La desviación de escritura es algo que puede ocurrir en un tipo de garantía que los proveedores de bases de datos llaman coherencia de instantáneas. En coherencia con las instantáneas, la transacción lee una instantánea que se tomó en el momento en que comenzó la transacción. La coherencia de las instantáneas evita muchas anomalías. De hecho, muchos pensaban que era completamente seguro hasta que empezaron a aparecer documentos (PDF) que demostraban lo contrario. Por lo tanto, no sorprende que los desarrolladores tengan dificultades para comprender por qué ciertas garantías simplemente no son lo suficientemente buenas.
Antes de analizar lo que no funciona en la coherencia de las instantáneas, analicemos primero lo que sí funciona. Imaginemos que tenemos una batalla entre un caballero y un mago, cuyos respectivos poderes vitales constan de cuatro corazones.
Cuando cualquiera de los personajes es atacado, la transacción es una función que calcula cuántos corazones se han eliminado:
damageCharacter(character, damage) { character.hearts = character.hearts - damage character.dead = isCharacterDead(character)}
Y, tras cada ataque, isCharacterDead
también se ejecuta otra función para ver si al personaje le quedan corazones:
isCharacterDead(character) { if ( character.hearts = 0 ) { return true } else { return false }}
En una situación trivial, el golpe del caballero le quita tres corazones al mago, y luego el hechizo del mago le quita cuatro corazones al caballero, lo que hace que sus propios puntos de vida vuelvan a ser cuatro. Estas dos transacciones se comportarían correctamente en la mayoría de las bases de datos si una transacción se ejecuta tras la otra.
Pero ¿qué pasa si añadimos una tercera transacción, un ataque del caballero, que se ejecuta simultáneamente con el hechizo del mago?
¿Está muerto el caballero y vivo el mago?
Para hacer frente a esta confusión, los sistemas de coherencia de instantáneas suelen implementar una regla llamada "el primer autor que confirma gana". Una transacción solo puede concluir si otra transacción aún no se escribió en la misma fila; de lo contrario, se revertirá. En este ejemplo, dado que ambas transacciones intentaron escribir en la misma fila (la salud del mago), solo el hechizo Life Leech funcionaría y el segundo golpe del caballero se revertiría. El resultado final sería entonces el mismo que en el ejemplo anterior: un caballero muerto y un mago con el corazón lleno.
Sin embargo, algunas bases de datos como MySQL e InnoDB no consideran que "el primer confirmador gana" como parte de un aislamiento de instantáneas. En tales casos, perderíamos la escritura : el mago ahora está muerto, aunque debería haber recibido la salud de la sanguijuela de vida antes de que el golpe del caballero surtiera efecto. (Mencionamos terminología mal definida e interpretaciones vagas, ¿verdad?)
La coherencia de las instantáneas que incluye la regla de “el primer confirmador gana” maneja algunas cosas bien, lo cual no es sorprendente ya que se consideró una buena solución durante mucho tiempo. Este sigue siendo el enfoque de PostgreSQL, Oracle y SQL Server, pero todos tienen nombres diferentes. PostgreSQL llama a esta garantía "lectura repetible", Oracle la llama "serializable" (lo cual es incorrecto según nuestra definición) y SQL Server la llama "aislamiento de instantáneas". No es de extrañar que la gente se pierda en este bosque de terminología. ¡Veamos ejemplos en los que no se comporta como cabría esperar!
Impacto en los usuarios finales
La próxima pelea será entre dos ejércitos, y un ejército se considera muerto si todos sus personajes están muertos:
isArmyDead(army){ if (all characters are dead) { return true } else { return false }}
Después de cada ataque, la siguiente función determina si un personaje ha muerto y luego ejecuta la función anterior para ver si el ejército ha muerto:
damageArmyCharacter(army, character, damage){ character.hearts = character.hearts - damage character.dead = isCharacterDead(character) armyDead = isArmyDead(army) if (army.dead != armyDead){ army.dead = armyDead }}
Primero, el corazón del personaje queda disminuido con el daño que recibió. Luego, verificamos si el ejército está muerto comprobando si cada personaje se ha quedado sin corazones. Luego, si el estado del ejército ha cambiado, actualizamos el booleano "muerto" del ejército.
Hay tres magos, cada uno de los cuales ataca una vez, lo que resulta en tres transacciones de 'Life Leech'. Las instantáneas se toman al comienzo de las transacciones, ya que todas las transacciones comienzan al mismo tiempo, las instantáneas son idénticas. Cada transacción tiene una copia de los datos donde todos los caballeros aún tienen plena salud.
Echemos un vistazo a cómo se resuelve la primera transacción 'Life Leech'. En esta transacción, el mago1 ataca al caballero1 y el caballero pierde 4 puntos de vida mientras que el mago atacante recupera toda su salud. La transacción decide que el ejército de caballeros no está muerto ya que solo puede ver una instantánea en la que dos caballeros todavía tienen plena salud y un caballero está muerto. Las otras dos transacciones actúan sobre otro mago y caballero pero proceden de manera similar. Cada una de esas transacciones inicialmente tenía tres caballeros vivos en su copia de los datos y solo vio morir a un caballero. Por tanto, cada transacción decide que el ejército de caballeros sigue vivo.
Cuando finalizan todas las transacciones, ninguno de los caballeros sigue vivo, pero nuestro valor booleano que indica si el ejército está muerto sigue siendo falso. ¿Por qué? Porque en el momento en que se tomaron las instantáneas, ninguno de los caballeros estaba muerto. Entonces, en cada transacción su propio caballero moría, pero no tenía idea de los otros caballeros del ejército. Aunque esto es una anomalía en nuestro sistema (que se llama escritura sesgada), las escrituras se realizaron ya que cada una escribió a un personaje diferente y la escritura al ejército nunca cambió. ¡Genial, ahora tenemos un ejército fantasma!
Impacto en los desarrolladores
Calidad de datos
¿Qué pasa si queremos asegurarnos de que los usuarios tengan nombres únicos? Nuestra transacción para crear un usuario verificará si existe un nombre; si no es así, escribiremos un nuevo usuario con ese nombre. Sin embargo, si dos usuarios intentan registrarse con el mismo nombre, la instantánea no notará nada ya que los usuarios están escritos en filas diferentes y, por lo tanto, no entran en conflicto. Ahora tenemos dos usuarios con el mismo nombre en nuestro sistema.
Existen muchos otros ejemplos de anomalías que pueden ocurrir debido a una escritura sesgada. Si está interesado, el libro de Martin Kleppman "Diseño de aplicaciones intensivas en datos" describe más.
Codifique de manera diferente para evitar las reversiones
Ahora, consideremos un enfoque diferente en el que un ataque no esté dirigido a un personaje específico del ejército. En este caso, la base de datos se encarga de seleccionar qué caballero debe ser atacado primero.
damageArmy(army, damage){ character = getFirstHealthyCharacter(knight) character.hearts = character.hearts - damage character.dead = isCharacterDead(character) // ...}
Si ejecutamos varios ataques en paralelo como en nuestro ejemplo anterior, getFirstHealthyCharacter
siempre apuntarán al mismo caballero, lo que resultaría en múltiples transacciones que se escriben en la misma fila. Esto sería bloqueado por la regla de “el primero que se compromete gana”, que revertirá los otros dos ataques. Aunque previene una anomalía, el desarrollador debe comprender estos problemas y codificarlos de manera creativa. ¿Pero no sería más fácil si la base de datos hiciera esto por usted de forma inmediata?
Bases de datos que sufren de sesgo de escritura
Cualquier base de datos que proporcione aislamiento de instantáneas en lugar de serialización puede sufrir un sesgo de escritura. Para obtener una descripción general de las bases de datos y sus niveles de aislamiento, consulte este artículo.
4. Escrituras fuera de servicio
Para evitar escrituras perdidas y lecturas obsoletas, las bases de datos distribuidas apuntan a algo llamado " consistencia fuerte". Mencionamos que las bases de datos pueden optar por acordar un orden global (la opción segura) o decidir resolver conflictos (la opción que conduce a la pérdida de escrituras). Si nos decidimos por un orden global, significaría que aunque la espada y el escudo se compran en paralelo, el resultado final debería comportarse como si compráramos primero la espada y luego el escudo. A esto también se le suele llamar “linealización”, ya que puede linealizar las manipulaciones de la base de datos. La linealización es el estándar de oro para garantizar que sus datos estén seguros.
Diferentes proveedores ofrecen diferentes niveles de aislamiento, que puedes comparar aquí. Un término que vuelve a aparecer con frecuencia es serialización, que es una versión ligeramente menos estricta de coherencia fuerte (o linealización). La serialización ya es bastante sólida y cubre la mayoría de las anomalías, pero aún deja espacio para una anomalía muy sutil debido a las escrituras que se reordenan. En ese caso, la base de datos es libre de cambiar ese orden incluso después de que se haya confirmado la transacción. La linealización en términos simples es la serialización más un orden garantizado. Cuando a la base de datos le falta este orden garantizado, su aplicación es vulnerable a escrituras desordenadas.
Impacto en los usuarios finales
Reordenación de conversaciones
Las conversaciones se pueden ordenar de forma confusa si alguien envía un segundo mensaje por un error.
Reordenación de las acciones del usuario.
Si nuestro jugador tiene 11 monedas y simplemente compra artículos en orden de importancia sin verificar activamente la cantidad de monedas de oro que tiene, entonces la base de datos puede reordenar estas órdenes de compra. Si no tuviera suficiente dinero, podría haber comprado primero el artículo de menor importancia.
En este caso, hubo una verificación de la base de datos que verificó si tenemos suficiente oro. Imagínese que no tuviéramos suficiente dinero y nos costaría dinero dejar que la cuenta bajara de cero, tal como un banco le cobra tarifas por sobregiro cuando baja de cero. Podrías vender un artículo rápidamente para asegurarte de tener suficiente dinero para comprar los tres artículos. Sin embargo, la venta que estaba destinada a aumentar su saldo podría reordenarse al final de la lista de transacciones, lo que efectivamente llevaría su saldo por debajo de cero. Si fuera un banco, probablemente incurriría en cargos que definitivamente no merecía.
Seguridad impredecible
Después de configurar los ajustes de seguridad, el usuario esperará que estos ajustes se apliquen a todas las acciones futuras, pero pueden surgir problemas cuando los usuarios hablan entre sí a través de diferentes canales. Recuerde el ejemplo que comentamos en el que un administrador está hablando por teléfono con un usuario que quiere hacer que un grupo sea privado y luego le agrega datos confidenciales. Aunque el período de tiempo dentro del cual esto puede suceder se reduce en las bases de datos que ofrecen serialización, esta situación aún puede ocurrir ya que es posible que la acción del administrador no se complete hasta después de la acción del usuario. Cuando los usuarios se comunican a través de diferentes canales y esperan que la base de datos esté ordenada en tiempo real, las cosas van mal.
Esta anomalía también puede ocurrir si un usuario es redirigido a diferentes nodos debido al equilibrio de carga. En ese caso, dos manipulaciones consecutivas terminan en nodos diferentes y podrían reordenarse. Si una niña agrega a sus padres a un grupo de Facebook con derechos de visualización limitados y luego publica sus fotos de las vacaciones de primavera, las imágenes aún podrían terminar en los feeds de sus padres.
En otro ejemplo, un robot de comercio automático podría tener configuraciones como un precio máximo de compra, un límite de gasto y una lista de acciones en las que centrarse. Si un usuario cambia la lista de acciones que el robot debería comprar y luego el límite de gasto, no estará contento si estas transacciones se reordenan y el robot comercial ha gastado el presupuesto recién asignado en las acciones antiguas.
Impacto en los desarrolladores
Hazañas
Algunos exploits dependen de la posible reversión de transacciones. Imagine que un jugador recibe un trofeo tan pronto como posee 1000 de oro y realmente quiere ese trofeo. El juego calcula cuánto dinero tiene un jugador sumando el oro de varios contenedores, por ejemplo su almacenamiento y lo que lleva (su inventario). Si el jugador intercambia dinero rápidamente entre su almacenamiento y su inventario, puede engañar al sistema.
En la siguiente ilustración, un segundo jugador actúa como cómplice para asegurarse de que la transferencia de dinero entre el almacenamiento y el inventario se realice en diferentes transacciones, lo que aumenta la posibilidad de que estas transacciones se dirijan a diferentes nodos. Un ejemplo más grave de esto en el mundo real ocurre con los bancos que utilizan una tercera cuenta para transferir dinero; el banco podría calcular mal si alguien es elegible o no para un préstamo porque se enviaron varias transacciones a diferentes nodos y no tuvieron tiempo suficiente para ordenarlas.
Bases de datos que sufren escrituras desordenadas
Cualquier base de datos que no proporcione linealización puede sufrir un sesgo de escritura. Para obtener una descripción general de qué bases de datos proporcionan linealización, consulte este artículo. Spoiler: no son tantos.
Todas las anomalías pueden regresar cuando la coherencia está limitada
Una última relajación de la fuerte coherencia a discutir es garantizarla sólo dentro de ciertos límites. Los límites típicos son una región del centro de datos, una partición, un nodo, una colección o una fila. Si programa sobre una base de datos que impone este tipo de límites para lograr una fuerte coherencia, debe tenerlos en cuenta para evitar abrir accidentalmente la Caja de Pandora nuevamente.
A continuación se muestra un ejemplo de coherencia, pero solo garantizada dentro de una colección. El siguiente ejemplo contiene tres colecciones: una para los jugadores, otra para las herrerías (es decir, herreros que reparan los objetos de los jugadores) y otra para los objetos. Cada jugador y cada herrería tiene una lista de identificaciones que apuntan a elementos de la colección de elementos.
Si desea intercambiar el escudo entre dos jugadores (por ejemplo, de Brecht a Robert), entonces todo está bien, ya que permanece en una colección y, por lo tanto, su transacción permanece dentro de los límites donde se garantiza la coherencia. Sin embargo, ¿qué pasa si la espada de Robert está en la herrería para ser reparada y él quiere recuperarla? La transacción abarca entonces dos colecciones, la colección de la herrería y la colección del jugador, y las garantías se pierden. Estas limitaciones se encuentran a menudo en bases de datos de documentos como MongoDB. Luego se le pedirá que cambie la forma en que programa para encontrar soluciones creativas para superar las limitaciones. Por ejemplo, podría codificar la ubicación del artículo en el artículo mismo.
Por supuesto, los juegos reales son complejos. Es posible que desees poder dejar caer objetos al suelo o colocarlos en un mercado para que un jugador pueda poseer un objeto pero no tenga que estar en su inventario. Cuando las cosas se vuelven más complejas, estas soluciones aumentarán significativamente la profundidad técnica y cambiarán la forma de codificar para mantenerse dentro de las garantías de la base de datos.
Conclusión
Hemos visto diferentes ejemplos de problemas que pueden surgir cuando su base de datos no se comporta como cabría esperar. Aunque algunos casos pueden parecer insignificantes al principio, todos tienen un impacto significativo en la productividad del desarrollador, especialmente a medida que el sistema escala. Más importante aún, lo exponen a vulnerabilidades de seguridad impredecibles, que pueden causar daños irreparables a la reputación de su aplicación.
Discutimos algunos grados de coherencia, pero juntémoslos ahora que hemos visto estos ejemplos:
Lecturas obsoletas | escrituras perdidas | escribir sesgado | Escribe fuera de servicio | |
---|---|---|---|---|
Linealización | seguro | seguro | seguro | seguro |
Serializabilidad | seguro | seguro | seguro | inseguro |
Consistencia de instantáneas | seguro | seguro | inseguro | inseguro |
Consistencia eventual | inseguro | inseguro | inseguro | inseguro |
Recuerde también que cada una de estas garantías de corrección puede tener límites:
Límites a nivel de fila | Las garantías entregadas por la base de datos solo se cumplen cuando la transacción lee/escribe en una fila. Manipulaciones como mover elementos de un jugador a otro pueden causar problemas. HBase es una base de datos de ejemplo que limita las garantías a una fila. |
Límites a nivel de colección | Las garantías entregadas por la base de datos solo se cumplen cuando la transacción lee/escribe en una colección. Por ejemplo, el intercambio de artículos entre dos jugadores permanece dentro de una colección de “jugadores”, pero el intercambio entre un jugador y una entidad de otra colección, como un mercado, abre nuevamente la puerta a anomalías. Firebase es un ejemplo que limita las garantías de corrección a las colecciones. |
Límites de fragmento/réplica/partición/sesión | Siempre que una transacción solo afecte los datos de una máquina o fragmento, las garantías se man
Subir
|
Deja un comentario