Monday, February 20, 2017

Replicação: o que mudou

A mais de 6 anos atrás, eu escrevi o artigo Hot Standby e Streaming Replication que descreve uma das funcionalidades mais aguardadas na época: replicação nativa. De lá para cá houveram diversas melhorias nessa funcionalidade que a tornaram sólida para diversas arquiteturas de sistemas. Irei atualizar os passos descritos no post antigo (que por sinal ainda são válidos) com opções que melhoraram a implantação da replicação no PostgreSQL.
A versão 9.0 implementou a replicação nativa baseada no envio de registros (streaming replication). Um protocolo foi definido para o envio de registros do WAL do servidor principal para o servidor secundário. O servidor secundário tem a capacidade de aceitar consultas somente leitura (hot standby). Em resumo, foi adicionado suporte a um servidor principal e vários servidores secundários (aceitando ou não consultas).
Da versão 9.1 a 9.6 houveram várias melhorias que tornaram essa solução mais sólida e robusta a cada versão. Destaco o suporte a: replicação síncrona (9.1), replicação em cascata (9.2), possibilidade de seguir nova linha do tempo (9.3), slots de replicação (9.4), replicação com atraso (9.4), resincronizar servidor principal antigo com pg_rewind (9.5), múltiplos servidores síncronos ao mesmo tempo (9.6).
A Timbira contribui com duas funcionalidades nessa evolução: função pg_xlog_location_diff (calcular diferença entre posições do WAL -- útil para descobrir o lag de replicação) e recovery_min_apply_delay (atrasar a aplicação do WAL no servidor secundário -- útil para recuperar de desastres tais como remover um banco de dados em produção).
O cenário que utilizaremos é o mesmo do artigo anterior cuja ilustração está na figura abaixo.


O cenário

  • 2 servidores PostgreSQL: um servidor principal (aceita leitura e modificação de dados) e um servidor secundário (aceita ou não consultas somente leitura). Estes servidores devem ser preferencialmente (quase) idênticos pois a replicação é "binária", sendo incompatível em arquiteturas e/ou sistemas operacionais distintos. (Em soluções de alta disponibilidade, utilizar um servidor secundário muito inferior ao servidor principal pode ser inaceitável para negócio. Certifique-se que o servidor secundário tenha capacidade de substituir o servidor principal.);
  • versão do PostgreSQL: a mesma versão em ambos servidores. Com mesma versão quero dizer que os dois primeiros números devem ser iguais (por exemplo, 9.1.3 e 9.1.24, 9.3.10 e 9.3.15, 9.6.2 e 9.6.2). Se for usar versões corretivas diferentes (9.3.10 x 9.3.15), mesmo que momentâneo para alguma atualização, é recomendado manter os servidores secundários com versões corretivas mais recentes para evitar que alguma incompatibilidade (causada por um bug) seja replicada e não consiga ser aplicada em uma versão corretiva anterior;
  • rede: a replicação utiliza o protocolo do PostgreSQL e, portanto, todo o tráfego será através da porta do serviço (padrão é 5432). Certifique-se que o servidor secundário consiga acessar o servidor principal a partir da porta do serviço. Se utilizarmos o pg_basebackup para montar o servidor secundário, não há necessidade de configuração especial (ele transmite os dados do agrupamento através do próprio protocolo do PostgreSQL); no entanto, a utilização de outro método de cópia (ssh, rsync, etc) demandará ajuste no acesso para a(s) porta(s) utilizada(s);
  • acesso: o acesso root não é obrigatório se o local do diretório de dados do servidor secundário estiver com as devidas permissões para o usuário do serviço do PostgreSQL (comumente usuário postgres). A partir da versão 9.1, o usuário que replica os dados precisa ter a permissão REPLICATION ou ser superusuário; na 9.0, o usuário deve ser superusuário;
  • endereço de rede: para o servidor principal e o servidor secundário usaremos 10.1.1.1 e 10.1.1.2, respectivamente;
  • diretório de dados: para o servidor principal usaremos /bd/primario e o servidor secundário usaremos /bd/secundario;
  • usuário do serviço: o usuário do sistema operacional em ambos os servidores será o postgres.

No servidor principal


Começamos alterando a configuração do postgres (/bd/primario/postgresql.conf) no servidor principal:

listen_addresses = '*'
wal_level = replica               # hot_standby até 9.5
max_wal_senders = 3
max_replication_slots = 3    # parâmetro a partir da 9.4

O parâmetro listen_addresses permite que o servidor principal escute todos os endereços da rede (o pg_hba.conf faz restrição por endereço). O parâmetro wal_level determina a quantidade de informação vai para WAL (replica permitirá arquivamento do WAL e replicação). O parâmetro max_wal_senders indica o número máximo de servidores secundários ou aplicações para backup base (tais como pg_basebackup e pg_receivexlog). O parâmetro max_replication_slots especifica o número máximo de slots de replicação permitidos.

É possível prevenir a reciclagem do WAL (que ainda é útil para algum servidor secundário) utilizando o parâmetro wal_keep_segments ou arquivando o WAL com o parâmetro archive_command. O parâmetro wal_keep_segments especifica um número mínimo de arquivos de log de transação a serem mantidos no pg_xlog caso algum servidor secundário necessite obtê-los via replicação. O parâmetro archive_command especifica um comando utilizado para copiar o arquivo de log de transação (que foi recentemente preenchido) para algum local acessível pelos servidores secundários. Contudo, esses métodos podem reter mais arquivos do que o necessário enquanto o slot de replicação visa reter somente o WAL necessário para algum servidor secundário. Uma desvantagem do slot de replicação é que ele não limita a quantidade de arquivos do WAL armazenados no diretório pg_xlog. Isso quer dizer que se algum servidor secundário for desligado e o DBA esquecer de remover o slot de replicação dele, o pg_xlog vai crescer indefinidamente.

As modificações no arquivo /bd/primario/postgresql.conf necessitam de um reinício do serviço.

postgres@principal:~$ pg_ctl restart -D /bd/primario
esperando o servidor desligar.....feito
servidor está parado
servidor está iniciando

A replicação usará uma conexão do postgres. E para realizar essa conexão utilizaremos uma role específica para replicação. A versão 9.0 exigia um superusuário para tal mas a partir da 9.1 essa role pode ter simplesmente a permissão REPLICATION.

postgres@principal:~$ psql
psql (9.6.2)
Digite "help" para ajuda.

postgres=# CREATE ROLE replicacao REPLICATION LOGIN;
CREATE ROLE
postgres=# \q

Dependendo de sua política de segurança, você precisará definir uma senha para a role replicacao. A senha poderá ser informada na string de conexão no arquivo /bd/secundario/recovery.conf ou preferencialmente em um arquivo de senhas (~postgres/.pgpass ou %APPDATA%\postgresql\pgpass.conf no Windows) no servidor secundário.
postgres@principal:~$ psql
psql (9.6.2)
Digite "help" para ajuda.

postgres=# \password replicacao
Digite nova senha:
Digite-a novamente:
postgres=# \q

Precisamos ajustar o acesso no servidor principal. Isso porque a conexão é feita do servidor secundário para o servidor principal. O arquivo /bd/primario/pg_hba.conf controla esse acesso. Os acessos de replicação são identificados utlizando replication com nome do banco de dados (se você tiver um banco chamado replication, ele deve estar entre aspas duplas). Se você decidir utilizar autenticação por senha, o método de autenticação pode ser md5.

host    replication    replicacao    10.1.1.2/32    md5

A adição de uma nova regra de autenticação exige um reload do serviço.

postgres@principal:~$ pg_ctl reload -D /bd/primario
servidor foi sinalizado

No servidor secundário


O próximo passo é obter uma cópia base (base backup) para montar o servidor secundário. A cópia base é uma cópia de todo agrupamento de dados (PGDATA) incluindo os diretórios de todas as tablespaces. Ilustraremos três maneiras de realizar essa cópia dependendo da disponibilidade do serviço.

1. Servidor principal parado


Se o servidor puder ficar parado durante a cópia, podemos fazer:
postgres@principal:~$ pg_ctl stop -D /bd/primario
esperando o servidor desligar.....feito
servidor está parado
postgres@principal:~$ rsync -av --exclude postgresql.auto.conf.tmp --exclude postmaster.pid --exclude postmaster.opts --exclude pg_stat_tmp/* --exclude pg_replslot/* --exclude pg_dynshmem/* --exclude pg_notify/* --exclude pg_serial/* --exclude pg_snapshots/* --exclude pg_subtrans/* --exclude pg_xlog/* --exclude pg_log/* /bd/primario/ postgres@10.1.1.2:/bd/secundario
sending incremental file list
./
PG_VERSION
pg_ident.conf
postgresql.auto.conf
.
.
.
pg_tblspc/
pg_twophase/
pg_xlog/

sent 364,684,662 bytes  received 96,744 bytes  9,234,972.30 bytes/sec
total size is 364,298,395  speedup is 1.00
postgres@principal:~$ pg_ctl start -D /bd/primario
servidor está iniciando

No comando rsync não esqueça da barra ao final do nome do diretório. Ele vai indicar que copiaremos somente o conteúdo de /bd/primario e não o diretório primario. Alguns arquivos e/ou diretórios excluídos (--exclude) podem não existir na sua versão, ignore-os. Os diretórios pg_stat_tmp e pg_log podem ser configurados pelo DBA, certifique-se que eles não foram alterados para outros valores.

2. Servidor principal em atividade


Caso você queira fazer a cópia base com o servidor em atividade, precisamos executar alguns passos além da cópia. O slot de replicação foi implementado na versão 9.4. Em versões anteriores, ignore o primeiro comando e utilize o arquivamento ou o parâmetro wal_keep_segments conforme explicado acima.
postgres=# SELECT * FROM pg_create_physical_replication_slot('secundario', true);
 slot_name   | xlog_position
-------------------+---------------------
 secundario  | 0/99005D50
(1 registro)

postgres=# SELECT pg_start_backup('replica', true);
 pg_start_backup
--------------------------
 0/9A000060
(1 registro)

postgres=# \q
postgres@principal:~$ rsync -av --exclude postgresql.auto.conf.tmp --exclude postmaster.pid --exclude postmaster.opts --exclude pg_stat_tmp/* --exclude pg_replslot/* --exclude pg_dynshmem/* --exclude pg_notify/* --exclude pg_serial/* --exclude pg_snapshots/* --exclude pg_subtrans/* --exclude pg_xlog/* --exclude pg_log/* /bd/primario/ postgres@10.1.1.2:/bd/secundario
sending incremental file list
./
PG_VERSION
backup_label
pg_ident.conf
.
.
.
pg_twophase/
pg_xlog/

sent 364,626,339 bytes  received 96,728 bytes  145,889,226.80 bytes/sec
total size is 364,240,052  speedup is 1.00
postgres@principal:~$ psql
psql (9.6.2)
Digite "help" para ajuda.

euler=# SELECT pg_stop_backup();
NOTA:  pg_stop_backup concluído, todos os segmentos do WAL foram arquivados
 pg_stop_backup
-------------------------
 0/9BDD1B38
(1 registro)

euler=# \q

Criamos um slot de replicação (com a função pg_create_physical_replication_slot) para limitar a reciclagem do WAL porque o servidor secundário precisará dele. O segundo parâmetro da função é importante porque a reserva é feita imediatamente ao invés de fazer somente na primeira conexão (sem a reserva imediata, uma cópia base que demora algumas horas poderia remover WAL necessário para montar a réplica). Em versões anteriores, faça arquivamento ou utilize o parâmetro wal_keep_segments.

A função pg_start_backup prepara o servidor principal para iniciar uma cópia base. Após a sua execução podemos fazer a cópia mesmo que arquivos estejam sendo criados (tabelas/índices novos) e/ou excluídos. A cópia base exclui alguns arquivos que podem ser diferentes no servidor secundário (como postgresql.conf e pg_hba.conf) ou que não são necessários (vide parâmetros --exclude no comando rsync). Executamos a função pg_stop_backup para indicar o fim da cópia.

3. pg_basebackup


O comando pg_basebackup automatiza a geração de cópias base com servidores postgres em atividade. A cópia base é feita utilizando uma conexão do postgres e utiliza o protocolo de replicação. As configurações feitas no servidor principal e o usuário criado são os pré-requisitos para executar o pg_basebackup. O pg_basebackup deve ser executado a partir do servidor secundário.

postgres@secundario:~$ pg_basebackup --format=plain --xlog-method=stream --checkpoint=fast --progress -D /bd/secundario -R -v -h 10.1.1.1 -p 5432 -U replicacao
ponto de início do log de transação: 0/16001FD8 na linha do tempo 1
pg_basebackup: iniciando receptor do WAL em segundo plano
98860/98860 kB (100%), 1/1 tablespace                                        
transaction log end point: 0/16076238
pg_basebackup: esperando processo em segundo plano terminar o envio ...
pg_basebackup: cópia base completa

No exemplo acima, utilizamos o formato plain, que mantém o mesmo layout dos arquivos e diretórios do servidor principal. Se tiver tablespaces, elas vão para o mesmo caminho absoluto definido do servidor principal (certifique-se que os caminhos existam antes de executar o pg_basebackup). O parâmetro --xlog-method especifica que o pg_basebackup irá incluir todos os arquivos de log de transação necessários. O valor stream indica que irá copiar o WAL em paralelo com a cópia base, necessitando para isso de uma conexão extra com o servidor principal (o outro valor é fetch mas só copia o WAL ao final da cópia base, necessitando que não haja reciclagem do WAL -- vide wal_keep_segments). O parâmetro --checkpoint controla se o CHECKPOINT será espalhado (spread) ou rápido (fast) -- a opção fast pode causar um pico de IO; use-a somente em horários de baixa carga. A opção -D especifica o diretório que o agrupamento de dados do servidor secundário será armazenado. A opção -R gera o arquivo recovery.conf (com as opções informadas pelo pg_basebackup) no diretório de saída (-D) para facilitar a montagem do servidor secundário. As opções -h (host), -p (porta) e -U (usuário) são relativas a conexão com servidor principal.

Iniciando a réplica


No servidor secundário, o próximo passo é criar o arquivo /bd/secundario/recovery.conf. A presença desse arquivo indica para o postgres que ele deve iniciar em modo de recuperação (a replicação é uma recuperação contínua).
standby_mode = 'on'
primary_conninfo = 'host=10.1.1.1 port=5432 user=replicacao'
O parâmetro standby_mode garante que o servidor secundário ficará em recuperação contínua indefinidamente. Para replicação por fluxo, é necessário informar o parâmetro primary_conninfo especificando como se conectar no servidor principal.

Se o servidor secundário ficar muito atrasado em relação ao servidor principal, a conexão de replicação pode ser interrompida porque o arquivo de log de transação que contém o registro a ser replicado, já foi reciclado no servidor principal. É uma cenário que pode ocorrer ao realizar cargas de dados e/ou manutenções. As soluções, conforme detalhamos acima, são: wal_keep_segments, arquivamento ou slots. Para wal_keep_segments, basta ajustá-lo no servidor principal. O arquivamento deve ser feito no servidor principal e o local de armazenamento deve ser acessível pelo servidor secundário.

No servidor principal, crie o diretório /bd/archives com permissão de escrita para usuário postgres e edite o arquivo /bd/primario/postgresql.conf:
archive_mode = on
archive_command = 'cp %p /bd/archives/%f'

Um reinício do postgres no servidor principal é necessário para que o arquivamento seja ativado.

E no servidor secundário, edite arquivo /bd/secundario/recovery.conf:
restore_command = 'scp postgres@10.1.1.1:/bd/archives/%f %p'
Nesse caso (scp), use um par de chaves para acesso via ssh sem solicitar senha.

Por fim, se optar por utilizar slots (versão >= 9.4), crie o slot com a função pg_create_physical_replication_slot conforme detalhado acima e adicione o seguinte parâmetro no arquivo /bd/secundario/recovery.conf:
primary_slot_name = 'secundario'

O nome do slot é aquele que você informou no primeiro parâmetro da função. Para obter os slots, utilize a seguinte consulta:
postgres=# SELECT slot_name, slot_type, active, restart_lsn FROM pg_replication_slots;
 slot_name   | slot_type | active | restart_lsn
-------------------+--------------+---------+--------------------
 secundario  | physical  | f         | 0/1A272848
(1 registro)

Mais alguns ajustes no arquivo /bd/secundario/postgresql.conf.
hot_standby = on
hot_standby_feedback = on

O parâmetro hot_standby permitirá consultas somente leitura no servidor secundário. O parâmetro hot_standby_feedback faz com que o servidor secundário envie informações ao servidor principal para impedir o cancelamento de consultas no servidor secundário. A desvantagem é o inchaço de tabelas no servidor principal porque algumas consultas longas no servidor secundário ainda utilizam registros que já foram removidos no servidor principal e não podem ser limpados porque provocariam o cancelamento da consulta no servidor secundário.

Se o objetivo do servidor secundário é alta disponibilidade, é preferível não ajustar hot_standby_feedback e manter os valores de max_standby_archive_delay e max_standby_streaming_delay baixos, assim o servidor secundário não ficará tão distante do servidor principal por causa de atrasos provocados por consultas longas.

Todavia, se o objetivo do servidor secundário é executar consultas longas habilite hot_standby_feedback e/ou defina os valores de max_standby_archive_delay e max_standby_streaming_delay altos ou mesmo -1 (infinito). Tenha em mente que manter um atraso grande fará com que outras conexões não vejam mudanças recentes no servidor principal (porque elas ainda não foram aplicadas).

O último passo é iniciar o serviço no servidor secundário:
postgres@secundario:~$ pg_ctl start -D /bd/secundario
postgres@secundario:~$ cat postgresql-2017-02-20_213649.log
LOG:  sistema de banco de dados foi interrompido; última execução em 2017-02-20 21:28:06 BRT
LOG:  entrando no modo em espera
LOG:  redo inicia em 0/1B000028
LOG:  estado de recuperação consistente alcançado em 0/1B0000F8
LOG:  sistema de banco de dados está pronto para aceitar conexões somente leitura
LOG:  iniciado fluxo de WAL do principal em 0/1C000000 na linha do tempo 1

Para monitorar a replicação, consulte a visão pg_stat_replication no servidor principal:
postgres=# SELECT *, pg_xlog_location_diff(sent_location, replay_location) AS lag_replay FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------------------------
pid                      | 18943
usesysid             | 16384
usename             | replicacao
application_name | walreceiver
client_addr          |
client_hostname  |
client_port           | -1
backend_start     | 2017-02-20 21:36:49.788049-03
backend_xmin     |
state                   | streaming
sent_location      | 0/1FE534C0
write_location     | 0/1FE534C0
flush_location     | 0/1FE4F4F0
replay_location   | 0/1FE4F4F0
sync_priority       | 0
sync_state          | async
lag_replay           | 16336
Lembre-se de remover o slot (se estiver utilizando), após desativar algum servidor secundário. Um slot sem uso retém arquivos de log de transação indefinidamente (podendo encher a sua partição rapidamente se o espaço for reduzido). Para remover o slot, execute:
postgres=# SELECT pg_drop_replication_slot('secundario');
 pg_drop_replication_slot
-------------------------------------

(1 registro)