La replicazione nativa: scenari di utilizzo

di Patrizio Tassone – Pubblicato su Linux&C. n° 54.

La replicazione nativa di MySQL

La replicazione nativa di MySQL offre ottime opportunità di scalabilità, come si è potuto intuire nella prima puntata di questo articolo, e non è un caso che portali ad altissimo traffico quali YouTube, Flickr, Wikipedia, Digg, del.icio.us, solo per citare i più noti, utilizzino questa tecnologia per servire migliaia, talvolta milioni di utenti ogni giorno.
In questo articolo cercheremo di indagare più a fondo sugli scenari che si possono avvantaggiare della replicazione, sia in termini di scalabilità che di affidabilità.

Scenari di utilizzo

La replicazione, come si è visto, dispone di un host deputato a raccogliere tutte le connessioni in scrittura da parte dei client, che vengono riversate su un file di log (direttiva log-bin in my.cnf) che poi viene letto dagli altri host slave, riallineando tutte le basi di dati in un paradigma di shared-nothing, che non vede la presenza di costosi dischi condivisi.
L’utilizzo principe di una tecnologia come questa è relativa all’ambito web, in particolare come layer di back-end per la memorizzazione delle informazioni di portali ad alto traffico (contenuti dinamici, utenti, metadati di interesse statistico utili per il datawarehouse con finalità di marketing), dove le operazione di selezione pura (select) sono in un ordine di grandezza superiore rispetto alle scritture effettuate.

lc54_figura1
Lo scenario più frequente, e più semplice, è quello visibile in figura 1, che vede la presenza di un bilanciatore, rigorosamente ridondato, che suddivide il traffico in entrata su più web server che costituiscono, in questo caso, il lato applicativo dell’infrastruttura (nel caso più semplice, Apache + PHP/Perl/Python/Ruby o quant’altro); la batteria di server con Apache, in numero variabile a seconda del numero di accessi previsti, della banda in ingresso, della complessità delle operazioni applicative, sono connessi direttamente al back-end di memorizzazione dei dati: un host, il Master, accetterà le scritture mentre gli altri host verranno interrogati nel caso di letture.

lc54_figura2

lc54_figura2bis

Lo scenario è leggermente più complesso nel caso in cui, tra la batteria dei web server e il back-end, si frapponga un layer di application server (figura 2, scenario tipico in ambiti Java), dove risiede parte della logica applicativa: lato database non vi è molta differenza, se non che l’aggiunta di un layer intermedio offre una scalabilità maggiore e un minor numero di connessioni concorrenti.
Per sfruttare appieno le possibilità offerte da questa infrastruttura, le applicazioni devono avere una minima conoscenza dell’architettura che sarà utilizzata in produzione: in verità, le uniche informazioni di cui devono disporre gli sviluppatori sono relative alla presenza di due tipi di connessioni, una in scrittura (verso il master), una in lettura (bilanciata fra più slave).
Non necessariamente i layer superiori devono conoscere il numero esatto di slave presenti nella infrastruttura, in quanto sistemi di virtualizzazione più o meno complessi possono astrarre questa informazione.
Una prima possibilità, forse la più semplice da implementare, almeno nella forma base, vede utilizzo del DNS che, in round robin, bilancia gli accessi tra gli slave, con un file di zona che conterrà qualcosa di simile alle righe seguenti:


master A 192.168.1.100
slave A 192.168.1.110
slave A 192.168.1.120
slave A 192.168.1.130

I client potranno riferire le connessioni a master.retelocale.lan per il master e slave.retelocale.lan per i diversi slave e, a meno di cache-ing degli IP, potranno bilanciare le letture sui diversi database server.
In alternativa, possono essere utilizzati direttamente appliance di bilanciamento, realizzate con hardware dedicati (Cisco, Alteon i più diffusi) o con sistemi software (Linux LVS, Balance e similari), inserendo tutti gli slave dietro a tale device.
In caso di caduta di un nodo slave “nascosto” dal balancer, la configurazione dell’apparato dovrà provvedere ad escludere l’IP del server caduto; nel caso di uso puro di DNS, sarà necessario implementare un servizio di monitoring e far eseguire aggiornamenti dinamici ai record DNS utilizzando nsupdate.
In alternativa, è possibile specificare direttamente all’interno della logica applicativa gli IP dei server database, anche se questo comporta una certa scomodità nel caso in cui venga modificata dinamicamente l’architettura, in particolare in caso di failure.

Caduta del master

La divisione in master e slave fa sorgere immediatamente una prima osservazione, più che lecita: cosa accade se il master ha una failure grave e smette di funzionare?
La notizia positiva, che in questi frangenti fa scorgere un accenno di sorriso al project manager incaricato della supervisione del portale, è che il servizio continua ad essere erogato, e l’impatto sugli utenti è minimo, almeno per quanto riguarda la fruizione dei contenuti.
Se stiamo parlando di una portale di informazione o di un forum ad altissimo traffico, gli utenti potranno ancora leggere gli articoli e i post, effettuare ricerche, ma non potranno inserire commenti – operazione che comporta una scrittura sul master.
MySQL ovviamente dispone dei meccanismi di recover di un simile evento: per avere la certezza (quasi) assoluta di non perdere alcun record, la soluzione consigliata potrebbe essere quella di mettere sotto HA classico il nodo master, e solo questo, utilizzando storage condiviso, eventualmente remotizzato con iSCSI o AoE, che permette una buona affidabilità offrendo, nel contempo, una riduzione dei costi non indifferente.
In alternativa, si può optare per l’elezione di uno degli slave al ruolo (provvisorio) di master, operazione che vedremo tra un attimo in pratica: l’idea è quella di promuovere uno slave al ruolo di master, eventualmente sospendendolo dal ruolo di slave fino a quando il master non sarà completamente reintegrato (non obbligatorio, a seconda del carico un server MySQL può compiere egregiamente entrambe le operazioni).
In genere, quando questa è la policy che viene decisa in fase di progetto, è preferibile identificare uno slave come “candidato master”, di solito il server dotato di maggiori risorse computazionali e un MTBF superiore.
E’ però necessario informare tutti gli slave che l’host dal quale stavano leggendo gli aggiornamenti è cambiato, e che continuino la lettura da un altro nodo.
Sostituzione del master
La caduta del master impedisce l’aggiornamento della base dei dati, e quando il fermo si prolunga oltremodo può essere conveniente utilizzare uno degli slave e promuoverlo a master per accettare le nuove scritture da parte dei client.
La modifica della configurazione deve essere propagata agli altri host della replicazione: ricordate, infatti, che nel file di configurazione my.cnf degli stessi era indicato l’host cui connettersi e dal quale leggere il log-bin? Se questo viene a mancare tutta la configurazione deve essere aggiornata, comprendendo anche un reset dello slave neo-eletto, per essere sicuri che altre informazioni presenti accidentalmente nel log-bin dello stesso non vadano ad alterare la base dei dati in modo improprio. Normalmente la sostituzione del server è provvisoria e, proprio per questo, la configurazione statica, quella presente nel file di configurazione, non verrà modificata.

lc54_figura3

Lo scenario, a livello di IP, è quello visto la volta scorsa, e riproposta in figura 4: nell’esempio il il .100 cade, il .110 prende il posto del .100 e il .120 si collega al 110 per leggere le query di update.
Per prima cosa sullo slave01 (IP 110) sarà obbligatorio controllare che la direttiva log-bin sia presente nel file di configurazione: per uno server slave tale file di log non è necessario ma ora, diventando master, diventa fondamentale affinché la replicazione diventi possibile.
Nel caso la direttiva log-bin non fosse attiva, dopo l’inserimento della stessa è doveroso un riavvio del server.
Le ulteriori operazioni da compiere, a MySQL ovviamente avviato, sono le seguenti:


mysql-110> reset master;
mysql-110> slave stop;

Con la prima istruzione si effettua un reset del server nella sua veste di master: serve a ripulire i log, nel caso qualche istruzione fosse presente, magari ereditata dalla fase di installazione. Il secondo comando impartito, invece, blocca la replicazione, visto che il polling sull’IP ormai caduto continua senza sosta: questa operazione non è fondamentale, ma lo diventa nel caso in cui si attui l’IP aliasing relativo all’IP del master caduto, per evitare comportamenti anomali.
Sul secondo slave, invece, le operazioni sono diverse: per prima cosa fermiamo la replicazione


mysql-120> slave stop;

Adesso si può effettuare il cambio dei parametri necessari per la nuova connessione: in realtà, poiché gli utenti sono stati già configurati anche sugli slave (all’epoca della creazione facemmo replicare anche l’istruzione GRANT, si veda lo scorso numero), l’unico cambio da effettuare è solo l’IP del master.


mysql-120> CHANGE MASTER TO MASTER_HOST='172.16.2.110';

La sintassi è piuttosto chiara: nel caso in cui venissero cambiati altri parametri, quali username, password, il file di binlog da leggere, oppure il timeout, si dovranno inserire le informazioni da modificare una dopo l’altra nell’istruzione precedente, separate da virgole; dal manuale ufficiale:


mysql> CHANGE MASTER TO
MASTER_HOST='master2.mycompany.com',
MASTER_USER='replication',
MASTER_PASSWORD='bigs3cret',
MASTER_PORT=3306,
MASTER_LOG_FILE='master2-bin.001',
MASTER_LOG_POS=4,
MASTER_CONNECT_RETRY=10;

A questo punto non rimane che far ripartire la replicazione:


mysql-120> slave start;

Interrogando lo stato dello slave, ci possiamo sincerare del fatto che tutto sia in ordine:


mysql-120> SHOW SLAVE STATUS;
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.1.110
Master_User: slave02
Slave_IO_Running: Yes
Slave_SQL_Running: Yes

Il Master_Host è cambiato, adesso compaiono le coordinate del vecchio slave, ora diventato Master: una prova semplice di creazione di una nuova tabella, o l’inserimento di un record potrà fugare ogni dubbio circa la funzionalità delle modifiche apportate.
Proviamo a riavviare a far ripartire MySQL sull’host 120, che svolge ancora il compito di slave anche se è cambiato il master al quale connettersi, e a lanciare un nuovo SLAVE STATUS: il server starà ancora replicando ma dall’IP che gli è stato passato con il CHANGE MASTER e non da quello presente nella configurazione!
Questo fatto potrebbe non essere piacevole ai più, anche perché sembrerebbe trovare poche spiegazioni logiche: sarà sufficiente controllare il file master.info presente nella directory di MySQL per capire cosa sia successo:


slave02# cat master.info
[...]
172.16.2.110
slave02
pass02
[...]

master.info riporta tutte le informazioni relative alla replica dal master, che hanno la precedenza rispetto a quelli presenti nel file di configurazione: tale file, a differenza di my.cnf, viene influenzato dai comandi di CHANGE MASTER ed è persistente al riavvio del server.
Nel caso in cui si voglia effettuare un reset completo dello slave è opportuno eseguire


mysql-120> SLAVE STOP; RESET SLAVE; SLAVE START;

perché la configurazione venga riletta da my.cnf.
In questo caso si ottiene quanto segue:


mysql-120> show slave status \G
Slave_IO_State: Connecting to master
Master_Host: 192.168.1.100
Master_User: slave02
Master_Port: 3306
Slave_IO_Running: No
Slave_SQL_Running: Yes

E’ tornato tutto nella norma: poiché il master è ancora fermo, la replicazione si è bloccata, come testimonia il No relativo a Slave_IO_Running.

Aggiornamento dei client

Il problema maggiore nel failure del Master non è elevare a tale ruolo un altro dei database server che, come avete visto, è piuttosto semplice. Certo, ci sono altre considerazioni da fare per un servizio veramente a prova di errore, ma sono considerazioni molto specifiche, una per tutte: chi è lo slave più aggiornato? Essendo asincrona, anche se molto veloce, non necessariamente gli slave sono tutti allo stesso livello.
Il problema maggiore, si diceva, è quello di informare del cambiamento gli application server o la batteria di web server: nel caso analizzato inizialmente di una connessione verso un indirizzo risolto dal DNS, tramite opportuni monitor che valutino lo stato dei servizi attivi è possibile aggiornare dinamicamente il record con nsupdate:


# nsupdate -k chiave.key << EOF
update delete master.retelocale.lan A
update add master.retelocale.lan 300 A 172.16.1.1
send
EOF

Ovviamente il tutto andrà eseguito dopo aver concesso le necessarie autorizzazioni ed aver configurato i permessi affinché questa operazioni possa avvenire in tutta sicurezza. In questo modo i client continueranno a collegarsi a master.retelocale.lan, ma l’indirizzo risolto questa volta sarà diverso.
Anche nel caso degli slave si può effettuare una operazione simile a quella appena vista, facendo attenzione affinché il round robin venga mantenuto integro.
Se invece la connessione avviene tramite IP diretto (oppure hostname presenti in /etc/hosts), che magari è inserito all’interno di file di configurazione delle applicazioni difficilmente modificabili, è opportuno utilizzare sistemi di clustering, quali ad esempio heartbeat, affinché l’IP venga migrato dal master caduto verso lo slave neo-eletto a master, in modo che non si perda l’associazione IP_master->Host_master.
Se questo è lo scenario che viene utilizzato, è consigliabile però decidere a priori qual è l’host candidato all’elezione, in modo da configurare il servizio di HA solo su questo, attivando gli opportuni sistemi di monitoraggio (dalla connessione di rete passando per lo switch, a cavi cross, a cavi seriali null-modem, nel caso la vicinanza delle macchine lo permetta, ecc.), affinché la migrazione avvenga senza particolari problemi.

Ritorno del Master

Già, prima o poi quel server dovrà tornare: a seconda di quanto passa dal fermo, se poche ore o qualche giorno, la base di dati presente sul master potrebbe essere poco aggiornata o estremamente disallineata: la linea generale vuole che il vecchio master entri nella replicazione come uno slave, in modo da leggere gli aggiornamenti dal nuovo master.
Nel caso però sia passato molto tempo, i log potrebbero essere stati ruotati e cancellati, e quindi non vi è più alcuna possibilità di aggiornarsi da essi.
E’ consigliabile ripristinare da un check point ben noto, il più recente possibile: allo scopo, normalmente, è deputato l’ultimo backup che, sincronizzato con la rotazione del log, permette di effettuare un reintegro in tutta sicurezza.
Se neppure questa soluzione è fattibile, a meno di complessi script di sync che agiscono sulla base di dati, c’è da effettuare la copia bruta delle informazioni da un nodo verso il vecchio Master.
Quando l’host rientrato sarà allineato alla base dei dati, si potrà prevedere uno switch per ripristinare la situazione iniziale, magari durante un fermo di servizio programmato; l’urgenza di tale operazione è da valutare di volta in volta, in particolare a seconda del tipo di server presenti: se sono tutti equivalenti, non ci dovrebbero essere problemi nell’attesa, mentre diversa è la valutazione nel caso in cui, ad esempio, il vecchio master sia un quadriprocessore e gli slave solo dei semplici mono-processori.

Affidabilità e solo affidabilità

Si parlava di scalabilità, ma sono molti i casi in cui un database MySQL è più che sufficiente, e quello che si vuole ottenere è solo migliorare l’affidabilità per non rischiare di vedere il servizio cadere magari per un guasto hardware.
Un possibile scenario vede la presenza di due server database in modalità attiva/passiva o attiva/attiva, con uno o due IP virtuali utilizzati per accettare le connessioni e che, in caso di caduta di uno dei nodi, vengono migrati secondo necessità.

lc54_figura4

Lo schema di figura 5 è il più semplice: i due nodi hanno un indirizzo privato per la gestione degli stessi (di solito posizionati su una rete di management separata, quando possibile), un indirizzo privato per la replicazione e dispongono di un indirizzo virtuale al quale arrivano le connessioni dal layer applicativo (siano essi Apache o Application Server).
L’IP virtuale viene assegnato, in partenza, a uno soltanto dei due server.
Tra i due nodi è attiva una replicazione che, per la particolarità della configurazione, viene detta circolare: in breve, ogni nodo è, contemporaneamente, master e slave dell’altro e ogni scrittura effettuata sul server .110 viene propagato sul server .120 e viceversa.
La configurazione è analoga a quelle già viste, anche se in modalità “incrociata”:


# Nodo 1 - IP reale 172.16.2.110
master-host = 172.16.2.120
master-user = nodo01
master-password = pass01


# Nodo 2 - IP reale 172.16.2.120
master-host = 172.16.2.110
master-user = nodo02
master-password = pass02

La direttiva log-bin deve essere abilitata su entrambi i server.
Si riavviano i due nodi per far leggere la nuova configurazione: eventualmente ci sarà da impartire reset master e reset slave, a seconda dei dati presenti nei file di log degli stessi. La situazione, alla fine, sarà simile alla seguente:


mysql-110> show slave status \G
Slave_IO_State: Waiting for master to send event
Master_Host: 172.16.2.120
Master_User: nodo01
Master_Log_File: mysql-bin.000001


Mysql-120> show slave status \G
Slave_IO_State: Waiting for master to send event
Master_Host: 172.16.2.110
Master_User: nodo02
Master_Log_File: mysql-bin.000001

Tutto sta replicando correttamente. Creiamo sul primo nodo un database di test:


mysql-110> create database test_ha;

Nel secondo creiamo una tabella:


mysql-120> use test_ha;
mysql-120> create table tab_ha (
id int auto_increment primary key, testo text);

Tutto sembra funzionare perfettamente. Se proviamo ad inserire alcuni record


mysql-110> insert into tab_ha values (
NULL, "primo record?");
mysql-120> insert into tab_ha values (
NULL, "secondo record?");

vengono entrambi replicate senza problemi. Ma cosa succederebbe se le scritture venissero effettuate contemporaneamente? Un modo veloce per effettuare una simulazione di un accesso concorrente alla base dati, considerando la velocità intrinseca del meccanismo di replicazione, consiste nel bloccare la replica in toto, e farla poi riprendere successivamente:


mysql-110> stop slave;
mysql-120> stop slave;

Inseriamo sia sul primo server che sul secondo un record:


mysql-110> insert into tab_ha values (
NULL, "terzo record?");
mysql-120> insert into tab_ha values (
NULL, "terzo record?");

Gli auto_increment dei due server hanno attribuito lo stesso ID progressivo:


mysql-110> select id, testo, @@server_id
from tab_ha where id=3;
| id | testo | @@server_id |
| 3 | terzo record? | 110 |


mysql-120> select id, testo, @@server_id
from tab_ha where id=3;
| id | testo | @@server_id |
| 3 | terzo record? | 120 |

Facendo ripartire la replicazione, il risultato non può che essere disastroso.


mysql-110> slave start;
mysql-110> show slave status \G
Slave_IO_Running : Yes
Slave_SQL_Running : No
Last_Errno : 1062
Last_Error : Error 'Duplicate entry '3' for
key 1' on query. Default database: 'test_ha'.
Query: 'insert into tab_ha values (NULL, "terzo record?")'

Risultato analogo per l’altro nodo. Ecco quindi che l’HA gestita con due serve attivi può dare sì benefici (due server che bilanciano le connessioni anche in scrittura), ma anche qualche piccolo problema al contorno, risolvibile solo se ben gestito.
La soluzione a questa empasse può avvenire in due modi: il primo, più semplice, è di non permettere di utilizzare i due server attivi contemporaneamente, ma solo uno alla volta (approccio attivo/passivo). Ecco che l’utilizzo di un solo IP virtuale aiuta nella configurazione, in quanto i client possono connettersi solo a quell’IP, e quell’IP risulta assegnato solo su uno dei server gestito da meccanismi di heartbeat.
Nel caso, invece, che si vogliano utilizzare i due server contemporaneamente, è opportuno utilizzare le direttive auto_increment_increment e auto_increment_offset, che permettono di impedire un conflitto di ID: il primo valore indica di quanto aumentare per ogni inserimento il valore del campo id, il secondo indica il valore di partenza dell’auto_increment stesso (che comunque deve essere inferiore o uguale al valore di auto_increment_increment)


mysql-110> show variables like "auto_increment%";
| Variable_name | Value |
| auto_increment_increment | 1 |
| auto_increment_offset | 1 |

E’ ovvio che, in caso di due server, inserire come valore di partenza 1 e 2, rispettivamente per il primo e secondo server, con un incremento di 2 ad ogni insert, si ottiene che il primo server utilizzi sempre i valori ID dispari, e il secondo server tutti gli ID pari, impedendo qualsivoglia conflitto.


mysql-110> set auto_increment_increment=2;
mysql-110> insert into tab_ha values (
NULL, "che id avrà?");
mysql-110> insert into tab_ha values (
NULL, "che id avrà?");
mysql-110> select * from tab_ha;
[...]
| 3 | terzo record? |
| 5 | che id avrà? |
| 7 | che id avrà? |

Questa soluzione, estremamente semplice, impedisce i problemi visti sopra, ma farà storcere il naso a chi utilizza i campi auto_increment come progressivo di inserimento (un ordinamento su id, nell’esempio, non garantirà più l’ordinamento temporale dei record, sarà necessario utilizzare i timestamp, meglio se valorizzati dai trigger di MySQL 5.0) e chi li utilizza come ID seriale per svariati compiti (no, non vanno usati per fatture o ricevute o altri documenti fiscali!), dovrà optare per qualcosa di diverso.
Il caso presentato è estremamente semplice, ma ci sono altri scenari, più complessi da analizzare, che potrebbero portare a situazioni analoghe, e che meritano uno studio approfondito.
Nel caso uno dei due nodi abbia una failure, l’altro continuerà ad essere operativo: al rientro del server caduto, esso si dovrà agganciare alla replicazione e aggiornerà la propria base di dati: è ovvio che, fino a quando il server non si sarà perfettamente allineato, è opportuno evitare che venga interrogato, perché quasi certamente fornirebbe informazioni non corrette e/o aggiornate.
L’utilizzo dell’IP virtuale può risolvere, almeno in parte, molti dei problemi visti sopra, perché inserisce una sorta di “semaforo” alla connessione, rendendola di fatto esclusiva verso un solo server (anche se molto dell’affidabilità dipende dalle tecniche con la quale la si implementa).

Conclusioni

Una soluzione scalabile e affidabile è fondamentale per chi non vuole avere limiti di crescita nel proprio business: a volte si è costretti a sperare di non avere troppo successo perché, altrimenti, le richieste sarebbero così tante da non poter essere servite. MySQL offre tecnologie estremamente semplici da gestire, affidabili e ad un costo più che accettabile, zero!
Nelle prossime puntate, se i lettori saranno interessati, si potrà osservare da vicino il nuovo motore NDB e le nuove soluzioni di bilanciamento e high availability basate su MySQL Cluster “five nine” 99.999%.

Riquadro 1: Commit a due fasi

Nonostante la replicazione sia estremamente veloce, la possibilità che, in caso di caduta del master, qualche record non venga inoltrato agli slave esiste, è poco probabile, ma è reale. La replicazione nativa, come più volte spiegato, è asincrona, e in particolare non implementa in alcun modo la cosiddetta two-phase-commit: se una transazione utilizza questa tecnica, essa dà esito positivo solo quando i due nodi (in genere, almeno due, poi possono essere in numero maggiore) effettuano l’operazione con successo garantendo così che, in caso di caduta del server, almeno l’altro server abbia eseguito quell’operazione con successo.
Quando il valore attribuito alla perdita di un record è estremamente elevato (ad esempio, una transazione bancaria) può essere conveniente utilizzare una soluzione HA più sicura e estremamente più costosa, come uno storage condiviso tra i nodi… oppure utilizzare il MySQL Cluster 99.999%, che implementa la two-phase-commit nativamente.

MySQL Cluster 99.999%: affidabilità e prestazioni

di Patrizio Tassone, pubblicato su Linux&C. n° 61.

MySQL Cluster 99.999%: affidabilità e performance prima di tutto

Quando i dati dell’azienda sono nelle mani di un database, quel database deve essere una roccia. A seconda del tipo di attività in cui quella società si trova ad operare si possono anche tollerare fermi di servizio per manutenzione, per aggiornamento, anche per eventuali failure di qualche tipo; ci sono però ambiti dove niente di tutto questo è neppure ipotizzabile, e dove serve l’affidabilità di una soluzione “five nine”: 99.999% di uptime, che significano qualcosa come poco più di 5 minuti di stop in un anno.
MySQL, parlo del database standard, è già un prodotto estremamente affidabile e, senza scomodare soluzioni iper-ingegnerizzate, con le opportune accortezze, con utilizzo della replicazione e di soluzione di clusterizzazione classiche, si raggiungono (e parlo per esperienza personale) livelli di uptime prossimi al 100%.
Ci sono ambiti, però, dove un approccio del genere può non bastare: sono gli stessi ambiti dove i limiti della replica nativa di MySQL viene amplificata, dove neppure le patch relative al semi-sync di un gigante come Google sono sufficienti a risolvere il problema, dove neppure la clusterizzazione standard – seppur raffinata – può dare una mano.
Serve un database dove tutti i nodi sono contemporaneamente attivi, indistinguibili tra loro, dove contattare l’uno o l’altro non fa differenza, dove non c’è tempo di propagazione delle modifiche come nella replica nativa, dove i nodi sono totalmente interconnessi, dove la caduta di un nodo risulta totalmente trasparente all’applicazione: poco importa se una soluzione di questo genere può avere – come è ovvio che sia – dei limiti, poco importa se, come disse uno dei massimi esperti MySQL scherzando – ma non troppo – “ogni riferimento ad un RDBMS è puramente casuale”.
Serve un database parallelo, e – possibilmente – serve non dover pagare licenze per decine di migliaia di euro a processore.
In definitiva, serve MySQL Cluster “five nine”, disponibile sotto GPL, ma anche sotto licenza proprietaria (proprio come MySQL classico).

Un database “in-memory”

Non troverete nessuna installazione di MySQL Cluster nelle top 10 dei database mondiali: http://link.oltrelinux.com/9932db
Il perché è molto semplice: questa versione di MySQL si appoggia interamente alla memoria dei sistemi che partecipano al cluster, rendendo di fatto impossibile il raggiungimento di dimensioni enormi della base di dati.
Ma questo, a pensarci bene, non è un problema. MySQL Cluster non è la soluzione per tutti i problemi, anzi, la scelta dovrebbe andare prima su altre soluzioni già note e stabili.
E’ bene ricordare – anche se dovrebbe essere scontato – che l’importanza di un database non è data dalla dimensione dello stesso. Pensate per un attimo all’autenticazione degli utenti in una rete VoIP, alla tracciabilità delle stesse telefonate, oppure alla localizzazione di un particolare numero di telefono nel sistema GSM, oppure ancora alla indicizzazione di parole chiave in un motore di ricerca: sono tutti ambiti dove un database raggiungerà a fatica qualche decina di GB, ciononostante un disservizio, ma anche solo un rallentamento nella risposta, potrebbe creare non propri problemi.
Sono tutti ambiti dove non ci sono query particolarmente complesse, dove spesso le interrogazioni non vanno oltre la ricerca a chiave, oppure la selezione dei record tra due valori per un determinato campo, oppure richieste di operazioni come somma o media: tutte operazioni, queste, facilmente parallelizzabili tra i vari nodi, e che permettono una risposta pressoché istantanea.

L’architettura del database

Per comprendere il funzionamento di MySQL Cluster è fondamentale dare uno sguardo alla architettura (figura 1) dello stesso, ai componenti e alle interazioni tra gli stessi.

lc61_figura1
La parola cluster fa intuire da subito che questa soluzione vede la presenza di più nodi: mentre il database classico può risiedere su un solo host, qua di host ne servono, per una prima configurazione, almeno quattro: due nodi storage, un nodo API (o SQL Node) e un nodo di management.
I nodi storage sono deputati esclusivamente della memorizzazione e alla interrogazione delle tuple. Leggono la configurazione dalla console di amministrazione (management console), e devono essere inizializzati alla prima esecuzione.
Il nodo SQL è il gateway che permette di effettuare le interrogazioni verso il sottosistema clusterizzato di memorizzazione delle informazioni: è costituito proprio dal classico MySQL che siamo abituati ad utilizzare, questo per permettere alle applicazioni di non essere modificate per beneficiare della soluzione di clustering.
Il nodo di management permette il controllo di tutto il sistema: provvede a consegnare ad ogni nodo la propria configurazione, permette l’esecuzione dei backup e dei restore, e tiene sotto controllo lo stato del sistema. Inoltre, in caso di failure dei nodi di storage, diventa l’arbitro (arbitrator) che decide quale sezione del cluster deve rimanere attiva (ruolo questo particolarmente critico in caso del cosiddetto split-brain) e quale invece deve effettuare uno shutdown per non compromettere l’integrità del database.
L’hardware che viene consigliato da MySQL AB è di fascia media: un sistema Linux o Unix, con due processori dual core, 16 GB di RAM per macchina (ma anche meno, dipende dalla dimensione del database), dei dischi veloci in raid 0+1 (quindi almeno 4 SCSI o SAS), Gigabit ethernet.
E’ di fondamentale importanza la velocità del network e l’affidabilità dello stesso: poiché i nodi sono totalmente interconnessi e scambiano messaggi di sincronia di continuo, la velocità della comunicazione tra i nodi rischia di diventare un collo di bottiglia. E’ così importante che, nel caso di utilizzo di più di 8 nodi, viene consigliata l’adozione di schede Dolphin, device a bassissima latenza molto utilizzati più nel campo del supercalcolo che nelle sale dati delle aziende.
E’ consigliabile, inoltre, che le schede di rete siano in channel bonding, e che i device ethernet (switch, schede di rete) siano anch’essi ridondati.
La scalabilità di questo genere di architetture è definita “scale-out”, che si contrappone all’altro approccio, lo “scale-up”: per aumentare la potenza di calcolo e lo spazio destinato alla memorizzazione dei dati è sufficiente aggiungere un nuovo server, anziché sostituire il vecchio sistema con uno più potente. Il costo dello scale-out è generalmente inferiore, e di un ordine di grandezza almeno, rispetto allo scale-up.

La memorizzazione distribuita delle informazioni: l’engine NDB

Per disporre dell’alta affidabilità richiesta, è fondamentale evitare qualsiasi single-point-of-failure e, per evitare i s-p-o-f, in un approccio share-nothing (senza cioè alcuno storage condiviso) è necessario duplicare l’informazione su altri nodi.
Per raggiungere un maggiore parallelismo l’informazione deve risiedere su più nodi possibile. Se si vuole massimizzare (potenzialmente almeno) il parallelismo, l’informazione deve risiedere su tutti i nodi.
Il nuovo storage NDBCluster applica queste due semplici regole meravigliosamente: scompone ogni tabella in “fette” (o slice), calcolando un hash particolare sulla chiave primaria, ed ogni slice viene memorizzato su un server diverso. Ogni slice ha un numero di repliche all’interno del cluster che può essere personalizzato, ma non può essere inferiore a 2 (sempre per non creare s-p-o-f). I nodi che dispongono della stessa slice fanno parte dello stesso node-group: fino a quando almeno un host all’interno del node-group è attivo, il cluster è funzionante anche se ha subito diversi failure.
In figura 2 possiamo vedere uno schema dello slicing.
Nello schema sono presenti quattro host fisici, e la tabella viene divisa in quattro slice: ogni slice viene replicato due volte, e per ogni replica esiste una copia “primaria” e una copia “di backup”.
Per la disposizione delle repliche, è evidente che Host 1 e Host 2 formino un node-group, in quanto – insieme – sono gli unici depositari della “fetta” F1 e F3.
Cosa potrebbe succedere in caso di crash dei sistemi o degli host? Qualche esempio chiarirà meglio il concetto:

  • In caso di caduta di Host 1, il database non subirebbe alcun danno, in quanto le informazioni in possesso del server caduto sono replicati anche sul secondo host.
  • In caso di caduta di Host 2, vale già quanto visto per Host 1.
  • In caso di caduta di Host 1 e INSIEME di Host 3, il database non rileverebbe alcun problema, perché i server rimanenti possiedono, globalmente, l’intera informazione F1, F3 (su Host 2) e F2, F4 su Host 4.
  • E’ altresì evidente che la caduta di un intero Node Group, cioè del Host 1 insieme a Host 2, oppure Host 3 insieme a Host 4, determinano lo shutdown del Cluster in quanto l’informazione è irrimediabilmente corrotta e sarà necessario provvedere al ripristino di un backup e al riavvio del sistema.

Come si può osservare dagli esempi fatti, MySQL Cluster va oltre la caduta del singolo server riuscendo, in alcune situazioni, a mantenere il servizio funzionante anche con la caduta di due nodi (che, su quattro sistemi, male non è di certo): tale sicurezza può essere aumentata incrementando il numero di repliche definite da file di configurazione.
Ovviamente, maggiore sarà la sicurezza minore sarà lo spazio a disposizione lasciato per i record (al pari di quanto avviene con i dischi in RAID).

Passiamo all’installazione

Dopo aver presentato a grandi linee le caratteristiche del database parallelo, è ora di iniziare a sperimentare un po’. Per il nostro esempio utilizzeremo una architettura composta da quattro server, un server di management (192.168.100.10), due server storage (192.168.100.20 e .30) e un nodo SQL (192.168.100.40), più che sufficiente per far apprezzare le novità del “five nine”.
L’esempio, puramente didattico, verrà eseguito in ambiente virtuale, e ogni singolo host avrà a disposizione “appena” 256 MB di memoria.
Sui nodi storage e sul nodo di interrogazione andrà installato MySQL, mentre sul nodo di management non sarà necessario provvedere all’installazione completa (noi faremo così per semplicità operativa): per l’esempio ho provveduto ad una installazione da file binari, nessun pacchetto quindi, in modo che sia più semplice seguire i pochi passaggi indipendentemente dalla distribuzione adottata, e nel riquadro 2 a pagina seguente trovate tutti i passaggi da effettuare.

Console di Management

La configurazione di tutti i parametri del cluster è centralizzata all’interno del nodo di management, dove è in esecuzione un apposito servizio: ogni nodo del cluster, sia esso storage o SQL, si connette a tale console e “richiede” la propria configurazione. All’interno della directory supports_files c’è un template di file di configurazione che possiamo utilizzare come prototipo, al pari di quanto avviene per MySQL.


# cp support-files/ndb-config-2-node.ini mysql-cluster.ini

Aprendo il file con un qualsiasi editor, si nota subito come siano presenti alcune sezioni con suffisso “default”, che specificano le impostazioni predefinite per quel particolare parametro: la sezione [ndbd default], quindi, stabilità i parametri generali per le varie sezioni [ndbd] che seguiranno poco più sotto, evitando inutili ripetizioni.
Tutte le tipologie di nodo trovano in questo file INI la sua configurazione: troviamo la sezione ndbd per i nodi che si occuperanno della memorizzazione dei dati, la sezione mysqld per gli host di interrogazione SQL, e nbd_mgmd per lo stesso nodo (ma possono essere più di uno, ridondati) di management.
Tra le voci presenti, analizziamo le più importanti:

  • NoOfReplicas= 2 – indica quante repliche devono essere effettuate per ogni slice. Con due nodi, come nel nostro caso, non può essere che due il valore.
  • DataMemory= 80M – indica lo spazio allocato per la memorizzazione dei dati veri e propri.
  • IndexMemory= 24M – indica lo spazio allocato per la memorizzazione degli indici.
  • DataDir= /var/lib/mysql-cluster – è la directory dove vengono memorizzati i file su disco, i vari check point, e dove eventualmente vengono memorizzati i backup del nodo. Nel nostro caso modificheremo questo parametro con /opt/mysql/data-cluster, non prima di aver creato tale directory e averla assegnata all’utente mysql (per coerenza con l’installazione di MySQL)


# mkdir /opt/mysql/data-cluster
# chown mysql /opt/mysql/data-cluster

E’ interessante notare come ogni singolo nodo disponga di un id, che viene indicato esplicitamente nel file di configurazione. Oltre all’id è possibile indicare l’indirizzo IP (o il nome dell’host) dal quale verrà effettuato il collegamento, per evitare che chiunque possa collegarsi al sistema.
Modifichiamo i parametri HostName con gli indirizzi IP specificati sopra, cancelliamo le occorrenze ulteriori delle sezioni [mysqld] e salviamo lasciando invariati tutti gli altri parametri.
Il file risultate è visibile nel riquadro 3.
Siamo pronti per lanciare in esecuzione – dall’utente mysql – il servizio relativo alla console di management (il numero davanti al prompt fa riferimento all’ultima cifra dell’IP del sistema):


(10)$ ndb_mgmd -f /opt/mysql/mysql-cluster.ini
(10)$ ps aux | grep ndb_mgmd
mysql 5331... ndb_mgmd -f mysql-cluster.ini

Colleghiamoci col client di console e lanciamo il comando show, che ci permette di avere una visione generale dello stato degli host coinvolti:


(10)$ ndb_mgm
-- NDB Cluster -- Management Client --
ndb_mgm> show
Connected to Management Server at: localhost:1186
Cluster Configuration
[ndbd(NDB)] 2 node(s)
id=2 (not connected, accepting connect from 192.168.100.20)
id=3 (not connected, accepting connect from 192.168.100.30)


[ndb_mgmd(MGM)] 1 node(s)
id=1 @192.168.100.10 (Version: 5.0.45)


[mysqld(API)] 1 node(s)
id=4 (not connected, accepting connect from 192.168.100.40)

Solo il nodo di management si è connesso al cluster (192.168.100.10, viene anche evidenziata la versione), e quest’ultimo è in attesa del collegamento degli altri host.

Attivazione dei nodi dati

L’installazione dei due nodi dati sarà identica: MySQL è già installato seguendo le istruzioni del riquadro e, come già visto per la management console, è stata creata la directory /opt/mysql/data-cluster. Vi è da specificare come contattare il server di management: l’indirizzo dello stesso dovrà essere inserito nel file my.cnf, non essendo utilizzati dal cluster meccanismi di auto-discovery di sorta. Aggiungete in fondo al file queste poche righe:


[mysqld]
ndbcluster
ndb-connectstring=192.168.100.10


[mysql_cluster]
ndb-connectstring=192.168.100.10

ndbd è il servizio di storage dei dati e, una volta lanciato, rimane attivo in background. La prima esecuzione deve essere invocata passando l’opzione –initial, in modo che possa preparare adeguatamente la directory indicata.


(20)$ ndbd --initial
(20)$ ls -la /opt/mysql/data-cluster/
drwxr-x--- 2 mysql mysql ... ndb_2_fs
-rw-r--r-- 1 mysql mysql... ndb_2_out.log
-rw-r--r-- 1 mysql mysql... ndb_2.pid

La console di management rileverà subito il nuovo arrivo:


(10)$ ndb_mgm -e show | grep Version
id=2 @192.168.100.20 (Version: 5.0.45, starting, Nodegroup: 0)
id=1 @192.168.100.10 (Version: 5.0.45)

Al nuovo nodo di storage è stato assegnato il Nodegroup 0. Dopo aver eseguito gli stessi comandi sull’altro nodo di storage, l’output della console cambia:


(10)$ ndb_mgm -e show | grep Version
id=2 @192.168.100.20 (Version: 5.0.45, Nodegroup: 0,
Master)
id=3 @192.168.100.30 (Version: 5.0.45, Nodegroup: 0)
id=1 @192.168.100.10 (Version: 5.0.45)

I due nodi deputati alla memorizzazione dei dati sono comparsi entrambi, e si sono auto-organizzati in un unico nodegroup; il primo che ha effettuato il collegamento è promosso a “master”, nodo primario per quel gruppo.
Attivazione del nodo SQL
All’appello manca ancora il server per effettuare le interrogazioni, abbastanza fondamentale visto che è anche l’unico sistema (per il momento almeno) per poter inserire qualcosa nel database e provare che funzioni.
Anche nel my.cnf del nodo SQL devono essere aggiunte le righe come già viste per i nodi storage. Inoltre è fondamentale verificare che l’engine NDB sia attivo, altrimenti addio alle funzionalità di clustering:


(40)$ mysql -e "show engines" | grep ndbcluster
ndbcluster DISABLED Clustered,
fault-tolerant, memory-based tables

Questo dovrebbe essere l’output prima dell’aggiunta delle righe al my.cnf: successivamente al posto di DISABLED troveremo YES. Sulla console intanto, appare anche l’id=4


(10)$ ndb_mgm -e show | grep id=4
id=4 @192.168.100.40 (Version: 5.0.45)

E’ stata dura, ma ce l’abbiamo fatta, e tutti i nodi del cluster sono riusciti a collegarsi alla console. Non rimane da fare altro che qualche prova: creiamo un database, poi una tabella, inseriamo qualche record e, successivamente, spegniamo uno dei due nodi dei dati.


(40)mysql> create database prova_ndb;
(40)mysql> use prova_ndb;
Database changed
(40)mysql> create table tab_ndb (id int auto_increment
primary key, testo char(100)) engine=ndbcluster;
(40)mysql> insert into tab_ndb (testo) values
("primo record con il cluster");
(40)mysql> insert into tab_ndb (testo) values
("secondo record con il cluster");
(40)mysql> select id, testo from tab_ndb;
| id | testo |
| 1 | primo record con il cluster |
| 2 | secondo record con il cluster |

A questo punto, spegniamo il nodo Master: uno shutdown pulito, uno spegnimento brutale, scegliete voi (magari uno snapshot della macchina virtuale, per non rischiare corruzioni di filesystem… in realtà basta un kill di ndbd).
Istantaneamente lo stato della console cambia:


(10)$ ndb_mgm -e show
[ndbd(NDB)] 2 node(s)
id=2 (not connected, accepting connect from 192.168.100.20)
id=3 @192.168.100.30 (Version: 5.0.45, Nodegroup: 0, Master)

Il secondo nodo è diventato Master, mentre il primo è disconnesso. E la query?


(40)mysql> select count(*) from tab_ndb;
| count(*) |
| 2 |

La caduta di uno dei due nodi dati non ha portato alcuna conseguenza e il database risponde senza alcun tipo di problema. Tutto secondo teoria.

Conclusione

MySQL Cluster ha un design veramente ben fatto, ma è necessario analizzare bene quando valga la pena di essere adottato e quando è meglio indirizzarsi verso altre tecnologie meno sperimentali.
Nella prossima puntata, se l’argomento sarà di interesse, potremo continuare ad approfondire la gestione del failover, il backup, e un esempio di migrazione da database MyISAM a NDBStorage.
Cercherò di limitare la dimensione delle macchine virtuali utilizzate in questo articolo, rendendole disponibili alla URL:
http://www.oltrelinux.com/risorse/LC61/mysql.tar.gz

Riquadro 1: Limitazioni di NDB

Non è tutt’oro quello che luccica, e pensare di migrare senza troppi problemi un database MyISAM o InnoDB su storage NDB per approfittare dell’affidabilità che questo offre è una illusione che svanirà presto non appena si passerà dalla lettura delle caratteristiche alla console :-).
E’ richiesta una profonda conoscenza, oltre che dell’ambiente MySQL Cluster, anche della vostra base di dati: già, perché lavorando in memoria ogni byte è sacro, e bisogna prima capire come viene utilizzata dal server, e poi centellinare la RAM laddove veramente serve.
Gli indici hanno un “costo elevato”, e devono essere utilizzati solo se veramente necessari. Tabelle senza chiavi primarie non devono esistere (questo, in generale, indipendentemente dal Cluster, dovrebbe essere una buona norma a prescindere) perché, in caso non sia specificata una primary key, viene auto-generata dal server con un hash che non aggiunge informazioni in più e consuma memoria.
Sono presenti le transazioni, ma solo nella modalità “REPEATABLE READ”, non quindi al livello elevato di InnoDB. Come MyISAM, non supporta le foreign key e, anche se sono permesse nella dichiarazione, sono semplicemente ignorate.
Non esistono i campi VAR: almeno fino alla versione 5.1, i campi variabili sono memorizzati con la massima occupazione di memoria: un varchar(255) sarà quindi un char(255).
Queste le principali limitazioni, ma ve ne sono anche altre che vi invito a consultare sul sito ufficiale di MySQL.

Riquadro 2: Installazione di MySQL da archivio binario (tar.gz)

Una volta effettuato il download da uno dei tanti mirror, ecco la sequenza classica delle operazioni per l’installazione:


# groupadd mysql
# adduser -g mysql mysql
# cd /opt
# tar xvfz /path/to/mysql-5.0.45-linux-i686.tar.gz
# chown -R root:mysql mysql-5.0.45-linux-i686
# ln -s mysql-5.0.45-linux-i686 mysql
# cd mysql
# scripts/mysql_install_db --user=mysql
# chown -R mysql data

Vista l’esiguità di memoria degli host, copiamo il file
my-small.cnf nella basedir di MySQL, insieme allo script mysql.server:


# cp support-files/my-small.cnf my.cnf
# cp support-files/mysql.server .

Modifichiamo il parametro basedir del file mysql.server per rispecchiare la path di installazione:


basedir = /opt/mysql

Aggiungiamo anche /opt/mysql/bin alla path di sistema, in modo che sia più semplice l’utilizzo dei comandi; inoltre effettuiamo un link simbolico per far apparire my.cnf in /etc:


# PATH=$PATH:/opt/mysql/bin
# ln -s /opt/mysql/my.cnf /etc/

Per far partire il server, sarà sufficiente il comando:


# /opt/mysql/mysql.server start

Riquadro 3: Il file mysql-cluster.ini


[ndbd default]
NoOfReplicas= 2
MaxNoOfConcurrentOperations= 10000
DataMemory= 80M
IndexMemory= 24M
TimeBetweenWatchDogCheck= 30000
DataDir= /opt/mysql/data-cluster
MaxNoOfOrderedIndexes= 512


[ndb_mgmd default]
DataDir= /opt/mysql/data-cluster


[ndb_mgmd]
Id=1
HostName= 192.168.100.10


[ndbd]


Id= 2
HostName= 192.168.100.20


[ndbd]
Id= 3
HostName= 192.168.100.30


[mysqld]
Id= 4
Hostname= 192.168.100.40


# choose an unused port number
# in this configuration 63132,
# 63133, and 63134 will be used
[tcp default]
PortNumber= 63132