Determina quando un database PostgreSQL è stato modificato l'ultima volta


10

Sto cercando di modificare il modo in cui vengono eseguiti i backup e mi chiedo se esiste un modo per determinare quali database in un cluster postgreql non sono stati modificati di recente?

Invece di usare pg_dumpall, mi piacerebbe usare pg_dump e scaricare solo quei database che sono cambiati dall'ultimo backup (alcuni database non vengono aggiornati molto spesso) - l'idea è che se nulla è cambiato, il backup corrente dovrebbe essere ancora buono.

Qualcuno conosce un modo per determinare quando un database specifico è stato aggiornato / modificato l'ultima volta?

Grazie...

Aggiornare:

Speravo di non dover scrivere trigger dappertutto perché non ho alcun controllo sulla creazione di database in un particolare cluster (per non parlare della creazione di oggetti db all'interno di un database).

Scavando ulteriormente, sembra che ci sia una correlazione tra i contenuti del file $ PGDATA / global / pg_database (in particolare il secondo campo) e i nomi delle directory in $ PGDATA / base.

Uscendo da un arto, immagino che il secondo campo del file pg_database sia l'oid del database e che ogni database abbia la propria sottodirectory in $ PGDATA / base (con l'oid per il nome della sottodirectory). È corretto? In tal caso, è ragionevole utilizzare i timestamp dei file dai file in $ PGDATA / base / * come trigger per la necessità di un backup?

... o c'è un modo migliore?

Grazie ancora...



Non dare mai per scontato che il backup corrente sia buono. Vuoi sempre fare nuovi backup secondo il tuo programma normale.
mrdenny,

Sonu Singh - Non riesco a controllare l'aggiunta di database, figuriamoci le tabelle in questo cluster in modo che i trigger non funzionino - inoltre (per quanto ne so) i trigger non rileveranno le modifiche ddl. mrdenny ♦ - Corretto. Tuttavia, vorrei evitare di generare backup incrementali ridondanti tra i backup completi periodici.

Risposte:


9

Mentre l'utilizzo select datname, xact_commit from pg_stat_database;come suggerito da @Jack Douglas non funziona abbastanza (apparentemente a causa del vuoto automatico), select datname, tup_inserted, tup_updated, tup_deleted from pg_stat_databasesembra funzionare. Entrambe le modifiche DML e DDL cambieranno i valori delle colonne tup_ * mentre a vacuumno ( vacuum analyzed'altra parte ...).

Nel caso in cui ciò possa essere utile per gli altri, sto includendo lo script di backup che ho creato. Funziona con Pg 8.4.x ma non con 8.2.x-- YMMV a seconda della versione di Pg utilizzata.

#!/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__

Aggiornamento: lo script è stato messo su github qui .


Un bel codice, grazie per averlo condiviso. A proposito, potrebbe essere github'ed, non credi? :-)
poige

2

Sembra che tu possa usare pg_stat_databaseper ottenere un conteggio delle transazioni e verificare se questo cambia da un'esecuzione di backup alla successiva:

select datname, xact_commit from pg_stat_database;

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

Se qualcuno ha chiamato pg_stat_resetnon puoi essere certo che un db sia cambiato o meno, ma potresti considerare abbastanza improbabile che ciò accada, seguito dal numero esatto di transazioni per abbinare la tua ultima lettura.

--MODIFICARE

vedi questa domanda SO per capire perché potrebbe non funzionare. Non sono sicuro del motivo per cui ciò potrebbe accadere, ma l'abilitazione della registrazione potrebbe far luce ...


Se qualcuno ha chiamato pg_stat_resetallora la probabilità che il valore di xact_commit corrisponda al precedente dovrebbe essere piuttosto bassa, no? In modo che certamente sembri cogliere l'esistenza di cambiamenti DML. Ora tutto ciò che serve è catturare se ci sono state modifiche DDL.
gsiems,

DDL è transazionale in postgres - mi aspetto che anche il conteggio degli commit aumenti in quel caso. Non controllato però ...
Jack dice di provare topanswers.xyz il

Signore, avete ragione. Avevo dimenticato che Pg DDL era transazionale e un rapido create table ...test sembra aumentare xact_commit.
gsiems,

1
Ulteriori test mostrano che xact_commit è in aumento anche se non c'è attività dell'utente in corso, forse autovacuum?
gsiems,

Questo sicuramente non funziona a scopo di backup. xact_commit aumenta molto frequentemente, anche quando nessuno è connesso al database.
Mivk,

1

Scavando nei documenti e nei newsgroup di Postgres:

txid_current()ti darà un nuovo xid- se richiami di nuovo la funzione in un secondo momento, se ne ottieni xiduno superiore, sai che nessuna transazione è stata impegnata tra le due chiamate. Tuttavia, potresti ottenere falsi positivi, ad esempio se qualcun altro chiamatxid_current()


Grazie per il suggerimento Non credo che funzionerà comunque poiché txid_current () sembra funzionare a livello di cluster piuttosto che a livello di database.
gsiems,

Ho cercato un documento su questo e non sono riuscito a trovare - hai un link?
Jack dice di provare topanswers.xyz il

1
Nessun collegamento. Ho provato passando da un database all'altro ed eseguendo "select current_database (), txid_current ();" e confrontando i risultati.
gsiems,

0

Ricordare il timestamp sui file contenenti i dati DB e verificare se sono stati modificati. Se lo facessero, c'era una scrittura.

Modifica dopo il suggerimento WAL: dovresti farlo solo dopo aver scaricato le scritture in sospeso.


2
Non è affidabile. Potrebbero esserci delle modifiche che non sono ancora state scritte (scaricate) nei file di dati, vale a dire che sono state solo scritte nella WAL.
a_horse_with_no_name

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.