[Foo]blog

Mas [bar]blog também funciona!

Backup do PostgreSQL em ambientes S3 com Pgbackrest

Nesse artigo, demonstrarei como executar um backup e restore do PostgreSQL em ambientes compatíveis com S3.

Faremos isso através da ferramenta pgbackrest, a qual eu tenho usado com sucesso em pequenos e grandes ambientes desde 2018.

Iremos instalar e configurar a ferramenta para funcionar em disco local, e logo na sequência vamos incrementar a configuração para usar o serviço S3.

O foco desse texto é ajudar os colegas que precisam entregar backups do PostgreSQL criptografados e armazenados em ambiente S3 ou similar.

Não é foco desse artigo discutir técnicas, ferramentas de backup ou a diferenciação entre backups físico e lógico.

Caso você precise se aprofundar ou relembrar esses tópicos, sugiro começar pela leitura sobre backup no manual do PostgreSQL, pois o manual explica esse tópico muito melhor do que eu. :)

Sem mais delongas, vamos aos meus motivos para ter escolhido o pgbackrest para essa tarefa:

  • CLI funcional, rápida com output em json para usar em scripts.

  • Não existe daemon ou serviço: Você pode chamar o backup via cron, via cli ou dentro de um script.

  • Funciona com S3 (amazon e compatíveis), com disco local, remoto com ssh + rsync, azure object storage…

  • Backups criptografados com apenas 2 linhas de configuração.

  • Backups já saem do host criptografados.

  • Manifesto em TXT! :)

  • Checksum feito durante o backup e conferido durante o restore.

  • Compactação usando tar.gz.

  • Dá pra fazer replica física com 1 comando apenas. :) :) :)

  • Reciclagem automática dos dados expirados e logs de transação (wal files).

  • Backup e restore paralelizados.

  • Envio e recuperação assíncrona e paralela de wal files (FU*K YEAH!).

  • Manutenção pós implementação mínima. Pgbackrest é set-and-forget e Just works

Dos motivos acima citados, o meu favorito é justamente o sentimento de set-and-forget e just works que o pgbackrest proporciona.

Tenho backups com restore automático rodando sem nenhuma intervenção manual.

Sério, as vezes eu só lembro que determinado servidor tem backup quando o serviço S3 dá uma soluçada. Sempre que o backup soluça, o problema está em outro ponto da infra.

Isso dá muita segurança ao DBRE que precisa sair do modelo de monólito (famoso bancão de 20TB ) e migrar para um modelo onde produto ou serviço possui 30+ instâncias de PostgreSQL por baixo do front-end.

Famoso dilema pet vs cattle que somente os profissionais que já começaram a carreira em nuvem estão isentos de enfrentar.

Brincadeiras e dilemas à parte, vamos ao que interessa.

Preparando o lab

Para esse lab, você vai precisar de uma instância PostgreSQL >= 8.3, uma forma de instalar os pacotes do pgbackrest e suas dependências, além de acesso a um ambiente S3.

Eu sei que tem gente que costuma usar o min.io(https://docs.min.io) como PoC antes de implementar em ambiente produtivo, e já digo de antemão que o pgbackrest funciona bem com min.io. :)

Além disso, sugiro que o PostgreSQL seja provisionado utilizando os repositórios oficiais do PGDG.

Neste lab irei usar Centos/7, mas o conceito pode ser aplicado em qualquer distribuição linux.

Para ambientes debian-like, siga o user guide do pgbackrest para ver as diferenças.

Preparando o PostgreSQL para backup e arquivamento contínuo

Assim como o pg_basebackup, o pgbackrest também precisa algumas configurações sejam ajustadas para que o arquivamento contínuo funcione adequadamente.

Assuma, de agora em diante, que todo ajuste do PostgreSQL será feito via psql com o comando alter system, como eu fiz abaixo:

alter system set archive_mode to 'on' ; 
alter system set archive_command to '/bin/true' ;
alter system set wal_level to 'replica' ; 
alter system set max_wal_senders to '5' ; 

O parâmetro archive_mode, wal_level e max_wal_senders precisam de restart do banco para entrar em vigor.

As vezes, eu preciso subir um banco de dados e voltar nele depois para acertar o backup.

Quando isso acontece, eu já coloco o archive_command = ‘/bin/true’ justamente para poder configurar e ajustar o backup físico sem restart, já que ele é o único parâmetro que pode ser ajustado apenas com o reload do banco. ;)

Para validar a configuração, iremos usar o pg_basebackup.

Se tudo ocorreu bem, você deverá ver algo similar à saída logo abaixo.

$ pg_basebackup -v -h localhost -D /tmp/pgbasebackup_test

pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/6000028 on timeline 1
pg_basebackup: starting background WAL receiver
pg_basebackup: created temporary replication slot "pg_basebackup_6625"
pg_basebackup: write-ahead log end point: 0/6000100
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: syncing data to disk ...
pg_basebackup: renaming backup_manifest.tmp to backup_manifest
pg_basebackup: base backup completed
$ 

Se tudo correu bem, pode apagar o diretório /tmp/pgbasebackup_test e prosseguir para o próximo passo.

Instalação do pgbackrest e primeira execução

Instalar o pgbackrest é fácil se você configurou o repositório do EPEL e PGDG. Se não o fez, sugiro que faça para facilitar o seu trabalho.

Uma vez configurados, basta executar o yum install em um terminal, como root ou sudo.

# yum install -y pgbackrest

Esse comando irá instalar o pgbackrest junto com as dependências.

Uma vez instalado, precisamos executar algumas tarefas adicionais antes de fazer o primeiro backup.

Criando o stanza

O pgbackrest trabalha com o conceito de stanza, que nada mais é do que um agrupamento de servidores PostgreSQL (primário e secundários), dos quais o pgbackrest irá puxar os dados.

Abra o arquivo de /etc/pgbackrest.conf com o seu editor favorito e ajuste para que fique parecido com o arquivo abaixo:

# Ajustes globais, validos para todos os stanzas
# Aqui você vai ajustar usuário, diretório onde ficarão os datafiles
# Se o backup vai para S3 ou disco local, etc. 

[global]
repo1-path=/pgbackrest

# Configuração da stanza. 
# Ajuste de acordo com o data_directory do seu PostgreSQL 
# (use o show data_directory via psql ;) 
# Caso o backup seja em um banco remoto, aqui vão as informações como host, porta e user. 

[lab_pgbackrest]
pg1-path=/pgdata/13

Aproveite e ajuste as permissões para dispensar o uso do root na execução dos backups.

# mkdir /pgbackrest 
# chown postgres:postgres /pgbackrest 
# chown postgres:postgres /etc/pgbackrest.conf  
# chown postgres:postgres /var/lib/pgbackrest/
# chown postgres:postgres /var/log/pgbackrest/

Agora, via psql, ajuste o archive_command.

alter system set archive_command to 'pgbackrest --log-level-console=info --stanza=lab_pgbackrest archive-push %p' ; 
select pg_reload_conf() ; 
show archive_command ; 

O resultado deve ser algo parecido com:

                                archive_command                                
-------------------------------------------------------------------------------
 pgbackrest --log-level-console=info --stanza=lab_pgbackrest archive-push %p
 

Validando a configuração e executando o primeiro backup.

Essa configuração é básica, porém suficiente para um test-run em disco local.
Em um terminal, com o usuário postgresql, execute:

$ pgbackrest --log-level-console=info --stanza=lab_pgbackrest stanza-create
2021-02-28 16:58:46.102 P00   INFO: stanza-create command begin 2.32: --exec-id=7102-3983e85a --log-level-console=info --pg1-path=/pgdata/13 --repo1-path=/pgbackrest --stanza=lab_pgbackrest
2021-02-28 16:58:46.738 P00   INFO: stanza-create for stanza 'lab_pgbackrest' on repo1
2021-02-28 16:58:46.767 P00   INFO: stanza-create command end: completed successfully (669ms)
$ 

Como você pode notar, a stanza foi criada com sucesso, logo, podemos checar se tudo correu conforme planejado com o comando check.

$ pgbackrest --log-level-console=info --stanza=lab_pgbackrest check 

E o resultado correto, como esperado:

2021-02-28 17:04:07.296 P00   INFO: check command begin 2.32: --exec-id=7214-555bb015 --log-level-console=info --pg1-path=/pgdata/13 --repo1-path=/pgbackrest --stanza=lab_pgbackrest
2021-02-28 17:04:08.131 P00   INFO: check repo1 configuration (primary)
2021-02-28 17:04:08.400 P00   INFO: check repo1 archive for WAL (primary)
2021-02-28 17:04:08.520 P00   INFO: WAL segment 000000010000000000000009 successfully archived to '/pgbackrest/archive/lab_pgbackrest/13-1/0000000100000000/000000010000000000000009-dd2ab76718adf805db35c2c1e08f0dbca913a46f.gz' on repo1
2021-02-28 17:04:08.520 P00   INFO: check command end: completed successfully (1227ms) 

Para executar um backup full, digite:

$ pgbackrest --log-level-console=info --stanza=lab_pgbackrest --type=full backup 

E consulte os logs, como de costume:

...
2021-02-28 17:08:38.881 P00   INFO: full backup size = 31.8MB
2021-02-28 17:08:38.881 P00   INFO: execute non-exclusive pg_stop_backup() and wait for all WAL segments to archive
2021-02-28 17:08:39.083 P00   INFO: backup stop archive = 00000001000000000000000B, lsn = 0/B000138
2021-02-28 17:08:39.084 P00   INFO: check archive for segment(s) 00000001000000000000000B:00000001000000000000000B
2021-02-28 17:08:39.099 P00   INFO: new backup label = 20210228-170834F
2021-02-28 17:08:39.129 P00   INFO: backup command end: completed successfully (5062ms)
2021-02-28 17:08:39.130 P00   INFO: expire command begin 2.32: --exec-id=7222-99642633 --log-level-console=info --repo1-path=/pgbackrest --stanza=lab_pgbackrest
2021-02-28 17:08:39.132 P00   INFO: option 'repo1-retention-archive' is not set - archive logs will not be expired
2021-02-28 17:08:39.133 P00   INFO: expire command end: completed successfully (4ms)

Não vou entrar em detalhes sobre o funcionamento do pgbackrest neste artigo.

Por hora, basta saber que os logs de transação, junto com os datafiles, estão sendo armazenados em /pgbackrest.

Para maiores detalhes, consulte a documentação oficial do projeto.

Antes de migrar para a nuvem, lembre-se de remover o diretório local de backup.

Do modo standalone para a nuvem

Agora que já temos um backup funcional, vamos ajustar o conf para funcionar em ambientes similares ao AWS S3.

O primeiro passo é a criação do bucket. Você pode usar o awscli, s3cmd, ou a interface web. O importante é criar o bucket, e eu irei usar o cli para isso. :)

$ aws s3 mb s3://labpgbackrest 

Com o bucket criado, vamos configurar o arquivo conf do pgbackrest. As senhas foram omitidas por motivos óbvios. ;)

[lab_pgbackrest]
pg1-path = /pgdata/13

[global]
repo1-path = /


# Dois processos simultâneos. 
process-max = 2

# Essas duas linhas ajustam a criptografia. 
# O backup já sai do host criptografado. 

repo1-cipher-type = aes-256-cbc
repo1-cipher-pass = ********

# Parâmetros  válidos para o S3. 
repo1-type = s3
repo1-s3-bucket = labpgbackrest
repo1-s3-endpoint = s3.amazonaws.com
repo1-s3-key = AK**************
repo1-s3-key-secret = *************************
repo1-s3-region = us-east-1
repo1-s3-ca-file=/etc/pki/tls/certs/ca-bundle.crt

# Número de retenções full, em número de execuções  
repo1-retention-full = 3

# Faz um checkpoint no banco e inicia o backup. 
start-fast = y

# Habilita compressão também para os wal-files
[global:archive-push]
compress-level = 3

O próximo passo é repetir o loop ‘cria stanza - testa - faz backup - verifica’, só que agora estamos usando a nuvem ao invés de disco local.

pgbackrest --log-level-console=info --stanza=lab_pgbackrest  stanza-create  
pgbackrest --log-level-console=info --stanza=lab_pgbackrest check  
pgbackrest --log-level-console=info --stanza=lab_pgbackrest --type=full  backup 
pgbackrest --log-level-console=info --stanza=lab_pgbackrest info 

O resultado deve ser algo similar à saída abaixo:

$ pgbackrest info
stanza: lab_pgbackrest
    status: ok
    cipher: aes-256-cbc

    db (current)
        wal archive min/max (13): 00000001000000000000000F/000000010000000000000011

        full backup: 20210301-150716F
            timestamp start/stop: 2021-03-01 15:07:16 / 2021-03-01 15:09:45
            wal start/stop: 000000010000000000000011 / 000000010000000000000011
            database size: 31.9MB, database backup size: 31.9MB
            repo1: backup set size: 3.9MB, backup size: 3.9MB
$ 

Notem que, ao ativar a compressão, o tamanho diminuiu de 31 MB para apenas 4. ;)

Criando a rotina de agendamentos.

Como eu mencionei, um dos motivos pelos quais eu amo o pgbackrest é a filosofia unix-like. Para que criar um daemon se um crontab resolve?

E é exatamente isso que eu faço em minhas instâncias EC2 e similares. Eu jogo o comando que faz o backup no crontab, deixando eles mais ou menos assim:

# Backup full no domingo
00 01 * * 0   pgbackrest --log-level-console=info --type=full --stanza=lab_pgbackrest backup

# Backup incremental de segunda a sábado 
00 01 * * 1-6 pgbackrest --log-level-console=info --stanza=lab_pgbackrest backup

Note que o backup padrão executa em modo incremental. Nesse modelo, eu estou mantendo 3 semanas de backup, o que eu julgo suficientes para um cenário de recuperação de desastres.

Backup ok!! E o restore?

Se potência não é nada sem controle, backup não é nada sem restore.

Uma das atribuições do DBRE é garantir o RPO e o RTO do negócio. E para que isso aconteça, é necessário testar o restore periodicamente.

Já que estamos com a mão na massa, vamos criar um cenário para testar o nosso processo de restore. Infelizmente, terei que deixar o Point in Time recovery para uma próxima ocasião.

Para isso, usando o psql, vamos criar uma database e inserir alguns valores:

create database sensors ; 
\c sensors 

create table readings ( id serial primary key, sensor_id INTEGER, sensor_reading FLOAT) ; 

\d readings
                                     Table "public.readings"
     Column     |       Type       | Collation | Nullable |               Default                
----------------+------------------+-----------+----------+--------------------------------------
 id             | integer          |           | not null | nextval('readings_id_seq'::regclass)
 sensor_id      | integer          |           |          | 
 sensor_reading | double precision |           |          | 
Indexes:
    "readings_pkey" PRIMARY KEY, btree (id)

Agora, vamos criar uma pl dentro da database sensors para inserir registros aleatórios. ( pl/psSQL é só amor!!!! )

CREATE OR REPLACE FUNCTION fake_readings(n INTEGER)
    RETURNS void
    AS $$
BEGIN
    FOR counter IN 1..n  LOOP
        INSERT INTO readings (sensor_id, sensor_reading)
            VALUES ( 
                    floor(random() * 10 + 1)::int, 
                    random()
                    );
    END LOOP;
END;
$$
LANGUAGE plpgsql;

E agora, vamos inserir 1000 registros nessa tabela.

-- Chamando a nossa pl
select * from fake_readings(1000); 

-- Listando os primeiros 10 valores

select * from readings LIMIT 10 ; 
 id | sensor_id |   sensor_reading    
----+-----------+---------------------
  1 |         1 | 0.11981797193904953
  2 |        10 |  0.4001936621540949
  3 |         5 |  0.6047034919492162
  4 |         8 | 0.31190874926387835
  5 |         6 | 0.04626358079672599
  6 |         7 |  0.7051340248007989
  7 |        10 |  0.8974671876753249
  8 |         9 |  0.8003084821880151
  9 |         4 |  0.7029617047361043
 10 |         3 | 0.23796749461166655
(10 rows)

Após inserir os primeiros 1000 registros, vamos efetuar um backup full.

pgbackrest --log-level-console=info --type=full --stanza=lab_pgbackrest backup

Agora, vamos parar o banco de dados e remover o diretório de dados.

$ /usr/pgsql-13/bin/pg_ctl -D /pgdata/13 stop
$ rm -rfv /pgdata/13/*  
$ pgbackrest --log-level-console=info --stanza=lab_pgbackrest --delta restore

Após terminar o restore, suba o banco de dados e verifique se os dados estão como deveriam estar:

/usr/pgsql-13/bin/pg_ctl -D /pgdata/13 start

Abra o psql veja se os 1000 registros estão lá:

\c sensors 
sensors=# select * from readings LIMIT 10 ;  
 id | sensor_id |   sensor_reading    
----+-----------+---------------------
  1 |         1 | 0.11981797193904953
  2 |        10 |  0.4001936621540949
  3 |         5 |  0.6047034919492162
  4 |         8 | 0.31190874926387835
  5 |         6 | 0.04626358079672599
  6 |         7 |  0.7051340248007989
  7 |        10 |  0.8974671876753249
  8 |         9 |  0.8003084821880151
  9 |         4 |  0.7029617047361043
 10 |         3 | 0.23796749461166655
(10 rows)

Aparentemente, o restore foi feito com sucesso. :P

Conclusão

Se você chegou até aqui, parabéns. Você tem conhecimento suficiente para provisionar um pgbackrest em ambiente S3 com criptografia.

Espero, em uma próxima ocasião, abordar o tema com mais profundidade, criar cenários mais complexos e demonstrar funcionalidades avançadas, porém, eu preciso da sua ajuda.

Se esse texto te ajudou de alguma forma, por favor, compartilhe com os amigos, deixe um comentário ou sugestão de melhoria.

Ajude o conhecimento chegar em quem precisa. ;)