Dette indlæg er en fortsættelse af vores tidligere indlæg om Online Schema Upgrade i Galera ved hjælp af TOI-metoden. Vi vil nu vise dig, hvordan du udfører en skemaopgradering ved hjælp af Rolling Schema Upgrade (RSU)-metoden.
RSU og TOI
Som vi diskuterede, når du bruger TOI, sker der en ændring på samme tid på alle noderne. Dette kan blive en alvorlig begrænsning, da en sådan måde at udføre skemaændringer på indebærer, at ingen andre forespørgsler kan udføres. For lange ALTER-udsagn er klyngen muligvis ikke tilgængelig i timer endda. Det er naturligvis ikke noget, man kan acceptere i produktionen. RSU-metoden løser denne svaghed - ændringer sker på én node ad gangen, mens andre noder ikke påvirkes og kan betjene trafik. Når ALTER er fuldført på én node, vil den slutte sig til klyngen igen, og du kan fortsætte med at udføre en skemaændring på den næste node.
Sådan adfærd kommer med sit eget sæt af begrænsninger. Den vigtigste er, at planlagte skemaændringer skal være kompatible. Hvad betyder det? Lad os tænke over det et stykke tid. Først og fremmest skal vi huske på, at klyngen er oppe og køre hele tiden - den ændrede node skal være i stand til at acceptere al den trafik, der rammer de resterende noder. Kort sagt, en DML, der udføres på det gamle skema, skal også fungere på det nye skema (og omvendt, hvis du bruger en form for round-robin-lignende forbindelsesdistribution i din Galera Cluster). Vi vil fokusere på MySQL-kompatibiliteten, men du skal også huske, at din applikation skal fungere med både ændrede og ikke-ændrede noder - sørg for, at din ændring ikke bryder applikationslogikken. En god praksis er eksplicit at videregive kolonnenavne til forespørgsler - stol ikke på "SELECT *", fordi du aldrig ved, hvor mange kolonner du får til gengæld.
Galera- og Row-baseret binært logformat
Ok, så DML skal arbejde på gamle og nye skemaer. Hvordan overføres DML'er mellem Galera-noder? Påvirker det, hvilke ændringer der er kompatible, og hvad der ikke er? Ja, sandelig - det gør det. Galera bruger ikke almindelig MySQL-replikering, men den er stadig afhængig af den til at overføre hændelser mellem noderne. For at være præcis bruger Galera ROW-format til begivenheder. En hændelse i rækkeformat (efter afkodning) kan se sådan ud:
### INSERT INTO `schema`.`table`
### SET
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
Eller:
### UPDATE `schema`.`table`
### WHERE
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
### SET
### @1=2
### @2=2
### @3='88764053989'
### @4='81084251066'
Som du kan se, er der et synligt mønster:en række identificeres ved dens indhold. Der er ingen kolonnenavne, kun deres rækkefølge. Dette alene burde tænde nogle advarselslamper:"hvad ville der ske, hvis jeg fjerner en af kolonnerne?" Nå, hvis det er den sidste kolonne, er dette acceptabelt. Hvis du vil fjerne en kolonne i midten, vil dette rode med kolonnerækkefølgen, og som et resultat vil replikeringen bryde. Lignende ting vil ske, hvis du tilføjer en kolonne i midten, i stedet for i slutningen. Der er dog flere begrænsninger. Ændring af kolonnedefinition vil fungere, så længe det er den samme datatype - du kan ændre INT-kolonnen til at blive BIGINT, men du kan ikke ændre INT-kolonnen til VARCHAR - dette vil bryde replikering. Du kan finde en detaljeret beskrivelse af, hvilken ændring der er kompatibel, og hvad der ikke er i MySQL-dokumentationen. Uanset hvad du kan se i dokumentationen, for at være på den sikre side, er det bedre at køre nogle tests på en separat udviklings-/iscenesættelsesklynge. Sørg for, at det ikke kun fungerer i henhold til dokumentationen, men at det også fungerer fint i netop din opsætning.
Alt i alt, som du tydeligt kan se, er det meget mere komplekst at udføre RSU på en sikker måde end blot at køre et par kommandoer. Men da kommandoer er vigtige, lad os tage et kig på eksemplet på, hvordan du kan udføre RSU'en, og hvad der kan gå galt i processen.
RSU-eksempel
Indledende opsætning
Lad os forestille os et ret simpelt eksempel på en applikation. Vi vil bruge et bechmark-værktøj, Sysbench, til at generere indhold og trafik, men flowet vil være det samme for næsten alle applikationer - Wordpress, Joomla, Drupal, you name it. Vi vil bruge HAProxy sammen med vores applikation til at opdele læsninger og skrivninger blandt Galera-noder på en round-robin måde. Du kan se nedenfor, hvordan HAProxy ser Galera-klyngen.
Hele topologien ser ud som nedenfor:
Trafik genereres ved hjælp af følgende kommando:
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
Skemaet ser ud som nedenfor:
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)
Lad os først se, hvordan vi kan tilføje et indeks til denne tabel. Tilføjelse af et indeks er en kompatibel ændring, som nemt kan udføres ved hjælp af 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)
Som du kan se på fanen Node, har værten, som vi udførte ændringen på, automatisk skiftet til Donor/Desynkroniseret tilstand, hvilket sikrer, at denne vært ikke vil påvirke resten af klyngen, hvis den bliver bremset af ALTER.
Lad os tjekke, hvordan vores skema ser ud nu:
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)
Som du kan se, er indekset tilføjet. Vær dog opmærksom på, at dette kun skete på den bestemte node. For at opnå en fuldstændig skemaændring skal du følge denne proces på de resterende noder i Galera-klyngen. For at afslutte med den første node kan vi skifte wsrep_OSU_method tilbage til TOI:
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Vi vil ikke vise resten af processen, fordi det er det samme - aktiver RSU på sessionsniveau, kør ALTER, aktiver TOI. Hvad der er mere interessant er, hvad der ville ske, hvis ændringen vil være uforenelig. Lad os tage et hurtigt kig på skemaet igen:
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)
Lad os sige, at vi vil ændre typen af kolonne 'k' fra INT til VARCHAR(30) på én node.
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
Lad os nu tage et kig på skemaet:
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)
Alt er som vi forventer - 'k' kolonne er blevet ændret til VARCHAR. Nu kan vi kontrollere, om denne ændring er acceptabel eller ej for Galera-klyngen. For at teste det, vil vi bruge en af de resterende, uændrede noder til at udføre følgende forespørgsel:
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Lad os se, hvad der skete. Det ser bestemt ikke godt ud - vores node er nede. Logs vil give dig flere detaljer:
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…
Som det kan ses, klagede Galera over, at kolonnen ikke kan konverteres fra INT til VARCHAR(30). Det forsøgte at genudføre skrivesættet fire gange, men det mislykkedes, ikke overraskende. Som sådan fastslog Galera, at nodekonsistensen er kompromitteret, og noden er smidt ud af klyngen. Det resterende indhold af logfilerne viser denne proces:
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.
Selvfølgelig vil ClusterControl forsøge at gendanne en sådan node - gendannelse involverer at køre SST, så inkompatible skemaændringer vil blive fjernet, men vi vil være tilbage ved udgangspunktet - vores skemaændring vil blive omvendt.
Som du kan se, mens kørsel af RSU er en meget enkel proces, kan den nedenunder være ret kompleks. Det kræver nogle tests og forberedelser for at sikre, at du ikke mister en node, bare fordi skemaændringen ikke var kompatibel.