Articoli approfonditi sulla tecnologia che plasma il futuro.

SQLite distribuito: 5 miti verificati dopo 12 anni

Dopo dodici anni di SQLite distribuito (Raft, Litestream, Turso, D1) testiamo cinque convinzioni su consistenza, replica e quando usare Postgres.

Un anello di piccoli schedari collegati da fili di luce, uno dei quali brilla come leader
Dodici anni di SQLite distribuito: molti design, un'idea centrale che continua a essere reinventata.

SQLite distribuito è passato da progetto del weekend di una sola persona a categoria di prodotto finanziata da venture capital, senza che la maggior parte degli ingegneri si accorgesse dei dieci anni di mezzo. Nel settembre 2014 Philip O'Toole ha messo un log Raft davanti a SQLite, ha replicato ogni scrittura su un cluster e ha pubblicato il codice su GitHub con il nome rqlite. Dodici anni dopo esiste un intero ecosistema (dqlite, Litestream, LiteFS, cr-sqlite, libSQL/Turso, Cloudflare D1) e gran parte di ciò che si crede in merito è sbagliato in modi interessanti.

Ho gestito database clusterizzati in produzione e ho rimesso in piedi i pasticci causati dalle loro modalità di guasto, quindi vi risparmio un po' di grattacapi: le discussioni sull'SQLite distribuito quasi mai riguardano SQLite. Riguardano la domanda che ci si sta davvero ponendo: cosa succede se la macchina muore, cosa succede se la macchina muore e non posso aspettare, oppure cosa succede se non c'è affatto una macchina. Ecco cinque miti, messi alla prova con dodici anni di storia.

Mito 1: l'SQLite distribuito è un'idea recente

L'ondata attuale di articoli sull'SQLite replicato comincia intorno al 2021, quando Litestream e il movimento «SQLite basta» hanno iniziato a fare rumore, e Turso e Cloudflare D1 sono arrivati con finanziamenti e annunci di lancio. Così si è diffusa la convinzione che qualcuno di brillante avesse notato di recente che SQLite poteva essere replicato. In realtà il primo sistema Raft su SQLite è stato rilasciato nel 2014, e non è nemmeno stata la prima replica di SQLite: Expensify la usava internamente con un protocollo di tipo Paxos da anni, prima di rilasciare Bedrock nel 2016, mentre ActorDB dava a ogni attore Erlang un proprio database SQLite replicato con Raft più o meno nello stesso periodo.

Perché questo conta, al di là dell'attribuzione? Perché un design che sopravvive a dodici anni di reinvenzioni indipendenti ti dice qualcosa. Almeno otto team diversi, con esigenze e protocolli diversi, hanno osservato il problema e sono arrivati alla stessa architettura: SQLite come motore di storage, con la replica aggiunta dall'esterno. Nessuno ha trovato un motivo per smettere. Quando vedi questo schema, cioè una ri-implementazione convergente lungo un decennio, hai davanti un'idea davvero valida e non un ciclo di hype. Confrontala con le categorie che esplodono e poi scompaiono: questa ha continuato ad accumulare valore.

Conta anche perché le vecchie decisioni di design portano ancora il peso della struttura. rqlite ha scelto nel 2014 di essere un sistema CP: le scritture passano dal leader, serve l'assenso di una maggioranza e una minoranza isolata dalla partizione rifiuta le scritture. Non ha mai cambiato idea. Quella singola scelta ha dettato l'API HTTP, i livelli di consistenza delle letture e il comportamento degli snapshot. I team che trattano la consistenza come un interruttore di configurazione da decidere più avanti di solito se ne pentono; il divario tra la specifica che avevi implicato e quella che hai effettivamente implementato è esattamente il terreno in cui crescono i bug distribuiti.

Mito 2: Raft su SQLite ti dà un Postgres distribuito a poco prezzo

È il mito più costoso della lista. La promessa è seducente: prendi il database più diffuso al mondo, aggiungi il consenso e ti risparmi il peso operativo di un sistema client-server. Quello che ottieni davvero è un database con un solo scrittore in cui ogni scrittura paga un round trip di rete verso la maggioranza dei nodi, e dove la superficie SQL di SQLite (niente stored procedure, un sistema di tipi essenziale, primitive di concorrenza limitate) resta quella di SQLite.

Il percorso di scrittura è il costo onesto. Un commit locale su SQLite costa un paio di fsync. Un commit replicato con Raft prevede: il leader aggiunge la voce al proprio log, la invia ai follower, attende che una maggioranza la persista, la applica a SQLite e poi risponde. Su una LAN sana si parla di uno o due millisecondi invece di decine di microsecondi. Tra regioni diverse, il costo è la velocità della luce più il disco. Per carichi con molte letture e un tasso di scrittura modesto (archivi di configurazione, metadati di service discovery, piccoli control plane) va benissimo, ed è esattamente la nicchia per cui è nato rqlite (la sua storia d'origine è un sistema clusterizzato che aveva bisogno di una piccola quantità di storage relazionale, senza una buona risposta alla domanda su quale nodo dovesse ospitarlo). Per un carico OLTP intensivo, è lo strumento sbagliato.

Poi c'è la realtà operativa che nessuno mette nel README: l'elezione del leader è una modalità di guasto di cui ora sei responsabile. Lo split-brain è prevenuto dal voto a maggioranza, certo, ma questo significa che un cluster a tre nodi che perde due nodi rifiuta tutte le scritture. Hai scambiato «il database è giù perché il server è morto» con «il database è giù perché il quorum è morto», e la seconda cosa è più difficile da spiegare al reperibile alle 3 di notte. Se non hai ancora pensato a test in stile Jepsen sul sistema che hai scelto, dovresti: un commentatore di Hacker News ha posto la domanda pungente se rqlite sia mai stato testato con Jepsen, perché i sistemi che non lo sono di solito nascondono sorprese. Nella stessa discussione, un ingegnere esperto ha osservato che, con gli strumenti moderni, far girare un vero Postgres è diventato abbastanza semplice da rendere di nuovo legittimo il default «usa semplicemente un server di database». Non ha torto.

Una bilancia che pesa un blocco semplice contro un groviglio di catene e ingranaggi
Il consenso ti compra il failover; il prezzo si paga su ogni singola scrittura.

Mito 3: replica significa consenso

Se c'è un'intuizione che la storia di dodici anni offre davvero, è questa: «SQLite replicato» non è un unico design, ma tre angoli di uno spazio di progettazione, comparsi in un ordine rivelatore. Confonderli è il modo in cui i team finiscono per far girare un cluster Raft per risolvere un problema di backup.

  • Consenso (CP). rqlite, dqlite. Un leader, forte consistenza, un vero cluster. Risponde a: «cosa succede se la macchina muore e non posso aspettare il ripristino?». Costo: latenza di scrittura e operazioni di quorum.
  • Streaming (un solo scrittore). Litestream, poi LiteFS. Nessun consenso: il WAL di SQLite viene trasmesso verso object storage, garantendo un disaster recovery quasi continuo per un singolo nodo. Risponde a: «cosa succede se la macchina muore?». Costo: una piccola finestra di possibile perdita di dati e tempi di ripristino nell'ordine di secondi o minuti.
  • Merge (AP). cr-sqlite e il movimento local-first. CRDT come estensione di SQLite, con molti scrittori che divergono e si fondono senza un leader. Risponde a: «cosa succede se non esiste affatto una macchina centrale?». Costo: consistenza eventuale e semantiche di merge che devi progettare.

L'argomentazione di Ben Johnson a favore di Litestream merita rispetto, perché era corretta per la maggior parte delle applicazioni: girano su una sola macchina e hanno bisogno di un modo per recuperare quando quella muore, non di un cluster. La prima domanda, «cosa succede se la macchina muore», è molto più comune della seconda, «e non posso aspettare il ripristino». Ho visto team installare un cluster Raft a tre nodi per proteggere un database il cui requisito di RTO era «prima di pranzo, più o meno». Non è ingegneria dell'affidabilità, è cargo cult. In una bella chiusura del cerchio, l'approccio di streaming del WAL di Litestream ha finito per ispirare il modo in cui rqlite stesso gestisce il WAL nella versione 8: il filone dello streaming ha alimentato il filone del consenso.

Quindi, prima di scegliere uno strumento, scrivi la domanda. Letteralmente. «Quando il nodo X muore, dobbiamo far riprendere le scritture entro N secondi, e accettiamo di perdere gli ultimi M secondi di dati confermati». Se M è di qualche secondo e N è un minuto, Litestream verso S3 è la risposta e non ti sveglierà mai per un problema di quorum. Se M è zero e N è di secondi, sei in territorio di consenso e lo pagherai su ogni scrittura. È la stessa disciplina di qualsiasi threat model: nomina il guasto prima di comprare la difesa, come quando ti avvicini ai sistemi che resistono alle catastrofi decidendo in anticipo quale catastrofe stai affrontando.

Mito 4: lo «SQLite al edge» gestito elimina i compromessi

I soldi in questa categoria sono andati alle piattaforme: Turso ha raccolto finanziamenti di venture capital ed è stata acquisita da Supabase; Cloudflare ha lanciato D1 dentro la propria rete edge. La promessa è che qualcun altro ora si occupi delle parti difficili del distribuito. A volte è vero. A volte è marketing, e il segreto poco pulito viene a galla: un developer di Cloudflare ha ammesso pubblicamente che D1 è essenzialmente un wrapper attorno a un singolo Durable Object che espone il suo database SQLite, e che spesso conviene usare direttamente lo storage dei Durable Object. Questo non è un database distribuito in senso consensuale; è un singolo file SQLite con una piattaforma intorno. Chi usa D1 in produzione riferisce un'affidabilità all'altezza.

Niente di tutto questo significa che le opzioni gestite siano cattive. Significa che i compromessi non sono scomparsi: sono finiti dietro un'API che non puoi ispezionare. Quando gestisci rqlite da solo, sai esattamente cosa significa CP: tre nodi, un leader, ack di maggioranza, una minoranza isolata che rifiuta le scritture. Quando compri «SQLite al edge», devi porre le stesse domande a una scatola nera: dove sta lo scrittore, cosa succede in caso di partizione, qual è la mia reale finestra di durabilità? Se la risposta del fornitore è vaga, la tua post-mortem non sarà precisa.

La replica come servizio non cancella la domanda sulla consistenza. Sposta solo la risposta nell'infrastruttura di qualcun altro, dove non puoi verificarla.

Il pattern storico vale la pena di essere interiorizzato: i sistemi open source (rqlite, dqlite, Litestream) hanno sviluppato l'idea per otto anni prima che qualcuno la vendesse, e il capitale è finito a chi ha trasformato l'idea in un servizio con una fattura. È il destino tipico dell'infrastruttura. Comprare è spesso la scelta giusta, ma compra con gli occhi aperti su quale dei tre angoli stai davvero ottenendo.

Una palla di vetro con dentro una calda baita, posata in un campo nebbioso di rack di server
SQLite edge gestito: un accogliente database singolo avvolto in una piattaforma che non puoi vedere dentro.

Mito 5: la forte consistenza è un aggiornamento gratuito rispetto ai backup

Le persone inquadrano la scelta come backup (deboli, economici) contro consenso (forte, costoso ma strettamente migliore). Non è vero. La forte consistenza tramite Raft è un prodotto diverso, non una versione migliore dello stesso prodotto, e impone tasse continue che i backup non hanno mai.

Prima tassa: il throughput. Un professionista che ha usato entrambi ha osservato che rqlite offre garanzie del tipo «salvato in più di un posto», ma è molto più lento di un database su un solo nodo: è il round trip del consenso, e non sparisce mai. Seconda tassa: semantiche sottili. Se replichi inviando istruzioni SQL, erediti problemi di determinismo: funzioni come random() o datetime('now') possono divergere tra i nodi a meno che il livello non le riscriva, ed è per questo che alcune implementazioni supportano solo un sottoinsieme dell'SQL di SQLite. Se replichi invece a livello di WAL, eviti il problema, ma ti accoppi al formato su disco e al layout delle pagine di SQLite. Non esiste una collocazione del livello senza un compromesso. Terza tassa: le transazioni distribuite sono davvero difficili. Un ingegnere ha descritto l'evoluzione di un design in cui ogni istruzione di una transazione era un comando Raft separato (corretto, più o meno, ma lento) verso uno in cui il nodo iniziatore esegue l'intera transazione in locale, la annulla e invia un singolo comando di commit con l'intero batch. È il tipo di lezione sudata che impari solo gestendo questi sistemi.

Ecco un'illustrazione minima della differenza nel percorso di scrittura, usando l'API HTTP di rqlite:

# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.

E nota cosa non hai ottenuto: la scalabilità delle letture. Raft replica il log affinché i follower possano subentrare; non fa sì che i follower servano letture fortemente consistenti per default (rqlite offre livelli di consistenza delle letture proprio perché questo è un compromesso). Se sei venuto per il «distribuito» sperando in più capacità di lettura, volevi delle read replica, non il consenso.

Cosa insegnano davvero dodici anni di Raft su SQLite

La storia della categoria è una prova insolitamente chiara di come maturano le idee infrastrutturali. Prima qualcuno costruisce la soluzione di principio per un'esigenza concreta (rqlite, nata da un vero problema di clustering in un vero lavoro). Poi altri la ricostruiscono per i propri vincoli di packaging: dqlite di Canonical è una libreria che si incorpora perché Canonical aveva bisogno di un database dentro i propri prodotti, mentre rqlite è un server che fai girare accanto al prodotto di chiunque. Poi qualcuno sostiene che il caso comune è più piccolo di quanto assuma la soluzione di principio (Litestream), si dimostra che aveva ragione e l'intero campo si ricalibra. Infine le piattaforme la avvolgono e la vendono. Se lavori abbastanza a lungo nell'infrastruttura, rivedrai questo film: anche gli agenti LLM stanno reimparando le lezioni dei sistemi distribuiti nello stesso modo proprio adesso.

La mia raccomandazione senza giri di parole, dopo tutto questo: parti con SQLite su singolo nodo più lo streaming del WAL verso object storage. È sorprendente quanto si possa arrivare lontano con questo, e la tua storia di guasto è un ripristino, non un postmortem sul consenso. Passa a un sistema basato su Raft solo quando sai dichiarare in una frase il requisito di disponibilità che lo streaming non riesce a soddisfare, e accetta il conto in latenza di scrittura e operazioni di quorum che ne deriva. Ricorri a Postgres nel momento in cui ti serve la sua superficie SQL o un throughput con più scrittori. E se nessuna di queste opzioni ti convince, il mondo dei database embeddable offre altre strade, dai database a grafo embeddable fino all'angolo CRDT local-first.

Il riepilogo che sfata i miti: l'SQLite distribuito non è nuovo, non è un Postgres economico, non è un unico design, non diventa sicuro grazie a un wrapper gestito e non è un aggiornamento gratuito rispetto ai backup. È uno spazio di progettazione di dodici anni, ben mappato, con tre angoli, ciascuno corretto per una domanda diversa. Gli ingegneri che si scottano sono quelli che non hanno mai scritto la domanda.