Determinar quando um banco de dados PostgreSQL foi alterado pela última vez

10

Estou olhando para alterar como os backups são feitos e estou imaginando se existe uma maneira de determinar quais bancos de dados em um cluster postgreql não foram alterados recentemente?

Em vez de usar o pg_dumpall, eu gostaria de usar o pg_dump e despejar apenas os bancos de dados que foram alterados desde o último backup (alguns bancos de dados não são atualizados com muita frequência) - a ideia é que, se nada mudou, o backup atual deve ainda seja bom.

Alguém sabe como determinar quando um banco de dados específico foi atualizado / alterado pela última vez?

Obrigado...

Atualizar:

Eu esperava não ter que escrever gatilhos em todo o lugar, pois não tenho controle sobre a criação de bancos de dados em um cluster específico (sem falar na criação de objetos db em um banco de dados).

Indo além, parece que há uma correlação entre o conteúdo do arquivo $ PGDATA / global / pg_database (especificamente o segundo campo) e os nomes de diretório em $ PGDATA / base.

Saindo do princípio, acho que o segundo campo do arquivo pg_database é o oid do banco de dados e que cada banco de dados tem seu próprio subdiretório em $ PGDATA / base (com o oid para o nome do subdiretório). Isso está correto? Em caso afirmativo, é razoável usar os registros de data e hora dos arquivos em $ PGDATA / base / * como o gatilho para a necessidade de um backup?

...Ou há uma maneira melhor?

Obrigado novamente...

gsiems
fonte
Nunca assuma que o backup atual é bom. Você sempre deseja fazer novos backups regularmente.
precisa saber é o seguinte
Sonu Singh - Não consigo controlar a adição de bancos de dados, muito menos tabelas neste cluster para que os gatilhos não funcionem-- mais (que eu saiba) os gatilhos não capturam alterações de ddl. Mrdenny ♦ - Correto. No entanto, eu gostaria de evitar a geração de backups incrementais redundantes entre os backups completos periódicos.

Respostas:

9

Embora o uso select datname, xact_commit from pg_stat_database;sugerido por @Jack Douglas não funcione (aparentemente devido ao vácuo automático), select datname, tup_inserted, tup_updated, tup_deleted from pg_stat_databaseele parece funcionar. As alterações no DML e no DDL alteram os valores das colunas tup_ * enquanto a vacuumnão ( vacuum analyzepor outro lado ...).

Na hipótese de isso ser útil para outras pessoas, estou incluindo o script de backup que coloquei em prática. Isso funciona na página 8.4.x, mas não na 8.2.x-- YMMV, dependendo da versão da página usada.

#!/usr/bin/env perl
=head1 Synopsis

pg_backup -- selectively backup a postgresql database cluster

=head1 Description

Perform backups (pg_dump*) of postgresql databases in a cluster on an
as needed basis.

For some database clusters, there may be databases that are:

 a. rarely updated/changed and therefore shouldn't require dumping as 
    often as those databases that are frequently changed/updated.

 b. are large enough that dumping them without need is undesirable.

The global data is always dumped without regard to whether any 
individual databses need backing up or not.

=head1 Usage

pg_backup [OPTION]...

General options:

  -F, --format=c|t|p    output file format for data dumps 
                          (custom, tar, plain text) (default is custom)
  -a, --all             backup (pg_dump) all databases in the cluster 
                          (default is to only pg_dump databases that have
                          changed since the last backup)
  --backup-dir          directory to place backup files in 
                          (default is ./backups)
  -v, --verbose         verbose mode
  --help                show this help, then exit

Connection options:

  -h, --host=HOSTNAME   database server host or socket directory
  -p, --port=PORT       database server port number
  -U, --username=NAME   connect as specified database user
  -d, --database=NAME   connect to database name for global data

=head1 Notes

This utility has been developed against PostgreSQL version 8.4.x. Older 
versions of PostgreSQL may not work.

`vacuum` does not appear to trigger a backup unless there is actually 
something to vacuum whereas `vacuum analyze` appears to always trigger a 
backup.

=head1 Copyright and License

Copyright (C) 2011 by Gregory Siems

This library is free software; you can redistribute it and/or modify it 
under the same terms as PostgreSQL itself, either PostgreSQL version 
8.4 or, at your option, any later version of PostgreSQL you may have 
available.

=cut

use strict;
use warnings;
use Getopt::Long;
use Data::Dumper;
use POSIX qw(strftime);

my %opts = get_options();

my $connect_options = '';
$connect_options .= "--$_=$opts{$_} " for (qw(username host port));

my $shared_dump_args = ($opts{verbose})
    ? $connect_options . ' --verbose '
    : $connect_options;

my $backup_prefix = (exists $opts{host} && $opts{host} ne 'localhost')
    ? $opts{backup_dir} . '/' . $opts{host} . '-'
    : $opts{backup_dir} . '/';

do_main();


########################################################################
sub do_main {
    backup_globals();

    my $last_stats_file = $backup_prefix . 'last_stats';

    # get the previous pg_stat_database data
    my %last_stats;
    if ( -f $last_stats_file) {
        %last_stats = parse_stats (split "\n", slurp_file ($last_stats_file));
    }

    # get the current pg_stat_database data
    my $cmd = 'psql ' . $connect_options;
    $cmd .= " $opts{database} " if (exists $opts{database});
    $cmd .= "-Atc \"
        select date_trunc('minute', now()), datid, datname, 
            xact_commit, tup_inserted, tup_updated, tup_deleted 
        from pg_stat_database 
        where datname not in ('template0','template1','postgres'); \"";
    $cmd =~ s/\ns+/ /g;
    my @stats = `$cmd`;
    my %curr_stats = parse_stats (@stats);

    # do a backup if needed
    foreach my $datname (sort keys %curr_stats) {
        my $needs_backup = 0;
        if ($opts{all}) {
            $needs_backup = 1;
        }
        elsif ( ! exists $last_stats{$datname} ) {
            $needs_backup = 1;
            warn "no last stats for $datname\n" if ($opts{debug});
        }
        else {
            for (qw (tup_inserted tup_updated tup_deleted)) {
                if ($last_stats{$datname}{$_} != $curr_stats{$datname}{$_}) {
                    $needs_backup = 1;
                    warn "$_ stats do not match for $datname\n" if ($opts{debug});
                }
            }
        }
        if ($needs_backup) {
            backup_db ($datname);
        }
        else {
            chitchat ("Database \"$datname\" does not currently require backing up.");
        }
    }

    # update the pg_stat_database data
    open my $fh, '>', $last_stats_file || die "Could not open $last_stats_file for output. !$\n";
    print $fh @stats;
    close $fh;
}

sub parse_stats {
    my @in = @_;
    my %stats;
    chomp @in;
    foreach my $line (@in) {
        my @ary = split /\|/, $line;
        my $datname = $ary[2];
        next unless ($datname);
        foreach my $key (qw(tmsp datid datname xact_commit tup_inserted tup_updated tup_deleted)) {
            my $val = shift @ary;
            $stats{$datname}{$key} = $val;
        }
    }
    return %stats;
}

sub backup_globals {
    chitchat ("Backing up the global data.");

    my $backup_file = $backup_prefix . 'globals-only.backup.gz';
    my $cmd = 'pg_dumpall --globals-only ' . $shared_dump_args;
    $cmd .= " --database=$opts{database} " if (exists $opts{database});

    do_dump ($backup_file, "$cmd | gzip");
}

sub backup_db {
    my $database = shift;
    chitchat ("Backing up database \"$database\".");

    my $backup_file = $backup_prefix . $database . '-schema-only.backup.gz';
    do_dump ($backup_file, "pg_dump --schema-only --create --format=plain $shared_dump_args $database | gzip");

    $backup_file = $backup_prefix . $database . '.backup';
    do_dump ($backup_file, "pg_dump --format=". $opts{format} . " $shared_dump_args $database");
}

sub do_dump {
    my ($backup_file, $cmd) = @_;

    my $temp_file = $backup_file . '.new';
    warn "Command is: $cmd > $temp_file" if ($opts{debug});

    chitchat (`$cmd > $temp_file`);
    if ( -f $temp_file ) {
        chitchat (`mv $temp_file $backup_file`);
    }
}

sub chitchat {
    my @ary = @_;
    return unless (@ary);
    chomp @ary;
    my $first   = shift @ary;
    my $now     = strftime "%Y%m%d-%H:%M:%S", localtime;
    print +(join "\n                  ", "$now $first", @ary), "\n";
}

sub get_options {
    Getopt::Long::Configure('bundling');

    my %opts = ();
    GetOptions(
        "a"             => \$opts{all},
        "all"           => \$opts{all},
        "p=s"           => \$opts{port},
        "port=s"        => \$opts{port},
        "U=s"           => \$opts{username},
        "username=s"    => \$opts{username},
        "h=s"           => \$opts{host},
        "host=s"        => \$opts{host},
        "F=s"           => \$opts{format},
        "format=s"      => \$opts{format},
        "d=s"           => \$opts{database},
        "database=s"    => \$opts{database},
        "backup-dir=s"  => \$opts{backup_dir},
        "help"          => \$opts{help},
        "v"             => \$opts{verbose},
        "verbose"       => \$opts{verbose},
        "debug"         => \$opts{debug},
        );

    # Does the user need help?
    if ($opts{help}) {
        show_help();
    }

    $opts{host}         ||= $ENV{PGHOSTADDR} || $ENV{PGHOST}     || 'localhost';
    $opts{port}         ||= $ENV{PGPORT}     || '5432';
    $opts{host}         ||= $ENV{PGHOST}     || 'localhost';
    $opts{username}     ||= $ENV{PGUSER}     || $ENV{USER}       || 'postgres';
    $opts{database}     ||= $ENV{PGDATABASE} || $opts{username};
    $opts{backup_dir}   ||= './backups';

    my %formats = (
        c       => 'custom',
        custom  => 'custom',
        t       => 'tar',
        tar     => 'tar',
        p       => 'plain',
        plain   => 'plain',
    );
    $opts{format} = (defined $opts{format})
        ? $formats{$opts{format}} || 'custom'
        : 'custom';

    warn Dumper \%opts if ($opts{debug});
    return %opts;
}

sub show_help {
    print `perldoc -F $0`;
    exit;
}

sub slurp_file { local (*ARGV, $/); @ARGV = shift; <> }

__END__

Atualização: o script foi colocado no github aqui .

gsiems
fonte
Muito bom código, obrigado por compartilhar. BTW, poderia ser um github'ed, você não acha? :-)
poige
2

Parece que você pode usar pg_stat_databasepara obter uma contagem de transações e verificar se isso muda de uma execução de backup para a seguinte:

select datname, xact_commit from pg_stat_database;

  datname  | xact_commit 
-----------+-------------
 template1 |           0
 template0 |           0
 postgres  |      136785

Se alguém ligou, pg_stat_resetvocê não pode ter certeza se um banco de dados mudou ou não, mas você pode considerar improvável o suficiente que isso aconteça, seguido pelo número exato de transações para corresponder à sua última leitura.

--EDITAR

consulte esta pergunta para saber por que isso pode não funcionar. Não sei por que isso pode acontecer, mas ativar o log pode lançar alguma luz ....

Jack diz que tenta topanswers.xyz
fonte
Se alguém ligou pg_stat_reset, a probabilidade do valor xact_commit correspondente ao anterior seria bastante baixa, não? Portanto, isso certamente captura a existência de alterações no DML. Agora tudo o que preciso é capturar se houve alterações no DDL.
gsiems
O DDL é transacional no postgres - eu esperaria que a contagem de confirmação aumentasse nesse caso também. Não verificado embora ...
Jack diz que tente topanswers.xyz 27/08
O senhor está correto. Eu tinha esquecido que a DDL da página é transacional e um create table ...teste rápido parece aumentar o xact_commit.
gsiems
11
Testes adicionais mostram o xact_commit aumentando, mesmo que não haja atividade do usuário - autovacuum, talvez?
gsiems
Definitivamente, isso não funciona para fins de backup. O xact_commit aumenta com muita frequência, mesmo quando ninguém está conectado ao banco de dados.
mivk
1

Ao pesquisar os documentos e grupos de notícias do postgres:

txid_current()fornecerá um novo xid- se você chamar a função novamente em uma data posterior, se você xidaumentar um, você sabe que nenhuma transação foi confirmada entre as duas chamadas. Você pode obter falsos positivos - por exemplo, se alguém ligartxid_current()

Jack diz que tenta topanswers.xyz
fonte
Obrigado pela sugestão. Eu não acredito que isso funcionará, no entanto, como txid_current () parece operar no nível do cluster e não no nível do banco de dados.
gsiems
Procurei um documento sobre isso e não consegui encontrar - você tem um link?
Jack diz que tente topanswers.xyz 27/08
11
Nenhum link. Testei alternando entre bancos de dados e executando "select current_database (), txid_current ();" e comparando os resultados.
gsiems
0

Lembre-se do carimbo de data / hora nos arquivos que contêm os dados do banco de dados e verifique se eles foram alterados. Se eles fizeram houve uma gravação.

Editar após a dica do WAL: você deve fazer isso somente após liberar as gravações pendentes.

Nils
fonte
2
Isso não é confiável. Pode haver alterações que ainda não foram gravadas (liberadas) nos arquivos de dados, ou seja, elas foram gravadas apenas no WAL.
precisa saber é o seguinte