MongoDB
e' un moderno database documentale NoSQL che al tempo stesso
offre le piu' avanzate funzionalita' di replica dati e di ricerca in parallelo (Map/Reduce).
MongoDB e' uno dei piu' diffusi DB NoSQL grazie alla sua velocita',
alla flessibilita,
alla semplicita' di installazione ed utilizzo.
Questo documento presenta gli aspetti principali di MongoDB.
I primi capitoli valgono come introduzione:
Installazione,
Modello dati,
Utilizzo.
Quindi si passa agli aspetti un poco piu' particolari o complessi:
Aggregazioni e Map/Reduce,
Replica e Sharding,
GridFS,
Capped collection ed indici TTL,
Transazioni (per ultimo ma e' il piu' importante).
A questo punto e' possibile vedere gli aspetti piu' tecnici:
Architettura,
Amministrazione.
Per finire un po' di
Storia,
...
MongoDB e' un software Open Source distribuito con licenza GNU (Engine) ed Apache (per i driver).
La versione di riferimento e' quella attuale di MongoDB [NdE 2Q14 versione 3.0] [NdA 4Q16 versione 3.2].
La versione di MongoDB descritta in questo documento e' obsoleta... anche se la maggioranza delle informazioni contenute in questo documento sono ancora valide si consiglia la lettura del documento MongoDB 4.0.
Installare MongoDB e' molto facile su Linux (eg. RedHat, Fedora, CentOS, ...). Una volta impostato il repository /etc/yum.repos.d/mongodb-org-3.0.repo [NdA Aggiornamento 3.2: copiare dal sito ufficiale il file /etc/yum.repos.d/mongodb-org-3.2.repo ] basta lanciare il comando:
In questo modo viene installato il demone (mongod) che gestisce il DB, la shell di accesso (mongo), il demone per il routing (mongos) ed i tool di gestione (eg. mongotop) della versione Community. In alternativa la versione Enterprise viene installata indicando il pacchetto mongodb-enterprise.
MongoDB e' disponibile per tutti i sistemi UNIX-based ed anche sulle piattaforme MS-Windows e Mac OS X. Su Mac OS X l'installazione consigliata e' con Homebrew:
Gia' fatto! Ora si puo' utilizzare...
Anche se mi piace scrivere pagine molto pratiche, prima di utilizzare MongoDB e' necessaria un po' di teoria per capire cosa si sta cercando di fare... vediamo quindi il modello dei dati.
MongoDB registra i document su un database utilizzando il formato BSON (che e' una rappresentazione binaria del formato JSON):
db.agenda.save( { nome: "Bartolomeo", cognome: "Bogliolo", soprannome: "Meo", eta: 60, esperienza: [ "MongoDB", "SQLite", "MySQL" ], telefono: "0123456789", peso : NumberLong(69) } )
Per inserire i dati non c'e' bisogno di fare nulla di piu':
il primo inserimento crea direttamente la struttura necessaria (collection).
Il campo chiave _id e' sempre presente e viene inserito automaticamente se non indicato.
Ogni document ha un suo elenco di campi (field) assolutamente libero.
Tuttavia tipicamente una collection ha document con un insieme di campi omogenei
e contiene dati tra loro in relazione.
Poiche' i nomi dei campi sono memorizzati per ogni documento, per collection di grandi dimensioni
e' opportuno utilizzare nomi di colonna brevi.
I tipi di dati utilizzabili sono molto ampi: numeri (interi 32bit, interi 64bit, double), stringhe (UTF-8)
ma anche array ed ObjectId.
L'equivalente relazionale di una collection e' una tabella, del document e' una riga, del field e' una colonna... ma tra i due modelli sono piu' le differenze che le analogie.
I dati possono essere estratti con l'operatore find che esegue la selezione
dei dati con operatori e la proiezione (la colonna _id per default e' presente).
find puo' essere modificato con l'ordinamento ed il limite:
db.agenda.find( { eta: { $gt: 18 } }, { nome: 1, indirizzo: 1 } ).limit(5).sort(nome)
E' possibile definire indici secondari sulle collection indicando semplicemente l'elenco dei campi:
db.agenda.createIndex( { cognome: 1 } )
I dati vengono estratti da una sola collection alla volta (non esiste il join).
Per gestire le relazioni tra collection diverse nel disegno della base dati possono
essere adottate due scelte: le references e gli embedded documents.
Con una reference viene posto il valore del campo _id della prima collection
come campo nella seconda collection. In questo modo le applicazioni potranno
utilizzare la find per trovare i dati collegati.
Questo tipo di tecnica e' analoga al modello normalizzato per un DB relazionale
(ma non c'e' l'SQL per fare i join e le ricerche corrette deve farle l'applicazione).
Con un embedded document un documento viene inserito all'interno di un altro documento.
Un esempio classico e' quello di un cliente all'interno di una fattura:
db.fattura.save( { numero: 17, data: new Date(), importo: 69.00, cliente: { RagSoc: "Acme ltd", vat: "12345678901" } } )
Poiche' MongoDB gestisce l'atomicita' delle transazioni solo a livello di documento, in questo modo e' possibile l'atomicita' di una operazione che comprende piu' documenti. Questa e' la tecnica piu' utilizzata, nonostante l'orrore di tutti i fanatici della normalizzazione.
MongoDB puo' essere utilizzato da vari linguaggi.
Sono disponibili driver per i piu' importanti linguaggi di programmazione
(eg C, C++, C#, Java, Perl, PHP, Python, Node.js, Ruby, ...).
MongoDB e' implementato dal processo mongod che e' tipicamente in ascolto sulla porta 27017
ed e' ovviamente richiamabile in modalita' client/server.
Inoltre e' disponibile una shell di accesso che utilizzeremo nei prossimi esempi: mongo.
mongo ha un'interfaccia amichevole, accetta tutti i comandi MongoDB
ed utilizza la sintassi ed i comandi JavaScript.
Anziche' descrivere in dettaglio tutti i comandi... ecco finalmente un esempio pratico!
Insomma non c'e' bisogno di ricordare a memoria i comandi!
Proviamo ora a creare un database, ad inserire qualche dato ed a fare qualche operazione:
Le condizioni della find() possono essere complesse e riferirsi a tutti i campi,
compresi quelli degli embedded document.
Sono disponibili AND, OR, operatori matematici, espressioni regolari, ...
save() e' un wrapper per insert() ed update(): viene richiamato l'uno o l'altro
a seconda che il campo _id sia presente o meno.
La funzione update() modifica un solo record se non e' indicata la clausola multi.
Per cancellare i documento si utilizza remove().
La funzione remove() cancella tutti i documenti corrispondenti alla condizione se
non e' indicata la clausola justOne.
In MongoDB, come in praticamente in tutti i NoSQL, non esistono i join... quindi vanno realizzati applicativamente. Ecco un semplice esempio tra i tanti possibili perche' il disegno di un DB in mongo non e' necessariamente in 3NF (nell'esempio ci sono diversi accorgimenti: meditate gente, meditate):
A differenza dell'SQL in cui sono disponibili le clausole di GROUP BY ed HAVING, i database NoSQL tipicamente utilizzano l'algoritmo MapReduce. La caratteristica di questo algoritmo e' quella di essere molto adatto ad un'esecuzione parallela su server distinti spezzando le fasi delle singole elaborazioni da quella di raccolta finale dei dati.
Con MongoDB si hanno entrambe le possibilita' perche' dispone di costrutti specifici come aggregate() simili al GROUP BY dei database SQL:
Ma naturalmente e' presente anche l'algoritmo Map/Reduce tipico dei database NoSQL:
La distribuzione e la replica dei dati su piu' server e' una caratteristica
fondamentale dei database NoSQL per supportare i BigData.
MongoDB dispone di due tecniche per la distribuzione dei dati:
la replica e lo sharding.
Piu' database MongoDB possono essere sincronizzati tra loro in modo molto semplice.
I database vengono aggiornati tra loro con una replicazione peer-to-peer incrementale
implementata nativamente nell'engine.
La replica in MongoDB e' basata sui Replica Set (RS):
un gruppo di nodi in cui il primario riceve le scritture e gli altri nodi le applicano.
In caso di caduta di un nodo i replica set si allineano automaticamente e, se necessario,
eleggono un nuovo primario.
E' sempre opportuno utilizzare un numero dispari di nodi di Replica Set);
se necessario e' possibile configurare
un nodo Arbiter che non gestisce dati e quindi ha requisisti molto ridotti.
Per garantire la durabilita' di un Replica Set e' necessario che siano presenti almeno
tre nodi con write concern impostato a w:majority (descritto nel prossimo capitolo).
Ecco i semplici comandi per configurare un Replica Set di 3 nodi (appena installati e senza autenticazione):
Per gestire basi dati di grandi dimensioni MongoDB permette di distribuire i dati
tra nodi differenti applicando un partizionamento tra i nodi: lo sharding.
La configurazione del cluster di sharding richiede diversi componenti:
routers (mongos), config server, shard e replica set.
In una configurazione minimale e' presente un processo mongos cui si interfacciano
le applicazioni e che si occupa del routing delle richieste ai nodi corretti.
La configurazione degli shard e' invece mantenuta dal MongoDB config cui e' assegnato questo
unico database. Infine i diversi shard su cui sono partizionati i dati
sono database MongoDB singoli o RS.
Le collection poste in shard vengono divise in chunk a seconda del valore della chiave
ed memorizzate su nodi diversi. Ad occuparsi della migrazione dei chunck e' il processo
balancer che puo' essere attivato da ciascun nodo mongos.
Se l'architettura dello shard delle collection presenta qualche complessita'
al contrario la loro gestione e' invece molto semplice.
Prima viene dichiarato lo shard del database, quindi
viene effettuato lo sharding di una collection partizionando i dati per una chiave
(MongoDB supporta il range partitioning, l'hash partitioning
e supporta il tag aware sharding):
Lo sharding introduce un elevato di scalabilita' orizzontale senza
introdurre complessita' dal punto di vista applicativo.
Dal punto di vista applicativo non vi sono differenze nell'uso
di collection in shard: l'unico requisito e' quello di collegarsi
ai processi di routing mongos anziche' ai nodi mongod.
La chiave di sharding va scelta con attenzione perche'
una scelta errata ha impatti prestazionali significativi
e non puo' essere modificata senza ricostruire la collection.
Le tecniche di replica e di shard dei dati in MongoDB
non sono alternative anzi si completano tra loro.
Tipicamente anche per una piccola installazione di produzione
si utilizza la replica per garantire la Durability dei dati.
Lo sharding e' invece rivolto alle installazioni piu' significative con decine o
centinaia di server dedicati e tipicamente si appoggia sulla replica nel
senso che uno shard e' tipicamente mantenuto da un replica set.
La dimensione massima di un documento BSON e' 16M, se un documento e' di maggiori dimensioni
va utilizzato il GridFS.
Un'importante funzionalita' di MongoDB e' quella di poter operare come File System distribuito.
Come al solito vediamo un esempio (in Perl):
my $musicDir = "/Users/meo/Music/Best"; my $client = MongoDB::MongoClient->new(host => 'localhost', port => 27017); my $database = $client->get_database( 'dj' ); my $grid = $database->get_gridfs; my @mFiles = getFiles($musicDir); $grid->drop(); foreach my $file (@mFiles) { my $fh = IO::File->new("$musicDir/$file", "r"); $grid->insert($fh, {"filename" => $file, "content-type" => "audio/mpeg", "author" => "eMo"}); }
Una collection cresce in modo indefinito e non sono presenti funzioni automatiche di riorganizzazione
(eg. autovacuum).
Oltre alle normali collection e' possibile utilizzare le Capped Collection che hanno
una dimensione fissa e sono molto efficienti nel gestire dati temporanei.
Una find() su una capped collection senza ordinamento recupera i dati in ordine naturale (LIFO).
Quando la capped collection raggiunge la dimensione massima o il numero massimo
di elementi, i nuovi inserimenti ricoprono i piu' vecchi.
In una capped collection e' consentito l'update() ma il documento non puo' crescere di
dimensioni, se si utilizza la replica anche la riduzione di dimensioni puo' generare problemi...
Un alternativa alle capped collection
sono gli indici TTL (time to live) che consentono di definire quanto mantenere un dato.
Un thread interno di mongod si occupa di cancellare periodicamente (per default ogni minuto)
i dati che hanno superato il TTL.
Una premessa importante, se conoscete i concetti delle transazioni ACID sui database relazionali... dimenticate tutto: qui si riparte da zero!
L'atomicita' di una operazione e' garantita a livello di singolo documento. Il documento puo' contenere altri documenti in modalita' embedded e solo in questo modo si puo' garantire l'atomicita' della transazione su piu' documenti.
{ numero: 17, data: new Date(), importo: 69.00, cliente: { RagSoc: "Acme ltd", vat: "12345678901" } }
L'atomicita' della gestione della fattura 17 e del cliente Acme e' cosi' garantita. Ma si tratta comunque di un solo documento definito nella stessa collection... Per modificarli assieme si e' denormalizzato il disegno del DB e se un cliente variasse (eg. la partita iva perche' errata) sarebbe necessario correggere ad una ad una tutte le fatture!
Le istruzioni che trattano piu' documenti (eg. update) vengono comunque eseguite
per un documento alla volta e garantiscono l'atomicita' solo a livello del singolo documento.
Eventuali gestioni piu' complesse, requisiti differenti, ...
possono solo essere gestiti applicativamente.
Il write concern descrive il livello di garanzia che MongoDB
fornisce quando riporta il successo di un'operazione di scrittura.
A seconda delle impostazioni, scelte nell'applicazione/configurazione,
si possono avere diversi livelli di sicurezza sulla consistenza dei dati e,
molto importante, differenti tempi di risposta.
Per ottenere le migliori prestazioni basta... non controllare nulla!
Il write concern ha tre metriche impostabili:
La scelta sulla gestione delle transazioni e' quindi demandata alla progettazione che deve esplicitamente richiedere il write concern corretto alla tipologia di applicazioni utilizzate ed adottare un opportuno disegno della base dati.
Riassumendo le scelte di MongoDB sulle transazioni sono molto semplificative rispetto ai tradizionali DB relazionali. Non sono presenti le classiche proprieta' ACID (Atomicity, Consistency, Isolation e Durability) se non con limiti molto significativi. Queste scelte pero' consentono un'elevata velocita' nelle scritture, la distribuzione/replica dei dati e la continuita' operativa anche nel caso di partizionamento della rete. Insomma, come dice il famoso teorema CAP, bisogna sapersi accontentare! A proposito di CAP: MongoDB e' considerato un CP di default e puo' essere configurato come Eventually consistent.
Dal punto di vista del sistema operativo MongoDB si presenta come un unico processo mongod in ascolto sulla porta TCP 27017. In realta' all'interno del processo operano diversi thread con compiti specifici. I thread di background sono una decina, inoltre ogni nuova connessione genera diversi thread. Ad ogni host che si collega vengono assegnate (per default) 10 connessioni gestite su un thread pool interno
I path utilizzati da MongoDB cambiano a seconda della piattaforma.
Su Linux la configurazione
si trova su /etc/mongod.conf, i file di database su /data/db, i log su
/var/log/mongo.
Su MAC OS X i path sono analoghi ma, se installato con Homebrew,
/usr/local/var/mongodb e' la directory principale con il dbPath ed il file mongod.lock,
la configurazione e' su /usr/local/etc/mongod.conf
mentre il log e' /usr/local/var/log/mongodb/mongo.log.
Su Windows il dbPath e' C:\DATA\DB.
Nella directory DBpath si trovano tutti i dati.
Per ogni database e' presente un file .ns (Name Space)
ed uno o piu' extent numerati progressivamente a partire da 0 (datafile).
Il NameSpace contiene i puntatori ai dati effettivi mantenuti negli extent
(che sono liste linkate contenute nei datafile e contengono uno o piu' document).
Gli extent sono di grandi dimensioni poiche' MongoDB utilizza la tecnica della preallocazione
con dimensione esponenzialmente crescente fino a 2G.
Inoltre MongoDB non cancella fisicamente i documenti cancellati a meno che non venga esplictamente
eseguita un'operazione di compact() o di repairDatabase().
Gli indici sono implementati come Btree ed ospitati sugli stessi datafile ma su extent loro dedicati.
db.stats() riporta lo stato corrente di tutte le allocazioni (eg. numero totale di documenti,
spazio allocato sul file system, ...).
Lo storage engine MMAPv1 utilizza le tecnica dei memory-mapped files memorizzare i dati.
Si tratta della stessa tecnica utilizzata dalla buffer file cache di Linux,
e' quindi molto efficiente ed affidabile.
Gia' con una base dati di medie dimensioni
e' normale per MongoDB utilizzare tutta la memoria disponibile sul sistema!
Lo storage engine wiredTiger e' un nuovo engine introdotto dalla versione 3.0
che implementa i lock a livello di documento e consente la compressione dei dati.
WT e' particolarmente efficiente nelle attivita' di scrittura poiche' non
effettua il journaling.
La D dell'ACID viene garantita con il journaling delle operazioni.
Se non richiesto dal write concern, il Journal viene aggiornato ogni 100 millisecondi
(default modificabile con l'opzione --journalCommitInterval).
E' importante ricordare che le transazioni sono atomiche solo a livello
di un singolo documento.
L'architettura di un Replica Set non e' complessa e prevede semplicemente un
numero di nodi (preferibilmente dispari) su cui ospitare i dati.
Il nodo primario mantiene su una speciale capped collection (local.oplog.rs) le modifiche
occorse ai dati. La dimensione dell'oplog e' definita al momento dell'avvio della
prima replica; la dimensione e' proporzionale allo spazio libero disponibile
e comunque dimensionata tra 1GB e 50GB.
L'oplog non puo' essere variato di dimensioni in modo dinamico ma il resize richiede una procedura specifica.
I nodi secondari utilizzano un heatbeat e mantengono una copia dell'oplog.
In modo asincrono i membri secondari applicano a loro volta le stesse operazioni.
La configurazione del cluster di sharding richiede diversi componenti:
router, config server e shard.
I router sono i processi mongos cui le applicazioni si collegano e che indirizzano
gli accessi. I config server debbono essere 3 (in una configurazione di produzione)
e sono normali database MongoDB
che mantengono la configurazione del cluster.
Gli Shard sono i database MongoDB che mantengono i dati e sono almeno due,
ma tipicamente sono di piu' per disporre di una maggiore scalabilita'
Ciascuno shard, per garantire la durability dei dati, e' a sua volta un replica set.
In numero di nodi per ciascun replica set e' di almeno 3 ma, se si vogliono effettuare
i backup in linea senza perdere la durability delle transazioni, sono necessari 5 nodi
per ogni RS.
In una configurazione in shard tipicamente i processi mongos rispondono sulla porta 27017
(in questo modo e' trasparente l'accesso alle applicazioni che non cambiano nulla
rispetto ad una configurazione con nodo singolo) ed i processi mongod utilizzano la
porta 27018. Ma tutte le porte sono facimente modificabili/configurabili.
Le attivita' di balancing degli shard vengono effettuate da un thread specifico
chiamato balancer che puo' essere attivato da un qualsiasi nodo mongos.
Il balancer imposta un lock sul database config [NdE per controllarlo utilizzare
il comando: db.locks.find( { _id : "balancer" } ).pretty() ], ed effettua lo spostamento
dei chunk delle collection a seconda della configurazione presente.
Per ovvie ragioni di consistenza quando il balancer e' attivo non possono essere eseguiti i backup.
E' pero' possibile definire una finestra temporale in cui il balancer viene eseguito.
Il GridFS memorizza i file in due collection: fs.files che contiene i metadati e fs.chunks che contiene i dati binari.
In una installazione tipica viene installato il demone (mongod) che gestisce il DB, la shell di accesso (mongo), il demone per lo sharding (mongos) ed i tool di gestione (mongoimport, bsondump, mongodump, mongoexport, mongofiles, mongooplog, mongoperf, mongorestore, mongostat, mongotop). Viene anche impostato il file di configurazione di default (eg. /etc/mongod.conf):
systemLog: destination: file path: /usr/local/var/log/mongodb/mongo.log logAppend: true storage: dbPath: /usr/local/var/mongodb net: bindIp: 127.0.0.1
Con un'installazione Linux vengono automaticamente predisposti gli script di start/stop dei servizi.
Mentre su OS X i comandi sono:
Per effettuare uno shutdown corretto di MongoDB ci sono diverse modalita'... ma non il kill -9:
Mentre le applicazioni che utilizzano MongoDB vengono realizzate con linguaggi diversi ed accedono alla base dati mediante un apposito driver, il DBA tipicamente accede al sistema con la shell mongo che abbiamo gia' utilizzato fino ad ora! L'amministrazione di MongoDB, oltre alla conoscenza del DB, richiede conoscenze del sistema operativo ospite per diagnosticare velocemente gli eventuali problemi e del particolare disegno della base dati MongoDB per suggerire ai programmatori eventuali ottimizzazioni.
La gestione dello spazio e l'ottimizzazione dell'IO sono fondamentali per MongoDB.
MongoDB, come la maggioranza dei DB NoSQL, non effettua automaticamente la compattazione
degli spazi.
Va utilizzato il comando db.repairDatabase() che e' lento, blocca l'accesso
alla base dati e richiede la disponibilita' di circa il doppio dello spazio.
Quando si utilizzano le repliche un'operativa comune e' quella di sganciare una replica,
effettuare la compattazione, risincronizzare la replica (che ora e' stata compattata) e
quindi sganciare il primario e ripetere l'operazione su tutti i nodi.
Il comando db.collection.compact() ottimizza i dati all'interno di una collection
ma non rilascia spazio al sistema operativo.
Sono possibili due tipologie di backup: fisico o logico.
Il backup fisico si effettua a database spento e deve comprendere tutti i datafile.
In alternativa e' possibile utilizzare uno snapshot del File System, in questo caso il file system
deve contenere sia il dbPache che i Journal.
Il backup logico si effettua con l'utility mongodump (ed il restore con l'utility mongorestore).
L'utility mongoexport effettua un backup in formato JSON o CSV; tuttavia poiche' il formato JSON
non rappresenta appieno tutte tipologia di dati BSON, benche' utile per la gestione di dati,
non va utilizzato come strumento di backup.
Su MongoDB per default l'autenticazione non e' abilitata (parametro --auth). Ovviamente per un'installazione di produzione e' necessario abilitarla! Le utenze sono definite per database, le utenze del database admin possono agire su tutti i database. L'algoritmo per la generazione della password e' SHA256(MD5(utente:mongo:password)) senza salt.
Dal punto di vista delle ottimizzazioni e del tuning sono molti gli aspetti
da considerare con MongoDB. Tuttavia le prestazioni dell'IO sono spesso il
primo elemento da analizzare (eg. utilizzare XFS).
Quando si utilizzano piu' sistemi in replica e' opportuno che i nodi
abbiano caratteristiche analoghe.
MongoDB puo' essere installato su sistemi virtualizzati ed in cloud.
Le versioni attuali sono state ottimizzate anche per supportare sistemi
con alta latenza nell'accesso allo storage.
Anche i Replica Set hanno configurazioni specifiche per distribuire
le elaborazioni su piu' datacenter (eg. priority 0 ed hidden per i siti di DR).
Infine vediamo alcuni script utili per l'amministrazione:
Sviluppato inizialmente (2007) da 10gen come un componente PaaS
e' stato trasformato in un progetto Open Source (2009)
con un supporto e servizi di livello Enterprise.
Esistono diverse categorie di database NoSQL,
i due piu' noti database NoSQL documentali sono MongoDB e CouchDB:
entrambi sono velocissimi (a scapito delle proprieta' ACID),
memorizzano i dati in BSON/JSON,
sono scalabili in modo nativo su piu' nodi e non forniscono un'interfaccia SQL.
MongoDB si e' rapidamente imposto sul mercato,
diventando uno tra i piu' usati database non relazionali:
e' infatti considerato il piu' diffuso database NoSQL,
non solo come database documentale ma su tutte le catogorie.
Le versioni pari di MongoDB corrispondono a quelle di produzione mentre quelle dispari sono di sviluppo ed presentano nuove funzionalita'. L'attuale versione produzione [4Q16] e' la 3.2. La storia delle versioni e' riassunta in questo documento. Nonostante l'introduzione recente di MongoDB le sue versioni presentano variazioni architetturali significative (eg. algoritmo di write, replica set) ed e' quindi fortemente consigliato utilizzare le ultime versioni rilasciate.
La versione di MongoDB descritta in questo documento e' obsoleta... anche se la maggioranza delle informazioni contenute in questo documento sono ancora valide si consiglia la lettura del documento MongoDB 4.0.
Per una documentazione piu' dettagliata si rimanda al sito ufficiale di MongoDB ed in particolare al manuale.
Titolo: Introduzione a MongoDB
Livello: Medio
Data: 14 Luglio 2014
Versione: 1.0.2 -
14 Febbraio 2016
Autore: mail [AT] meo.bogliolo.name