Questo post è una continuazione del nostro precedente post sull'aggiornamento dello schema online in Galera utilizzando il metodo TOI. Ora ti mostreremo come eseguire un aggiornamento dello schema utilizzando il metodo Rolling Schema Upgrade (RSU).
RSU e TOI
Come abbiamo discusso, quando si utilizza TOI, si verifica una modifica contemporaneamente su tutti i nodi. Questo può diventare un serio limite in quanto tale modo di eseguire le modifiche allo schema implica che non è possibile eseguire altre query. Per istruzioni ALTER lunghe, il cluster potrebbe non essere disponibile anche per ore. Ovviamente, questo non è qualcosa che puoi accettare in produzione. Il metodo RSU risolve questo punto debole:le modifiche si verificano su un nodo alla volta mentre gli altri nodi non sono interessati e possono servire il traffico. Una volta completato ALTER su un nodo, si unirà nuovamente al cluster e potrai procedere con l'esecuzione di una modifica dello schema sul nodo successivo.
Tale comportamento ha una propria serie di limiti. Il principale è che la modifica dello schema pianificata deve essere compatibile. Cosa significa? Pensiamoci un po'. Prima di tutto dobbiamo tenere a mente che il cluster è sempre attivo e funzionante:il nodo modificato deve essere in grado di accettare tutto il traffico che colpisce i nodi rimanenti. In breve, un DML eseguito sul vecchio schema deve funzionare anche sul nuovo schema (e viceversa se utilizzi una sorta di distribuzione di connessione tipo round robin nel tuo cluster Galera). Ci concentreremo sulla compatibilità con MySQL, ma devi anche ricordare che la tua applicazione deve funzionare con nodi alterati e non alterati:assicurati che il tuo alter non rompa la logica dell'applicazione. Una buona pratica consiste nel passare esplicitamente i nomi delle colonne alle query:non fare affidamento su "SELECT *" perché non sai mai quante colonne riceverai in cambio.
Formato di registro binario basato su Galera e riga
Ok, quindi DML deve lavorare su schemi vecchi e nuovi. Come vengono trasferiti i DML tra i nodi Galera? Influisce su quali modifiche sono compatibili e quali no? Sì, in effetti - lo fa. Galera non utilizza la normale replica di MySQL, ma si basa comunque su di essa per trasferire gli eventi tra i nodi. Per essere precisi, Galera utilizza il formato FILA per gli eventi. Un evento in formato riga (dopo la decodifica) può avere il seguente aspetto:
### INSERT INTO `schema`.`table`
### SET
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
Oppure:
### UPDATE `schema`.`table`
### WHERE
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
### SET
### @1=2
### @2=2
### @3='88764053989'
### @4='81084251066'
Come puoi vedere, c'è uno schema visibile:una riga è identificata dal suo contenuto. Non ci sono nomi di colonna, solo il loro ordine. Questo da solo dovrebbe accendere alcune spie:"cosa accadrebbe se rimuovessi una delle colonne?" Bene, se è l'ultima colonna, questo è accettabile. Se si rimuove una colonna nel mezzo, ciò rovinerà l'ordine delle colonne e, di conseguenza, la replica si interromperà. Una cosa simile accadrà se aggiungi una colonna nel mezzo, invece che alla fine. Ci sono più vincoli, però. La modifica della definizione della colonna funzionerà purché sia dello stesso tipo di dati:è possibile modificare la colonna INT per diventare BIGINT ma non è possibile modificare la colonna INT in VARCHAR:ciò interromperà la replica. Puoi trovare una descrizione dettagliata di quale modifica è compatibile e cosa non lo è nella documentazione di MySQL. Indipendentemente da ciò che puoi vedere nella documentazione, per stare al sicuro, è meglio eseguire alcuni test su un cluster di sviluppo/staging separato. Assicurati che funzioni non solo secondo la documentazione, ma che funzioni bene anche nella tua configurazione particolare.
Tutto sommato, come puoi vedere chiaramente, eseguire RSU in modo sicuro è molto più complesso che eseguire solo un paio di comandi. Tuttavia, poiché i comandi sono importanti, diamo un'occhiata all'esempio di come eseguire la RSU e cosa può andare storto nel processo.
Esempio RSU
Configurazione iniziale
Immaginiamo un esempio piuttosto semplice di applicazione. Useremo uno strumento di benchmark, Sysbench, per generare contenuto e traffico, ma il flusso sarà lo stesso per quasi tutte le applicazioni:Wordpress, Joomla, Drupal, e tu lo chiami. Useremo HAProxy collocato con la nostra applicazione per dividere letture e scritture tra i nodi Galera in modo round robin. Puoi controllare di seguito come HAProxy vede il cluster Galera.
L'intera topologia è simile alla seguente:
Il traffico viene generato utilizzando il seguente comando:
while true ; do sysbench /root/sysbench/src/lua/oltp_read_write.lua --threads=4 --max-requests=0 --time=3600 --mysql-host=10.0.0.100 --mysql-user=sbtest --mysql-password=sbtest --mysql-port=3307 --tables=32 --report-interval=1 --skip-trx=on --table-size=100000 --db-ps-mode=disable run ; done
Lo schema è simile al seguente:
mysql> SHOW CREATE TABLE sbtest1.sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=29986632 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
Per prima cosa, vediamo come possiamo aggiungere un indice a questa tabella. L'aggiunta di un indice è una modifica compatibile che può essere eseguita facilmente utilizzando RSU.
mysql> SET SESSION wsrep_OSU_method=RSU;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER TABLE sbtest1.sbtest1 ADD INDEX idx_new (k, c);
Query OK, 0 rows affected (5 min 19.59 sec)
Come puoi vedere nella scheda Nodo, l'host su cui abbiamo eseguito la modifica è passato automaticamente allo stato di donatore/non sincronizzato, il che garantisce che questo host non influirà sul resto del cluster se viene rallentato da ALTER.
Controlliamo come appare ora il nostro schema:
mysql> SHOW CREATE TABLE sbtest1.sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`),
KEY `idx_new` (`k`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=29986632 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
Come puoi vedere, l'indice è stato aggiunto. Tieni presente, tuttavia, che ciò è accaduto solo su quel particolare nodo. Per eseguire una modifica completa dello schema, è necessario seguire questo processo sui nodi rimanenti del cluster Galera. Per finire con il primo nodo, possiamo riportare wsrep_OSU_method a TOI:
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Non mostreremo il resto del processo, perché è lo stesso:abilita RSU a livello di sessione, esegui ALTER, abilita TOI. La cosa più interessante è cosa accadrebbe se il cambiamento fosse incompatibile. Diamo ancora una rapida occhiata allo schema:
mysql> SHOW CREATE TABLE sbtest1.sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`),
KEY `idx_new` (`k`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=29986632 DEFAULT CHARSET=latin1
1 row in set (0.00 sec)
Supponiamo di voler cambiare il tipo di colonna 'k' da INT a VARCHAR(30) su un nodo.
mysql> SET SESSION wsrep_OSU_method=RSU;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER TABLE sbtest1.sbtest1 MODIFY COLUMN k VARCHAR(30) NOT NULL DEFAULT '';
Query OK, 10004785 rows affected (1 hour 14 min 51.89 sec)
Records: 10004785 Duplicates: 0 Warnings: 0
Ora, diamo un'occhiata allo schema:
mysql> SHOW CREATE TABLE sbtest1.sbtest1\G
*************************** 1. row ***************************
Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` varchar(30) NOT NULL DEFAULT '',
`c` char(120) NOT NULL DEFAULT '',
`pad` char(60) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`),
KEY `idx_new` (`k`,`c`)
) ENGINE=InnoDB AUTO_INCREMENT=29986632 DEFAULT CHARSET=latin1
1 row in set (0.02 sec)
Tutto è come ci aspettavamo:la colonna "k" è stata cambiata in VARCHAR. Ora possiamo verificare se questa modifica è accettabile o meno per il cluster Galera. Per testarlo, utilizzeremo uno dei nodi rimanenti e inalterati per eseguire la seguente query:
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Vediamo cosa è successo. Sicuramente non ha un bell'aspetto:il nostro nodo è inattivo. I registri ti forniranno maggiori dettagli:
2017-04-07T10:51:14.873524Z 5 [ERROR] Slave SQL: Column 1 of table 'sbtest1.sbtest1' cannot be converted from type 'int' to type 'varchar(30)', Error_code: 1677
2017-04-07T10:51:14.873560Z 5 [Warning] WSREP: RBR event 3 Write_rows apply warning: 3, 982675
2017-04-07T10:51:14.879120Z 5 [Warning] WSREP: Failed to apply app buffer: seqno: 982675, status: 1
at galera/src/trx_handle.cpp:apply():351
Retrying 2th time
2017-04-07T10:51:14.879272Z 5 [ERROR] Slave SQL: Column 1 of table 'sbtest1.sbtest1' cannot be converted from type 'int' to type 'varchar(30)', Error_code: 1677
2017-04-07T10:51:14.879287Z 5 [Warning] WSREP: RBR event 3 Write_rows apply warning: 3, 982675
2017-04-07T10:51:14.879399Z 5 [Warning] WSREP: Failed to apply app buffer: seqno: 982675, status: 1
at galera/src/trx_handle.cpp:apply():351
Retrying 3th time
2017-04-07T10:51:14.879618Z 5 [ERROR] Slave SQL: Column 1 of table 'sbtest1.sbtest1' cannot be converted from type 'int' to type 'varchar(30)', Error_code: 1677
2017-04-07T10:51:14.879633Z 5 [Warning] WSREP: RBR event 3 Write_rows apply warning: 3, 982675
2017-04-07T10:51:14.879730Z 5 [Warning] WSREP: Failed to apply app buffer: seqno: 982675, status: 1
at galera/src/trx_handle.cpp:apply():351
Retrying 4th time
2017-04-07T10:51:14.879911Z 5 [ERROR] Slave SQL: Column 1 of table 'sbtest1.sbtest1' cannot be converted from type 'int' to type 'varchar(30)', Error_code: 1677
2017-04-07T10:51:14.879924Z 5 [Warning] WSREP: RBR event 3 Write_rows apply warning: 3, 982675
2017-04-07T10:51:14.885255Z 5 [ERROR] WSREP: Failed to apply trx: source: 938415a6-1aab-11e7-ac29-0a69a4a1dafe version: 3 local: 0 state: APPLYING flags: 1 conn_id: 125559 trx_id: 2856843 seqnos (l: 392283, g: 9
82675, s: 982674, d: 982563, ts: 146831275805149)
2017-04-07T10:51:14.885271Z 5 [ERROR] WSREP: Failed to apply trx 982675 4 times
2017-04-07T10:51:14.885281Z 5 [ERROR] WSREP: Node consistency compromized, aborting…
Come si può vedere, Galera si è lamentata del fatto che la colonna non può essere convertita da INT a VARCHAR(30). Ha tentato di rieseguire il set di scrittura quattro volte ma non è riuscito, ovviamente. Pertanto, Galera ha determinato che la coerenza del nodo è compromessa e il nodo viene espulso dal cluster. Il contenuto rimanente dei registri mostra questo processo:
2017-04-07T10:51:14.885560Z 5 [Note] WSREP: Closing send monitor...
2017-04-07T10:51:14.885630Z 5 [Note] WSREP: Closed send monitor.
2017-04-07T10:51:14.885644Z 5 [Note] WSREP: gcomm: terminating thread
2017-04-07T10:51:14.885828Z 5 [Note] WSREP: gcomm: joining thread
2017-04-07T10:51:14.885842Z 5 [Note] WSREP: gcomm: closing backend
2017-04-07T10:51:14.896654Z 5 [Note] WSREP: view(view_id(NON_PRIM,6fcd492a,37) memb {
b13499a8,0
} joined {
} left {
} partitioned {
6fcd492a,0
938415a6,0
})
2017-04-07T10:51:14.896746Z 5 [Note] WSREP: view((empty))
2017-04-07T10:51:14.901477Z 5 [Note] WSREP: gcomm: closed
2017-04-07T10:51:14.901512Z 0 [Note] WSREP: New COMPONENT: primary = no, bootstrap = no, my_idx = 0, memb_num = 1
2017-04-07T10:51:14.901531Z 0 [Note] WSREP: Flow-control interval: [16, 16]
2017-04-07T10:51:14.901541Z 0 [Note] WSREP: Received NON-PRIMARY.
2017-04-07T10:51:14.901550Z 0 [Note] WSREP: Shifting SYNCED -> OPEN (TO: 982675)
2017-04-07T10:51:14.901563Z 0 [Note] WSREP: Received self-leave message.
2017-04-07T10:51:14.901573Z 0 [Note] WSREP: Flow-control interval: [0, 0]
2017-04-07T10:51:14.901581Z 0 [Note] WSREP: Received SELF-LEAVE. Closing connection.
2017-04-07T10:51:14.901589Z 0 [Note] WSREP: Shifting OPEN -> CLOSED (TO: 982675)
2017-04-07T10:51:14.901602Z 0 [Note] WSREP: RECV thread exiting 0: Success
2017-04-07T10:51:14.902701Z 5 [Note] WSREP: recv_thread() joined.
2017-04-07T10:51:14.902720Z 5 [Note] WSREP: Closing replication queue.
2017-04-07T10:51:14.902730Z 5 [Note] WSREP: Closing slave action queue.
2017-04-07T10:51:14.902742Z 5 [Note] WSREP: /usr/sbin/mysqld: Terminated.
Ovviamente, ClusterControl tenterà di ripristinare tale nodo:il ripristino implica l'esecuzione di SST, quindi le modifiche allo schema incompatibili verranno rimosse, ma torneremo al punto di partenza:la nostra modifica allo schema verrà annullata.
Come puoi vedere, mentre l'esecuzione di RSU è un processo molto semplice, sotto può essere piuttosto complesso. Richiede alcuni test e preparativi per assicurarsi di non perdere un nodo solo perché la modifica dello schema non era compatibile.