Tento příspěvek je pokračováním našeho předchozího příspěvku o online aktualizaci schématu v Galeře pomocí metody TOI. Nyní vám ukážeme, jak provést upgrade schématu pomocí metody Rolling Schema Upgrade (RSU).
RSU a TOI
Jak jsme diskutovali, při použití TOI dochází ke změně ve stejnou dobu na všech uzlech. To se může stát vážným omezením, protože takový způsob provádění změn schématu znamená, že nelze provádět žádné další dotazy. U dlouhých příkazů ALTER může být cluster nedostupný i několik hodin. Je zřejmé, že to není něco, co můžete ve výrobě akceptovat. Metoda RSU řeší tuto slabinu - změny se dějí na jednom uzlu, zatímco ostatní uzly nejsou ovlivněny a mohou obsluhovat provoz. Po dokončení ALTER na jednom uzlu se znovu připojí ke clusteru a můžete pokračovat ve provádění změny schématu na dalším uzlu.
Takové chování má svá vlastní omezení. Hlavní je, že plánovaná změna schématu musí být kompatibilní. Co to znamená? Pojďme o tom chvíli přemýšlet. Především musíme mít na paměti, že cluster je neustále v provozu – změněný uzel musí být schopen přijmout veškerý provoz, který zasáhl zbývající uzly. Stručně řečeno, DML spuštěný na starém schématu musí fungovat také na novém schématu (a naopak, pokud používáte nějakou distribuci připojení typu round-robin ve vašem Galera Cluster). Zaměříme se na kompatibilitu s MySQL, ale také si musíte pamatovat, že vaše aplikace musí pracovat se změněnými i nezměněnými uzly – ujistěte se, že vaše změna nenaruší aplikační logiku. Jedním z dobrých postupů je explicitně předávat názvy sloupců dotazům – nespoléhejte na „SELECT *“, protože nikdy nevíte, kolik sloupců na oplátku dostanete.
Binární formát protokolu na bázi Galera a řádků
Dobře, takže DML musí pracovat na starých a nových schématech. Jak se přenášejí DML mezi uzly Galera? Ovlivňuje to, které změny jsou kompatibilní a které ne? Ano, skutečně - má. Galera nepoužívá běžnou replikaci MySQL, ale stále na ni spoléhá při přenosu událostí mezi uzly. Abychom byli přesní, Galera používá pro události formát ROW. Událost ve formátu řádku (po dekódování) může vypadat takto:
### INSERT INTO `schema`.`table`
### SET
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
Nebo:
### UPDATE `schema`.`table`
### WHERE
### @1=1
### @2=1
### @3='88764053989'
### @4='14700597838'
### SET
### @1=2
### @2=2
### @3='88764053989'
### @4='81084251066'
Jak vidíte, existuje viditelný vzor:řádek je identifikován podle obsahu. Neexistují žádné názvy sloupců, pouze jejich pořadí. To samo o sobě by mělo rozsvítit některá varovná světla:„co by se stalo, kdybych odstranil jeden ze sloupců?“ Pokud je to poslední sloupec, je to přijatelné. Pokud byste odstranili sloupec uprostřed, narušilo by to pořadí sloupců a v důsledku toho se replikace přerušila. Podobná věc se stane, pokud přidáte nějaký sloupec doprostřed místo na konec. Existuje však více omezení. Změna definice sloupce bude fungovat, pokud se jedná o stejný datový typ - můžete změnit sloupec INT tak, aby se stal BIGINT, ale nemůžete změnit sloupec INT na VARCHAR - tím přerušíte replikaci. Podrobný popis toho, která změna je kompatibilní a která ne, najdete v dokumentaci MySQL. Bez ohledu na to, co vidíte v dokumentaci, pro jistotu je lepší spustit některé testy na samostatném vývojovém/stagingovém clusteru. Ujistěte se, že bude fungovat nejen podle dokumentace, ale také že bude fungovat správně ve vašem konkrétním nastavení.
Celkově vzato, jak jasně vidíte, provádění RSU bezpečným způsobem je mnohem složitější než pouhé spuštění několika příkazů. Přesto, protože příkazy jsou důležité, pojďme se podívat na příklad toho, jak můžete provést RSU a co se v procesu může pokazit.
Příklad RSU
Počáteční nastavení
Představme si poměrně jednoduchý příklad aplikace. Ke generování obsahu a návštěvnosti použijeme nástroj bechmark, Sysbench, ale tok bude stejný pro téměř všechny aplikace – Wordpress, Joomla, Drupal, co si jen vzpomenete. Použijeme HAProxy spojené s naší aplikací k rozdělení čtení a zápisu mezi uzly Galera způsobem cyklicky. Níže můžete zkontrolovat, jak HAProxy vidí cluster Galera.
Celá topologie vypadá níže:
Provoz je generován pomocí následujícího příkazu:
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
Schéma vypadá níže:
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)
Nejprve se podívejme, jak můžeme do této tabulky přidat index. Přidání indexu je kompatibilní změna, kterou lze snadno provést pomocí 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)
Jak můžete vidět na kartě Uzel, hostitel, na kterém jsme provedli změnu, se automaticky přepnul do stavu Donor/Desynced, což zajišťuje, že tento hostitel neovlivní zbytek clusteru, pokud bude zpomalen ALTER.
Pojďme se nyní podívat, jak naše schéma vypadá:
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)
Jak vidíte, index byl přidán. Mějte však na paměti, že k tomu došlo pouze na tomto konkrétním uzlu. Chcete-li provést úplnou změnu schématu, musíte tento proces sledovat na zbývajících uzlech clusteru Galera. Chcete-li skončit s prvním uzlem, můžeme přepnout metodu wsrep_OSU_method zpět na TOI:
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Nebudeme ukazovat zbytek procesu, protože je to stejné - povolit RSU na úrovni relace, spustit ALTER, povolit TOI. Zajímavější je, co by se stalo, kdyby změna byla nekompatibilní. Pojďme se znovu rychle podívat na 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)
Řekněme, že chceme změnit typ sloupce ‚k‘ z INT na VARCHAR(30) na jednom uzlu.
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
Nyní se podívejme na 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)
Vše je, jak očekáváme – sloupec „k“ byl změněn na VARCHAR. Nyní můžeme zkontrolovat, zda je tato změna přijatelná nebo ne pro Galera Cluster. Abychom to otestovali, použijeme jeden ze zbývajících nezměněných uzlů k provedení následujícího dotazu:
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Uvidíme, co se stane. Rozhodně to nevypadá dobře – náš uzel je dole. Protokoly vám poskytnou další podrobnosti:
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…
Jak je vidět, Galera si stěžovala na to, že sloupec nelze převést z INT na VARCHAR(30). Čtyřikrát se to pokusilo znovu spustit sadu zápisů, ale nepřekvapivě to selhalo. Galera jako taková zjistila, že konzistence uzlu je narušena a uzel je vyhozen z clusteru. Zbývající obsah protokolů ukazuje tento 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.
ClusterControl se samozřejmě pokusí takový uzel obnovit – obnova zahrnuje spuštění SST, takže nekompatibilní změny schématu budou odstraněny, ale vrátíme se na začátek – naše změna schématu bude vrácena.
Jak můžete vidět, zatímco spuštění RSU je velmi jednoduchý proces, pod ním může být poměrně složitý. Vyžaduje to určité testy a přípravy, abyste se ujistili, že neztratíte uzel jen proto, že změna schématu nebyla kompatibilní.