CouchDB Performance
Finalmente sono riuscito a provare CouchDB con una quantità di dati interessante e su una macchina interessante. L’obiettivo era quello di verificare se CouchDB poteva reggere un carico di milioni di documenti e se il tempo per il calcolo delle view è effettivamente incrementale e con complessità logaritmica.
Una brevissima introduzione: CouchDB è un database documentale, ogni documento (il record di una tabella in un database relazionale) è insieme di coppie chiave/valore, non esiste uno schema dei dati, ogni documento può contenere qualsiasi insieme di coppie chiave/valore, ad ogni database possono essere associati una o più view (query in un database relazione), ogni view è composta da due funzioni javascript, una funzione map che consente di trasformare ogni documento contenuto nel database in un altro insieme di coppie chiave/valore e di associarle ad una chiave, e una funzione reduce (opzionale) che prende in pasto l’output della funzione map precedente raggruppata per chiave (una sorta di group by dei database relazionali) e che può essere utilizzata per computare valori aggregati
Un piccolissimo esempio che è molto vicino al test che ho fatto. Supponiamo di avere un database (ovvero un insieme di documenti) di questo tipo
[
{
"url": "http://salute.corriere.it/news/some-news.html",
"visits": 3
},
{
"url": "http://lavoro.corriere.it/index.html",
"visits": 10
}
]
Ovvero associamo ad ogni url il numero di volte che è stata visitata. Noi vogliamo sapere il numero di visite per ogni “sezione” delle url presenti nel nostro database. Definiamo intuitivamente il significato di “sezione” dicendo che l’url “http://salute.corriere.it/news/some-news.html” appartiene a tre sezioni “corriere.it”, “salute.corriere.it” e “salute.corriere.it/news”. Supponiamo di avere a disposizione una funzione “forEachSectionOf(url,doSomething)” che prede in pasto una url e richiama la funzione doSomething per ogni sezione dell’url passandogli la sezione stessa come parametro. La nostra funzione map sarebbe una cosa del tipo
function(doc) {
forEachSectionOf(doc['url'], function(section) {
emit(section, doc['visits'])
});
}
La funzione emit viene chiamata tutte le volte che vi vuole produrre un output, il primo parametro è la chiave del risultato e il secondo è il valore (sia la chiave che il valore possono essere strutture dati complesse, non devono essere per forza valori come in questo caso, ma questa è un’altra storia). L’output prodotto dalla map applicata al database di cui sopra sarà
[
{ "corriere.it": 3 },
{ "salute.corriere.it": 3 },
{ "salute.corriere.it/news": 3 },
{ "corriere.it": 10 },
{ "lavoro.corriere.it": 10 }
]
Abbiamo detto che l’imput alla reduce è l’output della map raggruppato per chiave, quindi
[
{ "corriere.it": [ 3, 10 ] },
{ “salute.corriere.it”: [ 3 ] },
{ “salute.corriere.it/news”: [ 3 ] },
{ “lavoro.corriere.it”: [ 10 ] }
]
Ricordandoci che vogliamo calcolare il numero di visite per ogni sezione, la reduce è molto semplice
function(keys, values, rereduce) {
return sum(values)
}
Che produce il risultato atteso
[
{ "corriere.it": 13 },
{ "salute.corriere.it": 3 },
{ "salute.corriere.it/news": 3 },
{ "lavoro.corriere.it": 10 }
]
Il grosso vantaggio di CouchDB in termini di perfomance è che le view vengono calcolate in maniera incrementale, ovvero tutte le volte che interroghi una view CouchDB ricalcola solo i valori relativi ai documenti che sono cambiati o che sono stati aggiunti dall’ultima volta che la stessa view era stata calcolata (la cosa più vicina a questa nel mondo dei database relazionali sono le materialized view di Oracle). Inoltre il costo del calcolo dell’incremento dovrebbe aumentare in maniera logaritmica rispetto all’aumentare della dimensione del database.
Ho voluto toccare con mano e quindi ho fatto il seguente esperimento:
- Fetch di 10000 record da una tabella di mysql contenente 15 milioni di record
- Store di ogni record come documento in CouchDB (i documenti non sono stati salvati singolarmente, ma in modalità batch che è molto più performante)
- Query della view di CouchDB che equivale al ricalcolo della view stessa
Ho misurato ognuna delle tre fasi e l’ho ripetuto fino a consumare tutti e 15 i milioni di record, di seguito i risultati
- Pro: la complessità del ricalcolo della view è effettivamente logaritmico
- Pro: il costo d’inserimento dei documenti in CouchDB è costante (la struttura dati utilizzata è append-only, quindi c’era da aspettarselo, ma fa comunque piacere verificarlo)
- Pro: una volta calcolata la view, i tempi di risposta sono stupefacenti, praticamente istantanei
- Pro: il tempo totale d’inserimento di 15 milioni di documenti è stato di 26 minuti
- Pro: l’occupazione di memoria durante tutto il processo non ha mai superato i 50MB
- Contro: il tempo totale di calcolo della view è stato di circa 17 ore. Bisogna tener conto che le view vengono calcolate da funzioni javascript in un processo separato e che la comunicazione fra CouchDB e l’interprete javascript è stdin/stdout, quindi a parte la velocità dell’interprete javascript (di default spidermonkey) c’è anche un costo notevole di serializzazione/deserializzazione. Scommetto che scrivendo le map/reduce direttamente in Erlang questo numero cambierebbe sensibilmente
- Contro: lo spazio occupato dal database è di 21GB contro i 7.5GB di mysql (anche se ci metterei un bel chissene visto il costo degli storage)
- Contro: lo spazio occupato dalla view è di 32GB (again chissene)
- Contro: per ogni database CouchDB usa uno ed un solo processore, quindi anche se avete 16 processori come nel mio caso, non ve ne fate niente a meno di non avere database multipli. Il modello map/reduce implementato da CouchDB potrebbe tranquillamente consentire il partizionamento dei dati su più database, ma attualmente gli sviluppatori si stanno concentrando solamente sulla replicazione, se volete dovete implementare voi il meccanismo di reduce finale
Conclusione: se vi trovate in una situazione per cui avete una grande quantità di dati e le query che fate non cambiano spesso, un database come CouchDB potrebbe essere un bel passo in avanti rispetto ad un database tradizionale

Avendo apprezzato Peter Seibel in “Founders at Work” ( consigliatissimo) in questi giorni sto leggendo con piacere il suo ultimo lavoro “Coders at Work”, stando all’ultimo suo 