Det här inlägget är en fortsättning på vårt tidigare inlägg om Online Schema Upgrade i Galera med TOI-metoden. Vi kommer nu att visa dig hur du utför en schemauppgradering med metoden Rolling Schema Upgrade (RSU).
RSU och TOI
Som vi diskuterade, när du använder TOI, sker en förändring samtidigt på alla noder. Detta kan bli en allvarlig begränsning eftersom ett sådant sätt att utföra schemaändringar innebär att inga andra frågor kan köras. För långa ALTER-satser kanske klustret inte är tillgängligt i timmar. Uppenbarligen är detta inget man kan acceptera i produktionen. RSU-metoden åtgärdar denna svaghet - förändringar sker på en nod i taget medan andra noder inte påverkas och kan betjäna trafik. När ALTER har slutförts på en nod kommer den att gå med i klustret igen och du kan fortsätta med att utföra en schemaändring på nästa nod.
Sådant beteende kommer med sina egna begränsningar. Det viktigaste är att schemalagda schemaändringar måste vara kompatibla. Vad betyder det? Låt oss tänka på det ett tag. Först och främst måste vi komma ihåg att klustret är igång hela tiden - den ändrade noden måste kunna acceptera all trafik som träffar de återstående noderna. Kort sagt, en DML som körs på det gamla schemat måste fungera även på det nya schemat (och vice versa om du använder någon form av round-robin-liknande anslutningsdistribution i ditt Galera-kluster). Vi kommer att fokusera på MySQL-kompatibiliteten, men du måste också komma ihåg att din applikation måste fungera med både ändrade och oförändrade noder - se till att din ändring inte bryter applikationslogiken. En bra praxis är att uttryckligen skicka kolumnnamn till frågor - lita inte på "SELECT *" eftersom du aldrig vet hur många kolumner du får i gengäld.
Galera- och radbaserat binärt loggformat
Ok, så DML måste arbeta med gamla och nya scheman. Hur överförs DML:er mellan Galera-noder? Påverkar det vilka ändringar som är kompatibla och vilka som inte är det? Ja, verkligen - det gör det. Galera använder inte vanlig MySQL-replikering men den förlitar sig fortfarande på den för att överföra händelser mellan noderna. För att vara exakt använder Galera ROW-format för evenemang. En händelse i radformat (efter avkodning) kan se ut så här:
### 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 finns det ett synligt mönster:en rad identifieras av dess innehåll. Det finns inga kolumnnamn, bara deras ordning. Bara detta borde tända några varningslampor:"vad skulle hända om jag tar bort en av kolumnerna?" Tja, om det är den sista kolumnen är detta acceptabelt. Om du skulle ta bort en kolumn i mitten kommer detta att störa kolumnordningen och som ett resultat kommer replikeringen att gå sönder. Liknande kommer att hända om du lägger till någon kolumn i mitten, istället för i slutet. Det finns dock fler begränsningar. Att ändra kolumndefinitionen fungerar så länge det är samma datatyp - du kan ändra INT-kolumnen till BIGINT men du kan inte ändra INT-kolumnen till VARCHAR - detta kommer att bryta replikeringen. Du kan hitta en detaljerad beskrivning av vilken förändring som är kompatibel och vad som inte finns i MySQL-dokumentationen. Oavsett vad du kan se i dokumentationen, för att vara på den säkra sidan, är det bättre att köra några tester på ett separat utvecklings-/staging-kluster. Se till att det inte bara fungerar enligt dokumentationen, utan att det också fungerar bra i just din installation.
Sammantaget, som du tydligt kan se, är det mycket mer komplext att utföra RSU på ett säkert sätt än att bara köra ett par kommandon. Men eftersom kommandon är viktiga, låt oss ta en titt på exemplet på hur du kan utföra RSU och vad som kan gå fel i processen.
RSU-exempel
Initial installation
Låt oss föreställa oss ett ganska enkelt exempel på en applikation. Vi kommer att använda ett bechmark-verktyg, Sysbench, för att generera innehåll och trafik, men flödet kommer att vara detsamma för nästan alla applikationer - Wordpress, Joomla, Drupal, you name it. Vi kommer att använda HAProxy tillsammans med vår applikation för att dela läsningar och skrivningar mellan Galera-noder på ett round-robin-sätt. Du kan kolla nedan hur HAProxy ser på Galera-klustret.
Hela topologin ser ut som nedan:
Trafik genereras med följande 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
Schemat ser ut som nedan:
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)
Låt oss först se hur vi kan lägga till ett index i den här tabellen. Att lägga till ett index är en kompatibel ändring som enkelt kan göras med 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å fliken Nod har värden som vi utförde ändringen automatiskt bytt till Donator/Desynced-läge vilket säkerställer att den här värden inte kommer att påverka resten av klustret om den bromsas av ALTER.
Låt oss kolla hur vårt schema ser ut 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 har indexet lagts till. Kom dock ihåg att detta bara hände på just den noden. För att åstadkomma en fullständig schemaändring måste du följa denna process på de återstående noderna i Galera-klustret. För att avsluta med den första noden kan vi byta tillbaka wsrep_OSU_method till TOI:
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Vi kommer inte att visa resten av processen, eftersom det är samma sak - aktivera RSU på sessionsnivå, kör ALTER, aktivera TOI. Vad som är mer intressant är vad som skulle hända om förändringen blir oförenlig. Låt oss ta en snabb titt på schemat 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)
Låt oss säga att vi vill ändra typen av kolumn 'k' från INT till VARCHAR(30) på en nod.
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
Låt oss nu ta en titt på schemat:
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)
Allt är som vi förväntar oss - kolumnen "k" har ändrats till VARCHAR. Nu kan vi kontrollera om denna förändring är acceptabel eller inte för Galera Cluster. För att testa det kommer vi att använda en av återstående, oförändrade noder för att utföra följande fråga:
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Låt oss se vad som hände. Det ser definitivt inte bra ut - vår nod är nere. Loggar ger dig mer information:
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 kan ses klagade Galera på att kolumnen inte kan konverteras från INT till VARCHAR(30). Den försökte köra om skrivuppsättningen fyra gånger men det misslyckades, föga överraskande. Som sådan fastställde Galera att nodkonsistensen äventyras och noden kastas ut ur klustret. Återstående innehåll i loggarna visar denna process:
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.
Naturligtvis kommer ClusterControl att försöka återställa en sådan nod - återställning innebär att köra SST så inkompatibla schemaändringar kommer att tas bort, men vi kommer att vara tillbaka på ruta ett - vår schemaändring kommer att vändas.
Som du kan se, medan körning av RSU är en mycket enkel process, kan den underliggande vara ganska komplex. Det kräver vissa tester och förberedelser för att säkerställa att du inte förlorar en nod bara för att schemaändringen inte var kompatibel.