Cet article est la suite de notre article précédent sur la mise à niveau de schéma en ligne dans Galera à l'aide de la méthode TOI. Nous allons maintenant vous montrer comment effectuer une mise à niveau de schéma à l'aide de la méthode Rolling Schema Upgrade (RSU).
RSU et TOI
Comme nous en avons discuté, lors de l'utilisation de TOI, un changement se produit en même temps sur tous les nœuds. Cela peut devenir une limitation sérieuse car une telle manière d'exécuter les changements de schéma implique qu'aucune autre requête ne peut être exécutée. Pour les longues instructions ALTER, le cluster peut même ne pas être disponible pendant des heures. Évidemment, ce n'est pas quelque chose que vous pouvez accepter en production. La méthode RSU corrige cette faiblesse - les changements se produisent sur un nœud à la fois tandis que les autres nœuds ne sont pas affectés et peuvent servir le trafic. Une fois ALTER terminé sur un nœud, il rejoindra le cluster et vous pourrez procéder à l'exécution d'un changement de schéma sur le nœud suivant.
Un tel comportement s'accompagne de son propre ensemble de limites. Le principal est que le changement de schéma planifié doit être compatible. Qu'est-ce que ça veut dire? Réfléchissons un moment. Tout d'abord, nous devons garder à l'esprit que le cluster est opérationnel en permanence - le nœud modifié doit être en mesure d'accepter tout le trafic qui atteint les nœuds restants. En bref, un DML exécuté sur l'ancien schéma doit également fonctionner sur le nouveau schéma (et vice-versa si vous utilisez une sorte de distribution de connexion de type round-robin dans votre cluster Galera). Nous nous concentrerons sur la compatibilité MySQL, mais vous devez également vous rappeler que votre application doit fonctionner avec des nœuds modifiés et non modifiés - assurez-vous que votre modification ne cassera pas la logique de l'application. Une bonne pratique consiste à transmettre explicitement les noms de colonne aux requêtes - ne vous fiez pas à "SELECT *" car vous ne savez jamais combien de colonnes vous obtiendrez en retour.
Format de journal binaire basé sur Galera et Row
Ok, donc DML doit travailler sur les anciens et les nouveaux schémas. Comment les DML sont-ils transférés entre les nœuds Galera ? Cela affecte-t-il les changements qui sont compatibles et ceux qui ne le sont pas ? Oui, en effet - c'est le cas. Galera n'utilise pas la réplication MySQL régulière, mais elle s'appuie toujours sur elle pour transférer les événements entre les nœuds. Pour être précis, Galera utilise le format ROW pour les événements. Un événement au format ligne (après décodage) peut ressembler à ceci :
### INSERT INTO `schema`.`table`
### SET
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
Ou :
### UPDATE `schema`.`table`
### WHERE
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
### SET
### @1=2
### @2=2
### @3='88764053989'
### @4='81084251066'
Comme vous pouvez le voir, il existe un modèle visible :une ligne est identifiée par son contenu. Il n'y a pas de noms de colonnes, juste leur ordre. Cela seul devrait allumer des voyants d'avertissement :"que se passerait-il si j'enlevais l'une des colonnes ?" Eh bien, si c'est la dernière colonne, c'est acceptable. Si vous supprimez une colonne au milieu, cela perturbera l'ordre des colonnes et, par conséquent, la réplication sera interrompue. Une chose similaire se produira si vous ajoutez une colonne au milieu, plutôt qu'à la fin. Il y a cependant plus de contraintes. La modification de la définition de colonne fonctionnera tant qu'il s'agit du même type de données - vous pouvez modifier la colonne INT pour devenir BIGINT mais vous ne pouvez pas modifier la colonne INT en VARCHAR - cela interrompra la réplication. Vous pouvez trouver une description détaillée des changements compatibles et de ceux qui ne le sont pas dans la documentation MySQL. Peu importe ce que vous pouvez voir dans la documentation, pour rester du bon côté, il est préférable d'exécuter certains tests sur un cluster de développement/transfert séparé. Assurez-vous que cela fonctionnera non seulement selon la documentation, mais qu'il fonctionnera également correctement dans votre configuration particulière.
Dans l'ensemble, comme vous pouvez le voir clairement, exécuter RSU de manière sûre est beaucoup plus complexe que de simplement exécuter quelques commandes. Néanmoins, comme les commandes sont importantes, examinons l'exemple de la façon dont vous pouvez effectuer le RSU et ce qui peut mal tourner dans le processus.
Exemple RSU
Configuration initiale
Imaginons un exemple assez simple d'application. Nous utiliserons un outil de référence, Sysbench, pour générer du contenu et du trafic, mais le flux sera le même pour presque toutes les applications - Wordpress, Joomla, Drupal, etc. Nous utiliserons HAProxy colocalisé avec notre application pour répartir les lectures et les écritures entre les nœuds Galera de manière circulaire. Vous pouvez vérifier ci-dessous comment HAProxy voit le cluster Galera.
L'ensemble de la topologie ressemble à ceci :
Le trafic est généré à l'aide de la commande suivante :
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
Le schéma ressemble à ci-dessous :
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)
Voyons d'abord comment ajouter un index à cette table. L'ajout d'un index est une modification compatible qui peut être facilement effectuée à l'aide de 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)
Comme vous pouvez le voir dans l'onglet Nœud, l'hôte sur lequel nous avons exécuté le changement est automatiquement passé à l'état Donateur/Désynchronisé, ce qui garantit que cet hôte n'aura pas d'impact sur le reste du cluster s'il est ralenti par ALTER.
Voyons maintenant à quoi ressemble notre schéma :
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)
Comme vous pouvez le voir, l'index a été ajouté. Veuillez garder à l'esprit, cependant, que cela ne s'est produit que sur ce nœud particulier. Pour effectuer un changement de schéma complet, vous devez suivre ce processus sur les nœuds restants du cluster Galera. Pour terminer avec le premier nœud, nous pouvons remettre wsrep_OSU_method sur TOI :
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Nous n'allons pas montrer le reste du processus, car c'est la même chose - activez RSU au niveau de la session, exécutez ALTER, activez TOI. Ce qui est plus intéressant, c'est ce qui se passerait si le changement était incompatible. Reprenons un coup d'œil rapide sur le schéma :
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)
Disons que nous voulons changer le type de colonne 'k' de INT à VARCHAR(30) sur un nœud.
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
Maintenant, regardons le schéma :
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)
Tout est comme prévu - la colonne 'k' a été changée en VARCHAR. Nous pouvons maintenant vérifier si ce changement est acceptable ou non pour le cluster Galera. Pour le tester, nous utiliserons l'un des nœuds restants, non modifiés, pour exécuter la requête suivante :
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Voyons ce qui se passe. Cela n'a vraiment pas l'air bien - notre nœud est en panne. Les journaux vous donneront plus de détails :
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…
Comme on peut le voir, Galera s'est plainte du fait que la colonne ne peut pas être convertie de INT en VARCHAR(30). Il a tenté de ré-exécuter le jeu d'écriture quatre fois, mais cela a échoué, sans surprise. En tant que tel, Galera a déterminé que la cohérence du nœud est compromise et que le nœud est expulsé du cluster. Le contenu restant des journaux montre ce processus :
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.
Bien sûr, ClusterControl tentera de récupérer ce nœud - la récupération implique l'exécution de SST afin que les modifications de schéma incompatibles soient supprimées, mais nous reviendrons à la case départ - notre modification de schéma sera annulée.
Comme vous pouvez le constater, l'exécution de RSU est un processus très simple, mais en dessous, il peut être assez complexe. Cela nécessite des tests et des préparations pour s'assurer que vous ne perdrez pas un nœud simplement parce que le changement de schéma n'était pas compatible.