inro

Per què inro

Hi ha almenys tres motors que ja funcionen aquí: Arkeion, quan les dades han de ser auditables al llarg del temps; SQLite, quan hi ha un sol node i una sola base de dades; i la família LMDB, quan la càrrega de treball necessita copy-on-write encastat amb anys d'enduriment en producció. Aquest és l'argument per construir un quart motor tot i així — on hi guanya inro, què hi renuncia per aconseguir-ho, i l'únic cas on la resposta honesta és: no ho facis.

Enfront d'Arkeion

Arkeion i inro són germans, no competidors, i optimitzen coses oposades. Arkeion optimitza l'auditabilitat, el versionat i la potència de consulta. inro optimitza el consum més petit possible de disc, RAM i CPU. Arkeion és el sistema de registre per a dades que han de ser auditables al llarg del temps; inro és l'emmagatzematge local per a nodes amb recursos escassos, o desplegats per milers.

El que inro renuncia per aconseguir la seva empremta: SQL — sense parser, planner ni executor; cerca full-text, índexs vectorials, triggers, views i foreign keys; accés des de més d'un procés; i time-travel, branques i consultes de tipus AS OF.

Time-travel és el que cal desgranar, perquè sembla barat i no ho és. Mesurat contra una càrrega de treball real d'edge — lots de mil registres, uns 166 commits al dia — retenir el camí arrel-a-fulla de cada commit (unes quatre pàgines de 4 KB cadascuna) acumularia 2,6 MB de pàgines immortals al dia: un sobrecost del 40% en la mida del fitxer. Pitjor que la mida: la poda per retenció deixaria d'alliberar espai, perquè una versió antiga encara veuria les files que va esborrar. Això obliga a un vacuum amb retenció — exactament la compactació periòdica que fa que LSM sigui rebutjat més avall.

Per a l'únic cas que realment justificaria el versionat — provar que ningú ha manipulat una xifra de facturació passada — la resposta d'inro és mil vegades més barata: una cadena de hash sobre els agregats de facturació, 768 bytes al dia, 280 KB a l'any, sense cap versionat de fitxers.

Un node, una base de dades, i una necessitat de SQL, branques o time-travel: tria Arkeion. Milers d'instàncies amb l'empremta limitada que només necessiten emmagatzematge clau-valor durador: tria inro.

Enfront de SQLite

On guanya SQLite

Un fitxer buit amb pàgines de 512 bytes i una taula ocupa uns 1 KB — menys que els 1,5 KB d'inro després de la reubicació del meta B. I més important que el recompte de bytes: la maduresa de SQLite. La seva suite de tests té cobertura completa de branques i un volum de codi de tests de l'ordre de 600 vegades el del propi motor. Cap byte estalviat compensa això, i cal dir-ho clarament.

On guanya inro, en la càrrega de treball de quota

| Dimensió | SQLite | inro | | --- | --- | --- | | Bytes per fila de 16 columnes | 42-45 B (uns 17 només dels codis de tipus, més el rowid i el punter de cel·la) | 37 B, gràcies a la compressió de prefix de la clau i l'absència de codis de tipus | | RAM del motor per base de dades oberta | ~2 MB de cache per connexió per defecte; de l'ordre de 100 KB fins i tot ajustat al mínim | 20-30 KB | | RAM total per base de dades, amb interning i agregats | Equivalent, més l'esquema analitzat i les prepared statements | ~832 KB després de la fase 1; ~34 KB després de la fase 2 | | Fitxers satèl·lit | En mode WAL: un -wal que creix fins a 4 MB abans del checkpoint, i un -shm de 32 KB mapat per connexió | Cap | | Poda per retenció | DELETE ... WHERE ts < X és O(files): 833.000 esborrats diaris per node | delete_range per desenganxament de subarbre: ~25 pàgines llegides | | Mida binària | ~700 KB, ~300 KB en una build mínima | 100-250 KB estimat, més 60-100 KB amb xifratge |

Les xifres de SQLite i LMDB són estimacions de la fase de disseny i s'han de confirmar amb les mesures de sortida de la fase 1.

inro no es justifica perquè SQLite sigui gran. Es justifica per la RAM agregada a través de milers d'instàncies, pel cost de la poda, per l'absència de fitxers satèl·lit, i pel control del format. Amb un node i una base de dades, un SQLite ben configurat és la decisió correcta i escriure un motor és una pèrdua de temps.

Enfront de LMDB, libmdbx i redb

Aquests tres comparteixen l'arquitectura d'inro: B+tree copy-on-write, fitxer únic, sense fils en segon pla, lectors no bloquejants, obertures de cost constant. Junts cobreixen al voltant del 70% del nucli amb codi que s'ha endurit en producció durant anys.

| Dimensió | LMDB / libmdbx / redb | inro | | --- | --- | --- | | Bytes per entrada | 45-55 B — sense compressió de prefix, sense truncament de separador | 35-37 B | | Mínim per base de dades | 8-16 KB, a partir d'una pàgina fixa de 4 KB — 40-80 MB en cinc mil tenants, només en bases de dades buides | 1,5 KB |

Les xifres de SQLite i LMDB són estimacions de la fase de disseny i s'han de confirmar amb les mesures de sortida de la fase 1.

El que l'arquitectura compartida no inclou és exactament el que produeix les xifres d'aquest disseny:

  • Sense esborrat per rang per desenganxament de subarbre: la poda recau en O(files).

  • Sense xifratge a LMDB ni a redb.

  • LMDB i libmdbx utilitzen mmap, cosa que esgota les regions de memòria virtual quan hi ha milers de bases de dades obertes.

  • Cap dels tres està dissenyat per a milers d'instàncies en un sol procés: no hi ha grup de pàgines compartit.

Per què copy-on-write, i què es va rebutjar

Es van avaluar tres arquitectures d'emmagatzematge abans de decidir-se per una.

Triada: B+tree copy-on-write amb una llista lliure. Una pàgina modificada s'escriu en una ranura lliure, mai sobre dades vives; un commit és només escriure una pàgina meta nova. L'ocupació és estable, entre 1,2× i 2× les dades vives en el pitjor cas. Sense compactació, sense fils en segon pla, sense fitxer auxiliar, RAM constant. Els lectors no bloquejants surten del disseny de franc: un lector fixa una pàgina meta, i l'arbre que en penja és immutable mentre la retingui.

Un commit allibera la pàgina antiga en lloc d'esborrar-la; torna a la llista lliure per reutilitzar-se. Append-only, sense aquesta llista, faria créixer el fitxer indefinidament.

Rebutjada: LSM (memtable més SSTables). La compactació en segon pla és inassumible en IoT — CPU i bateria — i catastròfica amb milers de tenants per procés. Pitjor encara, cada base de dades oberta necessitaria el seu propi memtable, multiplicant la RAM pel nombre de tenants.

Rebutjada: B+tree in-place amb un WAL. Escriu cada pàgina dues vegades, obliga a un segon fitxer que s'ha de truncar, i les lectures concurrents necessiten una cache de pàgines amb latches o MVCC explícit — més codi que l'opció triada, no menys.

El matís que gairebé ningú diu en veu alta: un copy-on-write purament append-only seria l'opció pitjor en disc, perquè el fitxer creixeria sense límit. És la llista lliure, amb reutilització de pàgines, el que fa que aquesta arquitectura serveixi l'objectiu del projecte — no el copy-on-write per si sol.

Cadena de subministrament

Zero unsafe al codi propi d'inro. unsafe_code = "forbid" es troba sota [lints.rust] a Cargo.toml — no només #![forbid(unsafe_code)] a lib.rs — de manera que la prohibició també arriba als tests, benchmarks i exemples.

Zero dependències en temps d'execució per defecte. El parser, el B+tree, les codificacions, varint, CRC32, bitpacking i compressió estan tots escrits a mà. L'única excepció planificada són les primitives criptogràfiques darrere de la funcionalitat encryption, i fins i tot allà inro no escriu la seva pròpia criptografia — utilitza les implementacions auditades de RustCrypto.

L'advertència honesta: forbid(unsafe_code) no es propaga a les dependències. Els crates de RustCrypto utilitzen unsafe en la detecció de característiques de la CPU i en els seus backends SIMD. Si el backend portable de ChaCha20 pot prescindir d'aquest unsafe — i a quin cost de rendiment — és una qüestió oberta per a la fase 1, que s'ha de mesurar i auditar amb cargo geiger a la CI, no donar per suposada.

Conseqüència concreta per a la fase 4: la compressió LZ4 s'escriurà a mà, unes 300 línies, en lloc d'incorporar lz4_flex, que sí que conté unsafe.