Determinar cuándo se modificó por última vez una base de datos PostgreSQL

10

Estoy buscando modificar cómo se realizan las copias de seguridad y me pregunto si hay una manera de determinar qué bases de datos en un clúster postgreql no se han cambiado recientemente.

En lugar de usar pg_dumpall, me gustaría usar pg_dump y solo volcar las bases de datos que han cambiado desde la última copia de seguridad (algunas bases de datos no se actualizan muy a menudo), la idea es que si nada ha cambiado, la copia de seguridad actual debería aun así se bueno.

¿Alguien sabe de una manera de determinar cuándo se actualizó / cambió por última vez una base de datos específica?

Gracias...

Actualizar:

Esperaba no tener que escribir disparadores en todo el lugar, ya que no tengo control sobre la creación de bases de datos en un clúster en particular (y mucho menos la creación de objetos db dentro de una base de datos).

Excavando más, parece que hay una correlación entre el contenido del archivo $ PGDATA / global / pg_database (específicamente el segundo campo) y los nombres de directorio bajo $ PGDATA / base.

Saliendo a la luz, supongo que el segundo campo del archivo pg_database es el oid de la base de datos y que cada base de datos tiene su propio subdirectorio bajo $ PGDATA / base (con el oid para el nombre del subdirectorio). ¿Es eso correcto? Si es así, ¿es razonable usar las marcas de tiempo del archivo de $ PGDATA / base / * como desencadenante para necesitar una copia de seguridad?

...¿O hay un mejor camino?

Gracias de nuevo...

gsiems
fuente
Nunca suponga que la copia de seguridad actual es buena. Siempre desea realizar nuevas copias de seguridad en su horario habitual.
mrdenny
Sonu Singh: no puedo controlar la adición de bases de datos, y mucho menos las tablas en este clúster para que los desencadenantes no funcionen, además (que yo sepa) los desencadenantes no detectarán los cambios de ddl. mrdenny ♦ - Correcto. Sin embargo, me gustaría evitar generar copias de seguridad incrementales redundantes entre las copias de seguridad completas periódicas.

Respuestas:

9

Si bien usar select datname, xact_commit from pg_stat_database;según lo sugerido por @Jack Douglas no funciona del todo (aparentemente debido al vacío automático), select datname, tup_inserted, tup_updated, tup_deleted from pg_stat_databaseparece funcionar. Tanto los cambios DML como DDL cambiarán los valores de las columnas tup_ * mientras que a vacuumno lo hace ( vacuum analyzepor otro lado ...).

En caso de que esto pueda ser útil para otros, incluyo el script de respaldo que he implementado. Esto funciona para Pg 8.4.x pero no para 8.2.x-- YMMV dependiendo de la versión de Pg utilizada.

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

Actualización: el script se ha puesto en github aquí .

gsiems
fuente
Muy buen código, gracias por compartir. Por cierto, podría ser github'ed, ¿no te parece? :-)
poige
2

Parece que puede usar pg_stat_databasepara obtener un recuento de transacciones y verificar si esto cambia de una ejecución de copia de seguridad a la siguiente:

select datname, xact_commit from pg_stat_database;

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

Si alguien lo ha llamado, pg_stat_resetno puede estar seguro de si una base de datos ha cambiado o no, pero puede considerar que es poco probable que eso suceda, seguido de exactamente el número correcto de transacciones para que coincida con su última lectura.

--EDITAR

vea esta pregunta SO por qué esto podría no funcionar No estoy seguro de por qué podría suceder esto, pero habilitar el registro podría arrojar algo de luz ...

Jack dice que intente topanswers.xyz
fuente
Si alguien llamó, pg_stat_resetentonces la probabilidad de que el valor de xact_commit coincida con el anterior sería bastante baja, ¿no? Así que ciertamente parece captar la existencia de cambios DML. Ahora todo lo que necesito es atrapar si ha habido cambios DDL.
gsiems
DDL es transaccional en postgres: esperaría que el recuento de confirmaciones también aumente en ese caso. Sin embargo, no está marcado ...
Jack dice que intente topanswers.xyz
Usted señor, tiene razón. Me había olvidado de que Pg DDL era transaccional y una create table ...prueba rápida parece aumentar xact_commit.
gsiems
1
Las pruebas adicionales muestran que el xact_commit aumenta a pesar de que no hay actividad del usuario en curso, ¿tal vez vacío automático?
gsiems
Esto definitivamente no funciona con fines de copia de seguridad. xact_commit aumenta con mucha frecuencia, incluso cuando nadie está conectado a la base de datos.
mivk
1

De cavar alrededor de los documentos de postgres y grupos de noticias:

txid_current()le dará una nueva xid: si vuelve a llamar a la función en una fecha posterior, si obtiene una xidmás alta, sabrá que no hay transacciones confirmadas entre las dos llamadas. Sin embargo, puede obtener falsos positivos, por ejemplo, si alguien llamatxid_current()

Jack dice que intente topanswers.xyz
fuente
Gracias por la sugerencia. Sin embargo, no creo que esto funcione, ya que txid_current () parece funcionar a nivel de clúster en lugar de a nivel de base de datos.
gsiems
Busqué un documento sobre eso y no pude encontrarlo, ¿tiene un enlace?
Jack dice que intente topanswers.xyz
1
No hay enlace. Probé cambiando entre bases de datos y ejecutando "select current_database (), txid_current ();" y comparando los resultados.
gsiems
0

Recuerde la marca de tiempo en sus archivos que contienen los datos de la base de datos y observe si han cambiado. Si lo hicieron, hubo una escritura.

Editar después de la sugerencia de WAL: debe hacer esto solo después de vaciar las escrituras pendientes.

Nils
fuente
2
Eso no es confiable. Podría haber cambios que aún no se han escrito (vaciado) en los archivos de datos, es decir, solo se escribieron en el WAL.
a_horse_with_no_name