Bases, Diseño de software
     

Consistencia, precisión y repetibilidad eventuales en la sincronización de datos

Construir sistemas distribuidos y escalables es complejo y, aún más común, gracias al creciente número de IoT y dispositivos móviles. Un problema que veo rutinariamente es que los desarrolladores intentan tratar los sistemas distribuidos como sistemas monolíticos. Específicamente, los desarrolladores que se aferran a patrones como ACID.

ACID, abreviatura de atomicidad, consistencia, aislamiento y durabilidad, representa un pilar fundamental de las bases de datos relacionales (RDMS). Para muchas aplicaciones, los comportamientos descritos por ACID y aplicados por RDMS son exactamente lo que se desea. Nadie quiere condiciones de carrera como lecturas sucias. Sin embargo, está leyendo esto porque presumiblemente está interesado en sistemas distribuidos – en resumen, no está creando la aplicación típica, y ciertamente no los tipos de aplicaciones previstas en la década de 1960 cuando Codd et. puso las bases para los RDMS modernos.

picture of Saturn, God of time
Tal vez Saturno, dios del tiempo, sabe algunas cosas sobre la consistencia final?

La mayoría de los elementos de un diseño clásico de base de datos giran en torno al concepto de una autoridad central (normalmente el propio RDMS) manteniendo todas las lecturas y escrituras. Esto funciona bien cuando todos los clientes pueden mantener una conexión con la autoridad central (por ejemplo, una conexión de base de datos) en cualquier momento que desee realizar una escritura. Siempre que pueda cumplir este requisito y proporcionar puede escalar este cuello de botella singular a medida que crece el número de cliente y el uso, entonces esta no es una solución incorrecta. De hecho, es una gran solución, ya que la rigidez de ACID existe por una razón, y los RDMS protegen contra todo tipo de escenarios de datos deficientes.

Claramente no está construyendo algo que esté “siempre conectado” y, por lo tanto, no se puede lograr una ACIDity estricta. Entonces, ¿qué?

Para responder a esta pregunta, retrocedamos y preguntemos por qué lo que estamos tratando de lograr y por extensión lo que nos importa. Para muchos sistemas esto se puede articular mediante la simple declaración:

“Quiero asegurarme de que las actualizaciones se procesan correctamente, y si surge un conflicto que la aplicación hace lo correcto.”

Está bien, bastante bien. Estos requisitos definen claramente el punto de vista del usuario, pero obviamente dejan un poco de margen para la interpretación – vamos a ejecutar con esto y evaluar las ambiguedades a medida que surgen.

Requisito 1: “asegúrese de que las actualizaciones se procesan correctamente.”

Suponiendo que “correctamente” significa “cuando llega una actualización, procesarla igual que si el cliente estuviera siempre conectado” entonces podemos lograr esto con bastante facilidad. Simplemente haga que el cliente sin conexión haga que sus solicitudes de actualización, y cuando logren un estado en línea, simplemente transmita al servidor, iterando sobre ellas en orden cronológico, procesando cada una. Siempre que no haya conflictos, esto es bastante simple – sin embargo, como veremos, ahí está el roce.

Requisito 2: “si surge un conflicto, haga lo correcto”

Como probablemente has supuesto, aquí es donde las cosas se complican. Los conflictos vienen en muchas formas, por lo que definiremos cada uno a su vez, y luego discutiremos soluciones generales para todos estos problemas.

Múltiples actualizaciones

Si varios clientes intentan actualizar el mismo registro, ¿qué hace? ¿Qué sucede si actualizan el mismo campo o, quizás, campos diferentes para el mismo registro?

Campos derivados/calculados

Muchas aplicaciones renuncian a la normalización estricta del esquema de base de datos para las optimizaciones de rendimiento, y a veces esto adopta la forma de campos calculados. La suma de los registros secundarios, el estado de un objeto (por ejemplo, estados de máquina de estado), etc. ¿Qué sucede cuando las actualizaciones en conflicto (véase arriba) también tienen un efecto en lo que de otro modo es un campo calculado?

Canceladuras

Lo ideal es evitar eliminaciones rígidas e implementar algún tipo de eliminación lógica, pero ¿cómo se mantienen esos patrones con los conflictos?

¿Cómo procedemos?

Con el desglose anterior de los tipos de conflicto fuera del camino, vamos a revisar cómo podemos superar esto en un sistema desconectado.

Afortunadamente existe una solución fácil debido a la similitud de cada uno de estos tipos de conflictos y, para el caso, a todos los conflictos. Todos los conflictos son esencialmente errores de temporización. Se suponía que la cosa A precedía a la cosa B, pero debido a la vida, el universo y todo lo que obtenemos B antes que A. Lo sabes porque eres un humano. Puede visualizar estos escenarios.

Por ejemplo, un escenario que involucra a un proveedor de almacenamiento de líquidos, que utiliza una aplicación basada en tabletas para registrar auditorías diarias de todos sus tanques de almacenamiento, incluidas las mediciones de seguimiento. Imagínese entonces, si:

  1. Fred mide una toma de almacenamiento antes de recibir el envío diario, y había registrado la medición diaria en 47 litros. Fred, sin embargo, está fuera de línea durante este tiempo debido a la falta de conectividad a Internet adecuada en la instalación de almacenamiento.
  2. Alice, sentada en la oficina, actualizando el nivel de fluido del mismo tanque de almacenamiento de los días anteriores leyendo de 50 litros a 150 litros porque acaba de recibir una notificación de un técnico de campo de que su entrega diaria fue recibida y el conductor de entrega notificó a Alice de la lectura correcta después de rematar el tanque.
  3. Más tarde en el día, Fred finalmente se conecta a Internet, y sus lecturas previamente grabadas pueden ser transmitidas.

En este escenario, a pesar de que la transmisión de Fred ocurre después de la actualización de Alice, nuestra intuición humana es que la actualización de Fred debe ser ignorada. Tal vez mantenido, en caso de una auditoría, o un jefe demasiado exigente preguntándose por qué Fred nunca está presentando sus lecturas para este tanque, pero ciertamente no tenemos la expectativa de que las lecturas de Fred deben reemplazar a la que Alice insumó en el sistema.

Esto se debe a que las actualizaciones de Fred ocurrieron cronológicamente antes de las registradas por Alice, y ahí está nuestra solución. Nuestro sistema debe preservar y respetar el orden cronológico que deberían haberse producido las actualizaciones, no necesariamente cuando fueron recibidas por el servidor central. De hecho, el tiempo que recibe una actualización un servidor es más un artefacto técnico, aunque curioso, pero ciertamente no es de interés para los usuarios medios del sistema.

Para corregir esto, necesitamos agregar otro campo a cada modelo en el que tenemos la intención de permitir actualizaciones sin conexión. Este campo debe realizar un seguimiento de la marca de tiempo precisa que el usuario indicó que deseaba que se produjera una acción. Llamemos a esto la “fecha de envío”.

Al revisar nuestro ejemplo anterior, la fecha de envío de la actualización de Fred podría ser a las 9 de la mañana del 1 de noviembre, mientras que la de Alice podría haber sido a las 11:45 de la mañana del mismo día. Si hubiéramos capturado esas marcas de tiempo, cuando llega el registro de Fred, podemos ver claramente que está enviando un valor que estaba destinado a llegar mucho antes. De hecho, incluso podemos ver que tenemos un registro más nuevo, y podemos optar por ignorar su actualización.

En este punto se puede ver la importancia de esta marca de tiempo adicional. Nuestro sistema está acumulando una gran cantidad de marca de tiempo, hemos actualizado y creado fechas para ayudar a facilitar la sincronización hacia abajo, y ahora hemos añadido una fecha de envío para facilitar la sincronización. Esto es simplemente porque, durante todos los tipos de sincronización, el problema más grande es el de una incoherencia cronológica – errores de temporización. Podemos prevenir esto manteniendo una buena contabilidad, y ya que el tema de nuestra preocupación es el tiempo, entonces eso significa que simplemente estamos manteniendo una gran cantidad de marcas de tiempo como contabilidad para asegurar el flujo adecuado de datos.

¿Qué pasa con las eliminaciones?

Ya hemos configurado los conceptos básicos para controlar las eliminaciones. Usando nuestra marca de tiempo de fecha de envío recién agregada, ahora podemos ignorar fácilmente las actualizaciones que ocurren para los registros de eliminación… si el tiempo es correcto. Verás, a pesar de haber añadido una nueva marca de tiempo, hemos pasado por alto otro problema de sincronización. Las eliminaciones les traen al frente y al centro, porque las eliminaciones son en realidad eliminaciones lógicas (¿no es difícil eliminar registros, verdad?) y, como tal, implican una actualización de una columna.

De hecho, el conflicto de una eliminación que se produce antes/después de otra actualización es simplemente un caso especial de dos actualizaciones que se producen, pero en diferentes campos del mismo modelo.

Dicho de otra manera, si podemos resolver el escenario de Alice actualizando la lectura diaria de fluidos del tanque, simultáneamente a una actualización fuera de línea de Fred tratando de actualizar la lectura de presión del mismo tanque, entonces también podemos resolver eliminaciones. Echemos un vistazo a estos escenarios más de cerca y veamos qué nos estamos perdiendo y por qué.

  1. Fred visita el sitio de almacenamiento 4, y registra una lectura de presión diaria para el tanque de almacenamiento B – digamos 60 PSI. Fred está fuera de línea, sin embargo, por lo que sus actualizaciones no serán transmitidas todavía.
  2. Alice recibe una llamada informándola de la recepción de líquidos adicionales para el tanque B en el sitio 4. Ella está en línea, e inmediatamente actualiza la lectura fluida a 150 litros.
  3. Fred finalmente obtiene conductividad de Internet, y transmite sus lecturas de presión de antes del día.

Mucho de esto se reduce a cómo estructuramos nuestros datos, vamos a hacer esto aún más concreto. Suponiendo que nuestro esquema se ve así:

Para cuando lleguemos al paso 3, tendríamos registros que se verían así:

Si nuestro sistema, señalando que es una marca de tiempo anterior, simplemente ignoró la presentación de Fred, entonces habríamos tirado datos ya que sólo el registro de Fred contiene la lectura de presión. ¿Qué hacer?

Valor de atributo de entidad al rescate

Por suerte para nosotros existe un patrón para resolver este problema exacto y se llama el patrón de valor de atributo de entidad. El patrón EAV es bastante simple, y una vez integrado puede simplificar en gran medida las complejidades alrededor de las sincronizaciones.

En su núcleo, EAV dice que (E)Las entidades (por lo que la representación de una lectura para un tanque de fluidos) que contienen múltiples (A)Atributos (el nivel de fluido y la lectura de presión de nuestro tanque) deben representarse por valores separados (V) cuando se registran. En resumen, debemos revisar nuestro esquema a que un registro dice “el nivel de fluido es de 150 litros” y otro dice “la presión es de 60 psi”, pero ninguno dice ambas. ¿Confundido? Echemos un vistazo a cómo el esquema anterior podría verse bajo EAV e ir desde allí:

En este esquema revisado estamos usando el campo de “lectura” para servir a un propósito dual. A veces una lectura es una presión, otras veces es un volumen. ¿Cómo diferenciar los dos? Para eso es el campo de tipo de lectura. En este ejemplo, el tipo 1 representa la lectura de presión y el tipo 2 representa el nivel de fluido. Estos tipos son totalmente específicos de la aplicación, así que no se deje en eso, sino que observe cómo en este esquema revisado hemos pivotado esencialmente nuestro registro, y permitir la preservación de la actualización de Alice y Fred sin ningún conflicto lo que-tan-nunca.

De hecho, si usamos este esquema y volvemos a nuestro ejemplo anterior de Fred también registrando el nivel de fluido, obtenemos algo muy interesante:

Aquí no solo hemos utilizado nuestro nuevo esquema, sino que también permitimos que la lectura de presión anterior de Fred se almacene en la base de datos. Puesto que estamos usando el patrón EAV, y tenemos una submission_date marca de tiempo adecuada, cuando consultamos la lectura correcta/actual del fluido, podemos ver claramente que la lectura fluida de Fred ocurrió antes e ignorarla. Sin embargo, todavía lo registramos con fines de auditoría. También, y tal vez lo más importante, es más fácil. Cada vez que estamos actualizando selectivamente los datos corremos el riesgo de errores. Sin embargo, si escribimos todo en lo que es esencialmente un registro de auditoría gigante, podemos (en el momento de la consulta) resolver los conflictos en tiempo real – cada registro recién guardado resuelve retroactivamente todos los conflictos anteriores.

Vale la pena discutirlo un poco más.

Si bien nuestros ejemplos hasta ahora han sido bastante simplistas, las actualizaciones de tiempo pueden ser bastante complicadas. Tira los dados lo suficiente, y puedes tener múltiples actualizaciones de la misma persona, algunas en línea, algunas fuera de línea, otras actualizaciones de diferentes usuarios, etc., todas ocurriendo en paralelo. Podría, en cada registro recién recibido evaluar todos los registros existentes en el sistema y determinar si debe insertar o actualizar esa fila, pero qué sucede si, como ha evaluado si se debe registrar la actualización de Alice, llega otra actualización de un usuario diferente (por ejemplo Bob). Si está ejecutando varios servidores web, como casi cualquier aplicación web moderna, entonces la lógica que está evaluando si el registro de Alice debe guardarse se ejecuta simultáneamente en Bob, y ambos de ignorante de la otra – más conflicto!

En su lugar, el patrón anterior permite que la actualización de Alice y Bob se escriba en la base de datos, y cuando cualquier otra persona lee los datos, solo se utiliza el registro con la marca de tiempo más reciente (según lo determinado por la fecha de envío).

 

About Jason

Jason es un experimentado emprendedor y desarrollador de software experto en liderazgo, desarrollo móvil, sincronización de datos y arquitectura SaaS. Obtuvo su Licenciatura en Ciencias de la Computación de la Universidad Estatal de Arkansas.
View all posts by Jason →

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *