Cabecera
64 bytes en el offset 0, con la identidad y la geometría del fichero. Se escribe una vez, en la creación, y nunca se reescribe — esa inmutabilidad es la razón de mantenerla separada de las páginas meta: si el puntero raíz viviera en la cabecera, cada commit la reescribiría, y un corte de energía a mitad de escritura podría dejar un fichero cuya geometría ya no se puede leer.
| Offset | Bytes | Campo | Propósito |
|---|---|---|---|
| 0 | 8 | magic |
“INRO0DB1” — detecta al instante que un fichero no es una base de datos inro, en lugar de interpretar bytes arbitrarios como punteros de página. |
| 8 | 2 | format_major |
Un binario que encuentra una versión mayor más alta que la suya se niega a abrir el fichero. Sin este campo, una versión antigua corrompería un fichero nuevo al aplicar reglas obsoletas. |
| 10 | 2 | format_minor |
Cambios compatibles con versiones anteriores. |
| 12 | 1 | page_size_log2 |
9 para 512 B, 12 para 4 KB. Un campo crítico: sin el tamaño de página no se puede leer ninguna página, por lo que vive en un offset fijo y se lee con una única lectura de 64 bytes. |
| 13 | 1 | flags |
Rasgos fijados en la creación: checksums de página, compresión de prefijos, cifrado. Fijados porque cambiarlos exigiría reescribir el fichero entero. |
| 14 | 2 | — | Reservado. |
| 16 | 16 | db_id |
El identificador único de esta base de datos. Permite que los despliegues multi-tenant detecten que un fichero ha sido cambiado, y evita que las herramientas de copia de seguridad confundan linajes no relacionados. |
| 32 | 8 | created_unix |
Marca de tiempo de creación, para diagnóstico y análisis forense. |
| 40 | 4 | meta_a_page |
La página que contiene el slot meta A. |
| 44 | 4 | meta_b_page |
La página del slot B. Explícita en lugar de fija por convención, para que los dos se puedan colocar en sectores físicos distintos del dispositivo sin subir la versión del formato. |
| 48 | 12 | — | Reservado para crecer sin romper el formato. |
| 60 | 4 | header_crc32 |
La cabecera se valida a sí misma. Si no cuadra, la base de datos se niega a abrir y lo dice claramente, en lugar de operar sobre una geometría corrupta. |
Páginas meta
Dos slots alternos, A y B. Cada uno contiene:
txn_id(u64, monótono, nunca se reutiliza)un puntero a la raíz del árbol
la cabeza de la lista de páginas libres
n_pages: el número de páginas que la transacción consideraba asignadasuna copia de los campos de geometría de la cabecera, para poder recuperarla si la cabecera se daña
la clave de datos envuelta, cuando el cifrado está activado
su propio CRC32, o una etiqueta de autenticación si el cifrado está activo
Un commit es: escribir las páginas nuevas, fsync, escribir la meta inactiva, fsync. Al abrir, se leen ambos slots, se descartan los que fallan la validación, y se monta el que tiene el txn_id más alto. Si ocurre un fallo mientras se escribe una meta, ese slot falla la validación y se monta el otro: se pierde ese commit, nada se corrompe.
Disposición de las metas y sectores físicos
Los discos actuales son de 4 KB físicos incluso cuando informan de sectores lógicos de 512 B. Una escritura interrumpida puede dañar los 4 KB completos. Si A y B compartieran un sector físico, un único corte de energía podría llevarse a ambos, y la base de datos sería irrecuperable.
Disposición con páginas de 512 B:
página 0 cabecera (64 B) + reservado
página 1 meta A (sector físico 0)
páginas 2-7 páginas del árbol (sector físico 0)
página 8 meta B (sector físico 1)
páginas 9+ páginas del árbolA y B caen en sectores físicos distintos por construcción. Que páginas de datos compartan sector con meta A no es un problema: son páginas copy-on-write, y las que se están escribiendo ya están libres desde el punto de vista de la meta superviviente, así que el daño que sufran es irrelevante.
Coste mínimo fijo por base de datos: 4,6 KB. En cinco mil tenants, 23 MB. La disposición ingenua con páginas de 4 KB en todas partes costaría 60 MB.
Reubicación de la meta B. La cabecera almacena meta_a_page y meta_b_page explícitamente precisamente para que las metas se puedan mover. Eso se usa para bajar el suelo: en un fichero que aún no ha pasado de 4 KB, la meta B vive en la página 2, y se mueve al offset 4096 en cuanto el fichero crece más allá de ese punto. Durante esa ventana ambas metas comparten sector físico, pero una base de datos por debajo de 4 KB apenas contiene datos y recuperarla significa recrearla, así que el riesgo es aceptable a cambio de bajar el suelo por base de datos de 4,6 KB a 1,5 KB. Cinco mil tenants inactivos pasan de 23 MB a 7,5 MB.
El traslado ocurre dentro de una transacción ordinaria: la meta B se escribe en su nueva posición, la cabecera se actualiza, y se sincroniza. Es la única operación de todo el diseño que reescribe la cabecera, razón por la que lleva la copia de geometría que las metas ya guardan.
Formato de página
Una página con slots y un directorio. Cabecera lógica de 8 bytes más 4 para el CRC cuando no hay cifrado:
0 1 tipo (0 hoja, 1 interno, 2 desbordamiento, 3 lista libre)
1 1 flags
2 2 número de slots
4 2 fin del array de slots
6 2 inicio de las celdas
8 4 crc32 del resto de la página (ausente si está cifrada)
12 .. array de slots (u16 por slot)
.. espacio libre
.. celdas, creciendo hacia el inicio de la páginaLos slots se mantienen ordenados por clave, lo que permite la búsqueda binaria sin mover celdas al insertar. Con una página de 512 B, hay unos 500 bytes utilizables.
Prefijo compartido por página, no por celda. El prefijo común a todas las claves de una página se almacena una vez, entre la cabecera de la página y el array de slots; cada celda conserva solo su sufijo.
Almacenarlo por celda, relativo a la anterior, comprimiría un poco más pero rompería la búsqueda binaria: la celda i no se podría decodificar sin decodificar antes de 0 a i−1, llevando el acceso a la página de O(log n) a O(n). El prefijo por página conserva casi todo el ahorro sin ese coste.
Celda hoja: [suffix_len varint][suffix][value_len varint][value]. En claves de log, que comparten los bytes altos de la marca de tiempo, el prefijo de página elimina unos 5 de cada 11 bytes de clave.
Celda de nodo interno: [key_len varint][truncated separator][child_page u32]. El separador se trunca a la secuencia de bytes más corta que distingue las dos hojas, en lugar de copiar la clave completa. Eso eleva el factor de ramificación de unos 35 a unos 50 con páginas de 512 B.
Valores grandes: por encima de un cuarto de página, los valores van a una cadena de páginas de desbordamiento; la celda almacena la longitud total y la primera página de la cadena.
Varint y CRC32
Ambos están escritos a mano, junto con el analizador, el B+tree y el resto de las codificaciones — los cubre la política por defecto del proyecto de cero dependencias en tiempo de ejecución.