Este post é uma continuação do nosso post anterior sobre Upgrade de Esquema Online no Galera usando o método TOI. Agora mostraremos como realizar uma atualização de esquema usando o método Rolling Schema Upgrade (RSU).
RSU e TOI
Como discutimos, ao usar o TOI, uma mudança acontece ao mesmo tempo em todos os nós. Isso pode se tornar uma séria limitação, pois essa maneira de executar alterações de esquema implica que nenhuma outra consulta possa ser executada. Para instruções ALTER longas, o cluster pode não estar disponível por horas. Obviamente, isso não é algo que você pode aceitar na produção. O método RSU aborda essa fraqueza - as alterações ocorrem em um nó por vez, enquanto outros nós não são afetados e podem servir o tráfego. Depois que ALTER for concluído em um nó, ele se unirá novamente ao cluster e você poderá prosseguir com a execução de uma alteração de esquema no próximo nó.
Tal comportamento vem com seu próprio conjunto de limitações. A principal é que a mudança de esquema agendada tem que ser compatível. O que isso significa? Vamos pensar sobre isso por um tempo. Antes de tudo, precisamos ter em mente que o cluster está funcionando o tempo todo - o nó alterado deve ser capaz de aceitar todo o tráfego que atinge os nós restantes. Resumindo, uma DML executada no esquema antigo tem que funcionar também no novo esquema (e vice-versa se você usar algum tipo de distribuição de conexão do tipo round-robin em seu Galera Cluster). Vamos nos concentrar na compatibilidade com o MySQL, mas você também deve lembrar que seu aplicativo deve funcionar com nós alterados e não alterados - certifique-se de que sua alteração não quebre a lógica do aplicativo. Uma boa prática é passar explicitamente os nomes das colunas para as consultas - não confie em "SELECT *" porque você nunca sabe quantas colunas receberá em troca.
Formato de registro binário baseado em galera e linha
Ok, então o DML precisa funcionar em esquemas antigos e novos. Como os DMLs são transferidos entre os nós do Galera? Isso afeta quais mudanças são compatíveis e quais não são? Sim, de fato - ele faz. O Galera não usa a replicação regular do MySQL, mas ainda depende dela para transferir eventos entre os nós. Para ser preciso, Galera usa o formato ROW para eventos. Um evento em formato de linha (após a decodificação) pode ter esta aparência:
### 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'
Como você pode ver, há um padrão visível:uma linha é identificada por seu conteúdo. Não há nomes de colunas, apenas sua ordem. Isso por si só deve acender algumas luzes de aviso:“o que aconteceria se eu removesse uma das colunas?” Bem, se for a última coluna, isso é aceitável. Se você remover uma coluna no meio, isso atrapalhará a ordem das colunas e, como resultado, a replicação será interrompida. Algo semelhante acontecerá se você adicionar alguma coluna no meio, em vez de no final. Há mais restrições, no entanto. Alterar a definição da coluna funcionará desde que seja o mesmo tipo de dados - você pode alterar a coluna INT para se tornar BIGINT, mas não pode alterar a coluna INT para VARCHAR - isso interromperá a replicação. Você pode encontrar uma descrição detalhada de qual mudança é compatível e o que não é na documentação do MySQL. Não importa o que você possa ver na documentação, para ficar do lado seguro, é melhor executar alguns testes em um cluster de desenvolvimento/staging separado. Certifique-se de que funcionará não apenas de acordo com a documentação, mas que também funcione bem em sua configuração específica.
Em suma, como você pode ver claramente, executar RSU de maneira segura é muito mais complexo do que apenas executar alguns comandos. Ainda assim, como os comandos são importantes, vamos dar uma olhada no exemplo de como você pode realizar o RSU e o que pode dar errado no processo.
Exemplo de RSU
Configuração inicial
Vamos imaginar um exemplo bastante simples de um aplicativo. Usaremos uma ferramenta de referência, Sysbench, para gerar conteúdo e tráfego, mas o fluxo será o mesmo para quase todos os aplicativos - Wordpress, Joomla, Drupal, você escolhe. Usaremos o HAProxy colocado com nosso aplicativo para dividir leituras e gravações entre os nós do Galera de maneira round-robin. Você pode conferir abaixo como o HAProxy vê o cluster Galera.
Toda a topologia se parece com abaixo:
O tráfego é gerado usando o seguinte 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
Esquema fica como abaixo:
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)
Primeiro, vamos ver como podemos adicionar um índice a esta tabela. Adicionar um índice é uma alteração compatível que pode ser feita facilmente usando 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)
Como você pode ver na guia Nó, o host no qual executamos a alteração mudou automaticamente para o estado Doador/Dessincronizado, o que garante que esse host não afetará o restante do cluster se ficar lento pelo ALTER.
Vamos verificar como nosso esquema está agora:
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)
Como você pode ver, o índice foi adicionado. No entanto, lembre-se de que isso aconteceu apenas nesse nó específico. Para realizar uma mudança completa de esquema, você deve seguir este processo nos nós restantes do Galera Cluster. Para finalizar com o primeiro nó, podemos mudar wsrep_OSU_method de volta para TOI:
SET SESSION wsrep_OSU_method=TOI;
Query OK, 0 rows affected (0.00 sec)
Não vamos mostrar o restante do processo, porque é o mesmo - habilite RSU no nível da sessão, execute ALTER, habilite TOI. O mais interessante é o que aconteceria se a mudança fosse incompatível. Vamos dar novamente uma rápida olhada no esquema:
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)
Digamos que queremos alterar o tipo de coluna 'k' de INT para VARCHAR(30) em um nó.
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
Agora vamos dar uma olhada no esquema:
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)
Tudo está como esperamos - a coluna 'k' foi alterada para VARCHAR. Agora podemos verificar se esta mudança é aceitável ou não para o Galera Cluster. Para testá-lo, usaremos um dos nós restantes e inalterados para executar a seguinte consulta:
mysql> INSERT INTO sbtest1.sbtest1 (k, c, pad) VALUES (123, 'test', 'test');
Query OK, 1 row affected (0.19 sec)
Vamos ver o que aconteceu. Definitivamente não parece bom - nosso nó está inativo. Os logs fornecerão mais detalhes:
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…
Como pode ser visto, Galera reclamou do fato de que a coluna não pode ser convertida de INT para VARCHAR(30). Ele tentou reexecutar o conjunto de gravações quatro vezes, mas falhou, sem surpresa. Como tal, Galera determinou que a consistência do nó está comprometida e o nó é expulso do cluster. O conteúdo restante dos logs mostra este 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.
É claro que o ClusterControl tentará recuperar esse nó - a recuperação envolve a execução do SST, portanto, as alterações de esquema incompatíveis serão removidas, mas estaremos de volta à estaca zero - nossa alteração de esquema será revertida.
Como você pode ver, embora a execução do RSU seja um processo muito simples, por baixo pode ser bastante complexo. Requer alguns testes e preparações para garantir que você não perderá um nó apenas porque a alteração do esquema não foi compatível.