saltar al contenido

Actualizaciones por copia en escritura

Introducción

La “copia en escritura” es una técnica que se utiliza en una amplia variedad de aplicaciones en informática. RDM utiliza esta técnica en un par de áreas. Antes de sumergirnos en RDM, echemos un vistazo al caso general.

 

La idea principal detrás de la copia en escritura es copiar parte del contenido y luego modificar la copia en lugar de modificar el original al escribir en la memoria, un archivo o un dispositivo de bloque. De ahí proviene el término copy-on-write. Algunas aplicaciones son de copia en escritura en la programación funcional en combinación con el "recuento de referencias". En Unix, la copia en escritura se usa para compartir la memoria virtual en la implementación de la llamada al sistema de la bifurcación con algo de ayuda del hardware. El sistema de archivos Btrfs en Linux utiliza copia en escritura para permitir un manejo eficiente de transacciones y aislamiento de instantáneas. Esta técnica también la utilizan los hipervisores y las implementaciones de máquinas virtuales para una gestión eficiente de la memoria.

 

En RDM, utilizamos esta técnica para transacciones anidadas. Sin embargo, este artículo se centra en el formato de archivo de base de datos implementado en RDM 15.0. Primero, echemos un vistazo a las versiones anteriores de RDM.


Versiones anteriores de RDM

RDMe 12.0 y anteriores ("RDMe") usaban un registro de transacciones. Esto significaba que los datos se escribían normalmente dos veces: primero en el registro de transacciones y luego en los archivos reales de la base de datos. RDMe también almacenaba elementos con una longitud fija y, en algunos casos, el contenido adicional tenía que almacenarse por separado en otro lugar. Este diseño resultó en un desperdicio de espacio, incurrió en una sobrecarga adicional cuando el contenido tuvo que dividirse y, en general, no fue muy flexible. Debido a los registros de longitud fija, tampoco fue útil realizar ningún tipo de compresión en los datos.

 

Para mitigar estos problemas, RDM 14.0 implementó registros de tamaño variable con actualizaciones mediante "copiar al escribir". Los archivos de la base de datos se dividieron en lo que denominamos "archivos de paquete". Una actualización del paquete siempre se realizaba haciendo "copia en escritura". Sin embargo, RDM 14.0 no realizó una "copia al escribir" recursiva completa. En cambio, se basó en un almacén de valores clave (índice de ID) cada vez que el contenido se movía de una ubicación en el paquete a otra ubicación.

 

Este diseño tuvo sus propios problemas. El índice de identificación mantuvo un registro del espacio no utilizado. Este seguimiento y las estructuras de datos necesarias para utilizar de manera eficiente el espacio no utilizado resultaron ser bastante costosos. También evitó la reutilización del espacio en combinación con la escritura masiva. El rendimiento de E / S tampoco fue óptimo, ya que los datos se escribieron en páginas cuando solo se actualizó una fracción de la página. 

 

Este diseño también necesitaba un registro de transacciones para el índice de identificación, que se integró en el paquete para RDM 14.1. Vaciar el índice de identificación también fue relativamente costoso, lo que resultó en una apertura y cierre de la base de datos potencialmente lentos. Estos problemas hicieron que este diseño no fuera adecuado para aplicaciones como el REST-API, que se basa en abrir y cerrar la base de datos para cada solicitud.

 

RDM 14.2 adoptó un enfoque ligeramente diferente al RDM 14.1. En la siguiente sección, discutiremos principalmente el diseño 15.0 que comparte muchas características con el diseño 14.2, así como algunas características con el diseño 14.1.


RDM 15.0

Dos principios

RDM 15.0 se basa en dos principios.

 

El primer principio en el que confiamos es la "copia en escritura" recursiva completa. Esto significa que la estructura de datos persistentes utilizada en RDM 15.0 es un árbol (o un árbol de árboles según el nivel de abstracción) sin el uso de un índice de identificación. Se utilizan dos estructuras de árbol: árboles B y árboles R. La base de datos tendrá un árbol para cada clave definida en el esquema, pero también se necesitan otros árboles. Los detalles de esto están más allá del alcance de este artículo. Para concluir esta discusión, mencionamos que para unir todos estos árboles, también hay un árbol principal que tiene referencias a los nodos de raíz de los otros árboles.

 

El segundo principio en el que confiamos es siempre agregar al final del último archivo del paquete.

Por lo tanto, los archivos de paquete nunca se actualizan en el medio. Esto significa que se evitan las costosas actualizaciones intermedias donde solo se actualiza una pequeña fracción de una página, y tampoco hay necesidad de administrar el espacio libre. El espacio al que ya no se hace referencia simplemente termina sin ser accedido.


Agregar y escribir cambios "de abajo hacia arriba"

Hay bastantes implicaciones que se derivan de estos dos principios. Una implicación que se sigue naturalmente es que todas las actualizaciones deben realizarse agregándolas al final del paquete. RDM confirma una transacción escribiendo primero elementos nuevos y actualizados que no tienen referencias directas o indirectas a otros elementos actualizados, seguidos de los nodos de árbol que hacen referencia a ellos, sus nodos de árbol padre, sus nodos de árbol abuelo, y así sucesivamente hasta el nodo raíz de se ha escrito el árbol B principal.

 

Este diseño significa que habrá nodos adicionales que se deben escribir aunque, de lo contrario, no hubo cambios en ellos. Por lo general, esto no es un problema para las transacciones grandes, donde es más probable que ocurran varias actualizaciones en el mismo nodo del árbol B. Por otro lado, los gastos generales adicionales pueden ser sustanciales para transacciones pequeñas con solo unos pocos cambios. Se puede esperar el mejor rendimiento para casos de uso que involucran series de tiempo o mesas circulares.


Instantánea

Con "copia al escribir" recursiva, instantáneas se puede implementar de manera muy eficiente. Cuando se solicita una instantánea, el cliente que solicita la instantánea puede utilizar el nodo raíz actual del árbol B principal y su vista de los datos se deriva de lo que se puede alcanzar desde este nodo. La única diferencia entre una instantánea y una lectura usando bloqueos de lectura es que un cliente que solicita un bloqueo de lectura bloqueará las actualizaciones de las tablas en las que se solicitan los bloqueos de lectura, mientras que una instantánea no lo hará. No se necesitan otras costosas estructuras de datos internas adicionales para manejar la instantánea. El cliente que solicita la instantánea puede simplemente manejar las solicitudes de contenido del Servidor de archivos transaccionales (TFS) exactamente como manejaría una lectura normal con bloqueos. Obtiene un nodo raíz principal de árbol B y, basándose en las referencias que contiene, solicita otros nodos del TFS.

 

Como resultado, el formato de archivo de paquete que utiliza copia en escritura es muy adecuado para instantáneas. Cuando se solicita una instantánea, se puede hacer referencia a datos antiguos en el paquete. Desde el punto de vista de la implementación del archivo de paquete, solo necesitamos asegurarnos de que los archivos de paquete que tienen datos visibles en una instantánea no se eliminen.


Durabilidad

Dado que los datos solo se añaden al final del último archivo de paquete, cuyo formato de archivo se ha diseñado cuidadosamente, la durabilidad se puede lograr en la mayoría de los casos simplemente sincronizando el último archivo de paquete. Esto tiene algunos beneficios de rendimiento en comparación con otras bases de datos que necesitan sincronizar varios archivos, incluidas las versiones anteriores de RDM. 

 

Resulta que la mayoría de las sincronizaciones se pueden omitir si no se requiere durabilidad. En este caso, una falla del sistema puede causar cierta pérdida de datos. Sin embargo, dado que solo estamos agregando a un archivo, si se encuentra un nodo raíz principal del árbol B, todos los datos a los que se hace referencia desde este nodo raíz deberían ser válidos. Esto supone una implementación del sistema de archivos que no pierde bloques en medio de un archivo cuando solo se agrega a ese archivo. Hay una precaución que RDM debe tomar para que esto funcione: cada vez que se solicita un nuevo archivo de paquete, el archivo de paquete anterior debe sincronizarse (y en Linux y Unix, también tenemos que sincronizar el directorio). Esto para asegurarse de que no se pierda ninguna referencia de un archivo de paquete a un archivo de paquete anterior.

 

También hemos implementado un modo inseguro, en el que no sincronizamos los archivos del paquete en absoluto. Este modo solo debe usarse si la pérdida de datos en la base de datos no es un problema o si se puede garantizar que el sistema operativo no se apaga o falla sin sincronizar los archivos. Está bien que la base de datos se bloquee ya que el contenido escrito estará en la memoria caché del sistema de archivos. Tenga en cuenta que este puede no ser el caso con algunos sistemas integrados, donde el sistema de archivos se implementa en el espacio del usuario.

 

Para la recuperación, el formato del archivo del paquete se ha diseñado para que sea posible leer el archivo del paquete tanto hacia adelante como hacia atrás. Por cada 64 KB, hay información de recuperación especial que permite escanear el archivo hacia adelante y hacia atrás sin tener que leer todo el archivo desde el principio. Esta propiedad es importante para una recuperación eficiente y confiable. La recuperación no escribe nada en el paquete, sino que encuentra el último nodo raíz del árbol B principal y verifica el final del paquete. Si se encuentra que una base de datos se encuentra en cierto estado incompleto, se creará un nuevo archivo de paquete. Esto simplemente significa que las fallas pueden dejar contenido en los archivos del paquete que no son referenciados directa o indirectamente por el nodo raíz del árbol B principal.

 

El formato de archivo del paquete permite que múltiples transacciones de actualización estén activas en paralelo, pero cuando se confirman, la adición al archivo del paquete se alternará entre las que se están confirmando actualmente. Una transacción pequeña puede agregar todos sus datos en un solo fragmento, mientras que las transacciones más grandes necesitarán turnarse para escribir sus datos. El árbol B principal debe escribirse exclusivamente y cada vez que se hace, los cambios de otras transacciones de actualización o el aspirador (un recolector de basura, cubrimos más adelante) deben integrarse con los cambios para la transacción en cuestión.


Almacenamiento en caché en tiempo de ejecución

El diseño antiguo con el índice de ID, así como las implementaciones basadas en elementos de tamaño fijo, requerían que los datos de una tabla determinada que estaba en la caché de tiempo de ejecución se depuraran cada vez que teníamos otro cliente realizando actualizaciones en la misma tabla. Con el diseño RDM 15.0, no necesitamos purgar ningún dato. La caché en tiempo de ejecución siempre busca datos utilizando la dirección del paquete y, por lo tanto, se garantiza que encontrará los datos correctos si se almacenan en caché. Esto significa que los datos pueden permanecer en la caché en tiempo de ejecución durante más tiempo, aumentando potencialmente el rendimiento.


Paralelismo y lectura y escritura directa

El diseño anterior con el índice ID no permitía que las operaciones se ejecutaran en paralelo. El diseño RDM 15.0 que utiliza copia en escritura permite que la mayoría de las operaciones de diferentes clientes se ejecuten en paralelo. El formato de archivo incluso permite que la mayoría de las operaciones de archivo se realicen en paralelo desde diferentes procesos en la misma máquina leyendo y escribiendo en los archivos del paquete directamente sin comunicarse con el TFS. Este enfoque produce un mayor rendimiento.


Pasar la aspiradora

Si bien el diseño RDM 15.0 tiene muchas ventajas, como se describió anteriormente, tiene un inconveniente importante. Como solo agregamos al final del paquete, crecerá indefinidamente como resultado. Para solucionar este problema, ejecutamos un proceso de recolección de basura denominado "aspirar". Pasar la aspiradora se discutirá en un artículo separado.


Conclusión

Hemos visto que el formato de archivo para RDM 15.0 es flexible y muy adecuado para el aislamiento de instantáneas, el paralelismo, la poca huella de memoria y la recuperación rápida. Le sugerimos que continúe leyendo sobre aislamiento de instantáneas y Pasar la aspiradora. ¡Disfrutar! 

 

por Sverre Hvammen Johansen y Daigoro Toyama