Déterminer quand une base de données PostgreSQL a été modifiée pour la dernière fois

10

Je cherche à modifier la façon dont les sauvegardes sont effectuées et je me demande s'il existe un moyen de déterminer quelles bases de données dans un cluster postgreql n'ont pas été récemment modifiées?

Au lieu d'utiliser pg_dumpall, j'aimerais utiliser pg_dump et ne vider que les bases de données qui ont changé depuis la dernière sauvegarde (certaines bases de données ne sont pas mises à jour très souvent) - l'idée étant que si rien n'a changé, la sauvegarde actuelle devrait soyez toujours bon.

Quelqu'un connaît-il un moyen de déterminer quand une base de données spécifique a été mise à jour / modifiée pour la dernière fois?

Merci...

Mettre à jour:

J'espérais ne pas avoir à écrire des déclencheurs partout car je n'ai aucun contrôle sur la création de bases de données dans un cluster particulier (sans parler de la création d'objets DB dans une base de données).

En creusant davantage, il semble qu'il existe une corrélation entre le contenu du fichier $ PGDATA / global / pg_database (en particulier le deuxième champ) et les noms de répertoire sous $ PGDATA / base.

Sortir sur une branche, je suppose que le deuxième champ du fichier pg_database est l'oid de la base de données et que chaque base de données a son propre sous-répertoire sous $ PGDATA / base (avec l'oid pour le nom du sous-répertoire). Est-ce exact? Dans l'affirmative, est-il raisonnable d'utiliser les horodatages des fichiers à partir des fichiers sous $ PGDATA / base / * comme déclencheur pour avoir besoin d'une sauvegarde?

... ou existe-t-il un meilleur moyen?

Merci encore...

gsiems
la source
Ne présumez jamais que la sauvegarde actuelle est bonne. Vous souhaitez toujours effectuer de nouvelles sauvegardes selon votre horaire régulier.
mrdenny
Sonu Singh - Je ne peux pas contrôler l'ajout de bases de données, sans parler des tables de ce cluster afin que les déclencheurs ne fonctionnent pas - plus (à ma connaissance) les déclencheurs n'attraperont pas les modifications ddl. mrdenny ♦ - Correct. Cependant, je voudrais éviter de générer des sauvegardes incrémentielles redondantes entre les sauvegardes complètes périodiques.

Réponses:

9

Bien que l'utilisation select datname, xact_commit from pg_stat_database;suggérée par @Jack Douglas ne fonctionne pas tout à fait (apparemment en raison de l'auto-vide), select datname, tup_inserted, tup_updated, tup_deleted from pg_stat_databasesemble fonctionner. Les changements DML et DDL changeront les valeurs des colonnes tup_ * tandis que a vacuumne le fait pas ( vacuum analyzeen revanche ...).

Dans le cas où cela pourrait être utile pour d'autres, j'inclus le script de sauvegarde que j'ai mis en place. Cela fonctionne pour Pg 8.4.x mais pas pour 8.2.x-- YMMV selon la version de Pg utilisée.

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

Mise à jour: le script a été mis sur github ici .

gsiems
la source
Code assez sympa, merci pour le partage. BTW, ça pourrait être github'ed, tu ne crois pas? :-)
poige
2

Il semble que vous puissiez utiliser pg_stat_databasepour obtenir un nombre de transactions et vérifier si cela change d'une exécution de sauvegarde à la suivante:

select datname, xact_commit from pg_stat_database;

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

Si quelqu'un vous a appelé, pg_stat_resetvous ne pouvez pas être certain si une base de données a changé ou non, mais vous pouvez considérer qu'il est assez improbable que cela se produise, suivi du nombre exact de transactions correspondant à votre dernière lecture.

--ÉDITER

voir cette question SO pour savoir pourquoi cela pourrait ne pas fonctionner. Je ne sais pas pourquoi cela peut se produire, mais l'activation de la journalisation peut faire la lumière ...

Jack dit d'essayer topanswers.xyz
la source
Si quelqu'un a appelé, pg_stat_resetalors la probabilité que la valeur xact_commit corresponde à la précédente devrait être assez faible, non? Donc, cela semble certainement attraper l'existence de changements DML. Maintenant, tout ce dont j'ai besoin est d'attraper s'il y a eu des changements DDL.
gsiems
DDL est transactionnel en postgres - je m'attends à ce que le nombre de validations augmente dans ce cas également. Pas vérifié cependant ...
Jack dit d'essayer topanswers.xyz
Vous monsieur, avez raison. J'avais oublié que Pg DDL était transactionnel et un create table ...test rapide semble incrémenter xact_commit.
gsiems
1
Des tests supplémentaires montrent que xact_commit augmente même s'il n'y a pas d'activité utilisateur en cours - autovacuum peut-être?
gsiems
Cela ne fonctionne certainement pas à des fins de sauvegarde. xact_commit augmente très fréquemment, même lorsque personne n'est connecté à la base de données.
mivk
1

De creuser autour des documents et des groupes de discussion postgres:

txid_current()vous donnera une nouvelle xid- si vous appelez à nouveau la fonction à une date ultérieure, si vous obtenez une xidplus élevée, vous savez qu'aucune transaction n'a été engagée entre les deux appels. Vous pouvez cependant obtenir des faux positifs - par exemple si quelqu'un d'autre appelletxid_current()

Jack dit d'essayer topanswers.xyz
la source
Merci pour la suggestion. Je ne crois pas que cela fonctionnera cependant car txid_current () semble fonctionner au niveau du cluster plutôt qu'au niveau de la base de données.
gsiems
J'ai cherché un document à ce sujet et je n'ai pas pu trouver - avez-vous un lien?
Jack dit d'essayer topanswers.xyz
1
Pas de lien. J'ai testé en basculant entre les bases de données et en exécutant "select current_database (), txid_current ();" et comparer les résultats.
gsiems
0

Rappelez-vous l'horodatage de vos fichiers contenant les données DB et regardez s'ils ont changé. S'ils le faisaient, il y avait une écriture.

Modifier après l'indice WAL: vous ne devez effectuer cette opération qu'après avoir vidé les écritures en attente.

Nils
la source
2
Ce n'est pas fiable. Il peut y avoir des modifications qui ne sont pas encore écrites (vidées) dans les fichiers de données, c'est-à-dire qu'elles n'ont été écrites que dans le WAL.
a_horse_with_no_name