Com o objetivo de otimizar o desempenho, os Sistemas Gerenciadores de Banco de Dados (SGBDs) modernos fazem intensivo uso da memória principal. Dessa forma, o disco rígido, por ser muito mais lento que a RAM, não é acessado a todo momento. Por causa disso, surge o seguinte cenário: um usuário inicia uma transação, efetua algumas alterações nos dados, e confirma a transação (commit), porém, antes que o SGBD grave as operações no arquivo de dados, uma falha acontece – falta de energia no servidor, por exemplo. E agora? Qual o mecanismo utilizado pelos SGBDs para garantir a durabilidade das transações nestes cenários? Nesse artigo, conceituaremos duas técnicas baseadas em log que são utilizadas pela maioria dos SGBDs: recuperação adiada e recuperação imediata. E, na seqüência, comentaremos sobre a estratégia escolhida pelo Postgresql para recuperação em caso caso de falhas.
Entendendo o Cenário
Para entendermos melhor as situações nas quais transações podem se encontrar no momento de uma falha, vejamos a Figura 1. Observamos a existência de uma operação denominada CHECKPOINT, que é o processo do SGBD responsável por transferir os dados de transações comitadas ou não (a depender do tipo de recuperação implementada), da memória principal para o arquivo de dados. O Banco pode, basicamente, deparar-se com cinco situações:
· Transação iniciada e comitada antes do checkpoint (T1);
· Transação iniciada antes do checkpoint e comitada após o checkpoint, porém antes da falha ocorrer (T2);
· Transação iniciada antes do checkpoint e não comitada devido à falha (T3);
· Transação iniciada e comitada após o checkpoint, contudo antes da falha (T4);
· Transação iniciada após o checkpoint e não comitada devido à falha (T5).
Quais seriam as transações que o Banco deveria recuperar ao ser iniciado após uma falha? O SGBD deve recuperar todas as transações que foram efetivadas (comitadas) antes do crash, portanto as transações T2 e T4 devem ser restauradas e suas operações efetivadas no arquivo de dados, ou seja, após a restauração o Banco deve forçar um checkpoint. Com a transação T1 não precisamos nos preocupar, pois no momento do checkpoint ela foi persistida. E quanto às transações T3 e T5? Vai depender da estratégia adotada pelo SGBD, como veremos adiante.
O que possibilita ao Banco recuperar as transações é um log criado em disco onde ele registra as operações de cada transação, bem como os checkpoints realizados. Um instante: acabamos de dizer que o SGBD não utiliza muito o disco para ganhar velocidade, então por que o arquivo de log não degrada tanto a performance? A resposta é muito simples: ao contrário do que acontece no arquivo de dados onde o acesso é aleatório, o arquivo de log é sempre seqüencial. Dessa forma, o acesso ao disco se torna muito eficiente, pois o cabeçote de leitura/gravação não precisa ficar se deslocando para áreas não contíguas.
Portanto, sempre mantenha o arquivo de log em um disco que não seja o utilizado pelo arquivo de dados. Percebam que isso não é uma imposição dos SGBDs, mas é altamente recomendado para que o Banco consiga ser mais eficiente.
A recuperação adiada e a imediata diferem entre si pela estratégia utilizada durante o checkpoint e, por conseqüência, em como esses dados são restaurados no caso de falha.
Recuperação Adiada (REDO)
Nessa estratégia, o checkpoint só grava no arquivo de dados as transações que estão marcadas como comitadas na memória principal. As operações de outras transações são somente feitas no buffer de banco de dados e registradas no arquivo de log. Assim sendo, transações não comitadas têm suas operações ignoradas no momento da recuperação (veja isso como um rollback do sistema), enquanto as transações que foram efetivadas devem ser refeitas e persistidas no arquivo de dados.
A implementação dessa estratégia é relativamente simples, pois não precisamos nos preocupar com as transações que não foram efetivadas. É necessário somente que as transações iniciadas e comitadas antes da falha sejam refeitas (REDO). Dessa forma, no arquivo de log não é necessário que o Banco armazene os valores antigos dos dados, porque nada será desfeito.
Vamos fazer uma análise do cenário das transações (Figura 1) de acordo com essa recuperação:
* Nada é preciso ser feito para T1, pois no momento do checkpoint ela foi persistida no arquivo de dados;
* A T2 precisa ser totalmente refeita, ou seja, partindo das informações registradas no arquivo de log, todas as operações de T2 são reprocessadas;
* A T3 é ignorada (rollback). Dessa forma, todas as operações são totalmente descartadas, tendo em vista que não foram comitadas até o momento da falha;
* Assim como T2, a T4 precisa ser totalmente refeita;
* Pelo mesma razão de T3, a T5 é ignorada (rollback).
Para transações curtas essa é uma estratégia interessante devido a sua simplicidade de implementação. Contudo, longas transações consomem mais buffers de banco de dados, pois todas as operações não comitadas são mantidas na RAM até que sejam efetivadas e o processo de checkpoint faça a transferência para o arquivo de dados.
Em seguida, demonstramos o algoritmo para a recuperação adiada.
1. Processar o arquivo de log de trás para frente até encontrar o checkpoint preenchendo duas listas: transações comitadas (LTC) e transações iniciadas (LTI);
2. Buscar ponto de partida da restauração:
3. Se a LTC estiver contida na LTI, o ponto de partida é o checkpoint;
4. Se existir alguma transação na LTC que não esteja na LTI, continuar fazendo a leitura do arquivo de log até LTI = LTC;
5. Processar o arquivo de log seqüencialmente refazendo (REDO) as operações das transações contidas na LTC;
6. Forçar um chekpoint.
Recuperação Imediata (UNDO/REDO)
Na recuperação imediata, todos os dados que foram alterados por transações comitadas ou não, são persistidas no arquivo de dados. Por essa razão, algumas transações precisam ser desfeitas (UNDO) e outras refeitas (REDO) no momento da recuperação.
Com a necessidade do UNDO, alguns controles adicionais precisam ser adicionados ao arquivo de log. O SGBD precisa conhecer, além dos valores alterados, os valores antes da modificação.
Baseado nessas diferenças, analisaremos o cenário das transações de acordo com essa estratégia:
* Nada é preciso ser feito para T1, pois no momento do checkpoint ela foi persistida no arquivo de dados;
* Como na recuperação adiada, a T2 precisa ser refeita a partir do checkpoint;
* A T3 precisa ser desfeita até o momento do checkpoint, portanto, todas as alterações feitas no arquivo de dados devem retornar ao seu estado anterior;
* A T4 precisa ser totalmente refeita;
* A T5 é descartada (rollback);
A implementação dessa abordagem é mais complexa do que a anterior, porém a retenção de memória tende a ser muito menor.
Vejamos o algoritmo para recuperação imediata:
1. Processar o arquivo de log na ordem reversa até encontrar o checkpoint montando uma lista de transações comitadas (LTC);
2. A lista de transações ativas (LTA) pode ser preenchida por dados de controle contidos no próprio checkpoint;
3. Desfazer (UNDO) todas as operações de gravação das transações contidas na LTA, começando do checkpoint ao início do log;
4. Refazer (REDO) todas as operações de gravação das transações contidas na LTC, do checkpoint ao final do arquivo;
5. Forçar um checkpoint.
Write-Ahead Logging (WAL)
Finalmente, depois de toda essa parte introdutória, entramos no universo Postgresql. Write-Ahead Logging nada mais é do que um sinônimo para o sistema de recuperação adiada (REDO). Dessa forma, através dos conceitos observados nas seções anteriores, já conhecemos as principais implicações dessa estratégia. Obviamente, o Postgresql tem algumas particularidades em sua implementação.
WAL torna possível técnicas de backup mais flexíveis, indo além de um simples dump da base de dados ou uma cópia física das estruturas de diretório. Podemos pensar em uma situação onde um crash aconteceu em nosso servidor de banco numa sexta-feira, porém o último backup físico que temos é o da segunda. E Agora? Basta que tenhamos o log WAL desse período para que o Postgresql refaça todas as operações efetivadas entre o último backup e o crash, levando o arquivo de dados para o último estado consistente antes da falha ocorrer. Além disso, temos a possibilidade de realizar backups on-line através do arquivamento do log e restaurar o estado da base para qualquer ponto no tempo. Esse recurso é conhecido como Point-In-Time Recovery (PITR) e tem material suficiente para um artigo dedicado só a ele.
Lembram-se quando falamos anteriormente que é de fundamental importância colocar o arquivo de log em um disco separado? No Postgresql, estes arquivos ficam no diretório pg_xlog, dentro do cluster gerado pelo comando initdb. Portanto, devemos movê-lo e criar um link simbólico no local original.
O Postgresql salva a posição do último checkpoint executado no arquivo pg_control. Em caso de falha, o SGBD faz a leitura desse arquivo e depois reprocessa as transações comitadas contidas nos arquivos de log. Portanto, cuide muito bem desse arquivo, pois caso ele fique corrompido não será possível refazer as operações. Contudo, poderíamos implementar o algoritmo da recuperação adiada, processando o arquivo de log de trás para frente e resolveríamos o problema. Que tal? Essa é uma excelente oportunidade para deixar a sua contribuição para a comunidade de software livre. Inclusive, isso já está na lista de coisas a fazer (http://www.postgresql.org/docs/faqs. TODO.html).
Parâmetros de Configuração
O Postgresql é um SGBD altamente parametrizado e com o WAL não é diferente. Podemos fazer diversos ajustes relevantes para adequar o SGBD a nossas necessidades. Cada ajuste pode ser feito diretamente no arquivo postgresql.conf (carregado no momento da inicialização do serviço) ou através do comando SQL: SET <nome do parâmetro> <valor> (altera o valor do parâmetro para a sessão atual).
O primeiro ponto a salientar é que o WAL é automaticamente habilitado, porém podemos configurar, dentre outras coisas, em que momento o processo server executará o checkpoint. Na Listagem 1 encontramos o trecho do arquivo postgresql.conf responsável pela configuração dos parâmetros WAL.
Listagem 1. postgresql.conf.
#——————————————————————————
# WRITE AHEAD LOG
#——————————————————————————
# – Settings -
#fsync = on # turns forced synchronization on or off
#synchronous_commit = on # immediate fsync at commit
#wal_sync_method = fsync # the default is the first option
# supported by the operating system:
# open_datasync
# fdatasync
# fsync
# fsync_writethrough
# open_sync
#full_page_writes = on # recover from partial page writes
#wal_buffers = 64kB # min 32kB
# (change requires restart)
#wal_writer_delay = 200ms # 1-10000 milliseconds
#commit_delay = 0 # range 0-100000, in microseconds
#commit_siblings = 5 # range 1-1000
# – Checkpoints -
#checkpoint_segments = 3 # in logfile segments, min 1, 16MB each
#checkpoint_timeout = 5min # range 30s-1h
#checkpoint_completion_target = 0.5 # checkpoint target duration, 0.0 – 1.0
#checkpoint_warning = 30s # 0 is off
# – Archiving -
#archive_mode = off # allows archiving to be done
# (change requires restart)
#archive_command = ” # command to use to archive a logfile segment
#archive_timeout = 0 # force a logfile segment switch after this
# time; 0 is off
Para o objetivo do artigo, nos concentraremos nos parâmetros de configuração do checkpoint: checkpoint_segments, checkpoint_timeout, checkpoint_completion_target e checkpoint_warning.
Na configuração padrão, o servidor executa um checkpoint a cada 3 segmentos do arquivo de log (3 * 16MB) ou a cada cinco minutos. Também podemos executar o checkpoint a qualquer momento através da instrução SQL CHECKPOINT.
Na configuração dos parâmetros temos que ter em mente duas características que devemos balancear: tempo de recuperação após um crash e desempenho do servidor quando em execução. Infelizmente, essas características são inversamente proporcionais.
Diminuindo os valores dos parâmetros checkpoint_segments e checkpoint_timeout, fazemos com que os checkpoints sejam executados com maior freqüência, portanto reduzindo o tempo de recuperação após uma falha. Porém, por ser um processo muito custoso, a execução de tantos checkpoints consecutivos degrada a performance do SGBD. Caso dois checkpoints sejam executados em um intervalo menor do que checkpoint_warning, uma mensagem é adicionada ao log do banco informando para aumentar o valor de checkpoint_segments.
Por outro lado, adiando a execução de checkpoints estaremos aumentando a retenção de memória e, em caso de falha, o tempo de recuperação. Contudo, o desempenho do SGBD será otimizado, pois a quantidade de operações de I/O em disco será reduzida. Se você tem um hardware altamente robusto e confiável essa é a opção ideal. Nesse sentido, a vantagem obtida com o ganho de desempenho compensa o custo no aumento do tempo de recuperação em virtude de um crash esporádico.
Com o objetivo de reduzir o I/O durante o processo de checkpoint, o Postgresql calcula a taxa de transferência necessária para que o processo termine em um tempo determinado. Na configuração padrão, o SGBD espera acabar cada checkpoint aproximadamente na metade do tempo antes que outro inicie (checkpoint_completion_target = 0.5). Desse modo, checkpoint_completion_target é uma fração de checkpoint_segments ou checkpoint_timeout, o que ocorrer primeiro. Para servidores que já estão trabalhando no limite de I/O é aconselhável aumentar o valor desse parâmetro, pois assim se reduz taxa de transferência do processo de chekpoint e, com isso, libera recursos para as transações operacionais.
Testando as Configurações
Nem sempre temos uma base com um volume de dados suficientemente grande para testarmos os efeitos dos nossos ajustes. Para suprir essa necessidade, o Postgresql vem com o utilitário pgbench.
Em primeiro lugar, vamos criar um database denominado dbtests, conforme Listagem 2.
Listagem 2. Criando database.
> createdb dbtests
Não é obrigatório criar um database só para os testes que iremos fazer, porém é uma boa prática não misturar dados de testes com dados reais de nossas aplicações.
Agora podemos executar o comando que cria as tabelas com os dados necessários para os testes (Listagem 3).
Listagem 3. Criação de tabelas e dados.
>pgbench –U postgres –h localhost –i dbtests
O comando da Listagem 3 cria as tabelas: accounts (100.000 linhas), branches (1 linha), history (nenhuma linha) e tellers (10 linhas). Caso se queira fazer testes com uma massa de dados maior, podemos adicionar o parâmetro –s <fator escalar> (multiplica o número de linhas geradas pelo fator escalar). Por exemplo, a Listagem 4 faz com que a tabela accounts seja criada com 1.000.000 de linhas.
Listagem 4. Usando fator escalar.
>pgbench –U postgres –h localhost –s 10 –i dbtests
E, finalmente, na Listagem 5 executamos o nosso benchmark. O nosso teste simula 10 clientes, cada um executando 10 transações, onde cada transação executa os comandos da Listagem 6. Observamos na Listagem 7 o resultado do bechmark executado em um servidor hipotético. O atributo tps nos informa quantas transações por segundo o banco conseguiu atender.
O utilitário pgbench é muito flexível. Além de outros parâmetros de linha de comando, temos a possibilidade de criar os nossos próprios scripts de teste.
Dessa forma, é interessante executarmos pelos menos um benchmark para cada configuração que alterarmos no postgresql.conf. Podendo ainda, disparar o pgbench de vários hosts. O tps, juntamente com outras métricas obtidas através de outras ferramentas (top, iostat, etc) , nos fornece informações extremamente relevantes para uma utilização eficiente dos recursos oferecidos pelo Postgresql.
Listagem 5. Executando benchmark.
>pgbench –U postgres –h localhost –c 10 –t 10 dbtests
Listagem 6. Operações executadas por cada transação.
BEGIN;
UPDATE accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM accounts WHERE aid = :aid;
UPDATE tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
UPDATE branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;
Listagem 7. Resultado do benchmark.
transaction type: TPC-B (sort of)
scaling factor: 10
number of clients: 10
number of transactions per client: 1000
number of transactions actually processed: 10000/10000
tps = 63.123853 (including connections establishing)
tps = 65.235782 (excluding connections establishing)
Conclusões
Nesse artigo tratamos dos conceitos fundamentais dos sistemas de recuperação de falha baseados em log e qual a abordagem adotada pelo Postgresql para garantir a durabilidade das transações. Como DBAs, é de fundamental importância entender o modo como se processa a recuperação de transações no nosso SGBD, pois podemos otimizar parâmetros de acordo com situações encontradas em ambientes reais de produção.
Notas:
Durabilidade: A Durabilidade é uma das quatro características de uma transação. Essa propriedade garante que os efeitos de uma transação comitada sejam permanentes, inclusive após uma falha. Representa a quarta letra da sigla ACID (Atomicidade, Consistência, Isolamento e Durabilidade).

Fevereiro 21, 2009 às 6:07 pm |
Muito bom artigo, parabéns, bem explicado e com um exemplo prático usando o pgbench.
Março 25, 2009 às 11:57 pm |
Gostei do artigo,muito bem explicado.Deus pra tirar muitas dúvidas.
Março 26, 2009 às 12:01 am |
Muito obrigado pelo feedback!
Maio 27, 2009 às 7:57 pm |
Parabéns, seu artigo está muito bom.