inro

Arquitectura

Escrito a partir de la especificación de diseño del motor. El motor se está implementando; esto describe el diseño, no una API publicada.

Un núcleo clave-valor ordenado

El motor expone claves y valores como secuencias de bytes opacas, ordenadas lexicográficamente. Las tablas, los documentos y los espacios de nombres se construyen encima con prefijos de clave, sin coste para el motor.

Los índices secundarios, sin embargo, sí viven en el motor. En disco cuestan lo mismo que mantenerlos a mano en el código que los llama — la diferencia es el catálogo, una página por base de datos. Lo que eso compra es la garantía de que un índice y sus datos se actualizan dentro de la misma transacción y no pueden desviarse el uno del otro.

B+tree copy-on-write con una lista libre

Las páginas modificadas se escriben en slots libres, nunca sobre datos vivos. Un commit es una única escritura de página meta. La ocupación es estable, entre 1,2× y 2× los datos vivos en el peor caso. Sin compactación, sin hilos en segundo plano, sin fichero auxiliar, RAM constante.

Los lectores sin bloqueo salen de ahí gratis: un lector fija una página meta, y el árbol que cuelga de ella es inmutable mientras la retiene. No hay nada más que sincronizar.

file1. read page2. write copy into a free slot3. commit = new metapagecopymeta
Un commit libera la página antigua en lugar de borrarla; vuelve a la lista libre para reutilizarse.

Un escritor, lectores sin bloqueo, fsync configurable

Un escritor por base de datos, retenido por un mutex dentro del proceso. Los lectores nunca bloquean ni son bloqueados.

Sync::Commit es el valor por defecto: un fsync de las páginas y uno de la página meta por commit. Un modo relajado, Sync::Interval(d), agrupa la sincronización para IoT, donde sincronizar en cada escritura desgasta la memoria flash. El modo relajado se mantiene seguro precisamente por el copy-on-write: un corte de energía descarta los últimos commits sin sincronizar, pero nunca corrompe, porque los datos vivos nunca se sobrescriben.

Un proceso, multihilo

Solo un proceso puede abrir un fichero para escritura o lectura. Eso elimina la tabla de lectores en memoria compartida que necesita LMDB, junto con su fichero de bloqueo y la limpieza de lectores muertos tras un apagado no limpio — la tabla de lectores se convierte en un contador dentro del proceso. flock sobre el propio fichero se mantiene solo como red de seguridad contra abrir la misma base de datos dos veces por accidente.

La lista libre

Las páginas que libera una transacción no se pueden reutilizar mientras exista un lector de una instantánea anterior — para ese lector, siguen siendo datos vivos. Van a una lista pendiente etiquetada con el txn_id que las liberó, y pasan a la lista libre en cuanto el lector activo más antiguo sobrepasa ese id. Como el acceso es de un solo proceso, el registro de lectores activos es solo una estructura en RAM: un contador por txn_id con lectores vivos.

Las páginas de la lista libre almacenan lotes de números de página más un puntero al siguiente nodo, así que liberar mil páginas no cuesta mil escrituras. La lista en sí es una cola, no una estructura en memoria: una lista enlazada de páginas con la cabeza en el extremo más antiguo — de donde se toman las páginas para reutilizarlas — y la cola en el extremo más reciente, donde se añaden. Solo esas dos páginas viven en RAM durante una transacción, y cada una se escribe una vez, en el commit.

La alternativa — mantener la lista completa en RAM y serializarla entera en cada commit — sería más simple, pero costaría 4 bytes de RAM por página libre: decenas de kilobytes en un nodo pro, lo que come del presupuesto de RAM que es el objetivo del proyecto. La complejidad se paga por esa razón.

Un lector que mantiene su instantánea abierta indefinidamente bloquea el reciclaje de cada página liberada desde entonces, y el fichero crece — la misma limitación que tiene LMDB. El motor expone una métrica del txn_id retenido más antiguo para que esto se pueda diagnosticar.