CouchDB Performance

11 October, 2009 (10:36) | couchdb, erlang, programming

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

urls_per_domain.times

  • 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

Comments

Comment from Franco Lombardo
Date: October 19, 2009, 5:50 pm

Gabriele,
post molto interessante, come al solito. Due annotazioni

1) Mi sembra che una delle raccomandazioni per creare buone applicazioni che fanno uso di DB sia quella di evitare al massimo l’uso delle stored procedures. Il concetto di Map-Reduce, come introdotto in Couchdb, non si può intendere come un ritorno in grande stile delle stored procedures? L’uso si Javascript per queste funzioni mi lascia poi un poco perplesso.

2) Da sostenitore di AS400, ti ricodo che, oltre alle materialized views di Oracle, esistono anche le Materialized Query Table di DB2 (http://publib.boulder.ibm.com/infocenter/iseries/v5r4/index.jsp?topic=/rzajq/rzajqmqt.htm) :-)

Ciao

Franco

Comment from devsmt
Date: October 26, 2009, 9:30 am

interessantissimo e molto chiaro, come merita un argomento al limite dello sperimentale, ottimo anche l’accorgimento narrativo di una conclusione in cui esemplifichi con uno use-case molto favorevole.
mi sfugge, però, se per un sito dinamico ma diciamo tradizionalmente concepito, per intenderci basato su LAMP e organizzato secondo MVC, se il database relazionale va ad affiancare quello documentale o viene totalmente rimpiazzato e se l’elaborazione map/reduce che descrivi rimpiazza totalmente o si affianca all’elaborazione lato server.

grazie e complimenti per l’ottimo lavoro

Comment from Gabriele Lana
Date: October 26, 2009, 10:30 am

@devsmt Grazie per i complimenti :-)

Per quanto riguarda il discorso convivenza del relazionale/documentale, ovviamente la risposta è “dipende”, in particolare dipende dalla natura dell’applicativo, se hai bisogno di una consistenza “forte” dei dati sopratutto per quanto riguarda la transazionalità del dato, penso che convenga restare sul db ralazione (ci sono modo di rigirare il problema nel mondo documentale, ex. basta creare un documento che rappresenta e che accumula tutti i dati della transazione). Ma se l’applicazione è il classico CRUD potresti scriverla tutta direttamente in CouchDB (http://github.com/jchris/couchapp) e vivresti una vita felice. Detto questo noi (http://www.yourank.it) stiamo facendo convivere il mondo relazionale con quello documentale senza grossi problemi, ma CouchDB guadagna sempre più terreno ;-)

Comment from Gabriele Lana
Date: October 26, 2009, 10:48 am

@Franco penso che ogni tecnologia ha la sua applicabilità, è la forza/condanna di strumenti come VisualBasic e Access, il mondo ha iniziato ad utilizzarli per quello che non erano, la Microsoft ha iniziato a trasformarli in ciò che non dovevano essere, alla fine sono diventati qualcosa di talmente bacato e complesso da scatenare l’odio da parte delle persone. Couchdb ad oggi ha il potenziale per poter risolvere in maniera semplice ed elegante una certa classe di problemi, facendo anche la scelta di usare javascript sia per il cliente che per il server, l’unica cosa per il momento è aspettare e vedere come lo faranno evolvere :-)

Nota: rispetto alle stored procedures il modello di CouchDB è assai più elegante, l’unica astrazione esistente è il documento, un documento può essere dato o procedura, nulla vieta di processare le procedure come dati per produrre dati o procedure, so che può sembrare complesso, in realtà rende il tutto molto semplice una volta che hai compreso i pochi concetti di base

Comment from Franco Lombardo
Date: October 26, 2009, 4:51 pm

Alcuni miei dubbi su map-reduce inglobate nel repository dei dati:
1) Come si fa a testarne la logica?
2) La logica applicativa non risulta dispersa (e magari duplicata) parte nel repository e parte nel programma “client”?

Grazie

Ciao

Franco

Comment from Gabriele Lana
Date: October 26, 2009, 6:23 pm

@Franco
1) Per CouchDB mi sono organizzato io, le funzioni in javascript le testo unitariamente e poi le assemblo automaticamente prima del deployment (lo faccio con rake), in più faccio dei test end-to-end del database caricando dei documenti ad-hoc e verificando che l’output è quello atteso (lo faccio con rspec)
2) Sicuramente meno dispersa rispetto a quello che avviene di solito, visto che il linguaggio è unico e che il client può controllare direttamente tutto del server, compresa la creazione al volo delle view (ovviamente scritte in javascript)

Comment from Andrea
Date: January 28, 2010, 12:08 pm

Ciao, conosci un progetto di Database Documentale interamente scritto in c#? Grazie.

Comment from markux
Date: February 12, 2010, 12:45 am

ciao, ottimo articolo… hai provato anche mongoDB? da questi test e dalla documentazione mi sembra + completo, anche per sostituire un relazionale in molti applcativi

http://www.snailinaturtleneck.com/blog/?p=74

Comment from Gabriele Lana
Date: March 1, 2010, 12:53 pm

@markux Si negli ultimi mesi li ho usati entrambi (per progetti diversi ovviamente) purtroppo ho tratto una conclusione poco pratica: sono due storage “special purpose”, ovvero molto adatti per determinati tipi di progetti e poco adatti per altri, per sapere se sono o meno adatti al tuo progetto li devi conoscere, a questo proposito ho pubblicato una presentazione su slideshare (couchdb-vs-mongodb)

Write a comment