aboutsummaryrefslogtreecommitdiff
path: root/src/bin/pg_basebackup
diff options
context:
space:
mode:
authorRobert Haas <rhaas@postgresql.org>2020-04-03 14:59:47 -0400
committerRobert Haas <rhaas@postgresql.org>2020-04-03 15:05:59 -0400
commit0d8c9c1210c44b36ec2efcb223a1dfbe897a3661 (patch)
treeaf5225aa493720c40a8d6142f043dde444a131af /src/bin/pg_basebackup
parentce77abe63cfc85fb0bc236deb2cc34ae35cb5324 (diff)
downloadpostgresql-0d8c9c1210c44b36ec2efcb223a1dfbe897a3661.tar.gz
postgresql-0d8c9c1210c44b36ec2efcb223a1dfbe897a3661.zip
Generate backup manifests for base backups, and validate them.
A manifest is a JSON document which includes (1) the file name, size, last modification time, and an optional checksum for each file backed up, (2) timelines and LSNs for whatever WAL will need to be replayed to make the backup consistent, and (3) a checksum for the manifest itself. By default, we use CRC-32C when checksumming data files, because we are trying to detect corruption and user error, not foil an adversary. However, pg_basebackup and the server-side BASE_BACKUP command now have options to select a different algorithm, so users wanting a cryptographic hash function can select SHA-224, SHA-256, SHA-384, or SHA-512. Users not wanting file checksums at all can disable them, or disable generating of the backup manifest altogether. Using a cryptographic hash function in place of CRC-32C consumes significantly more CPU cycles, which may slow down backups in some cases. A new tool called pg_validatebackup can validate a backup against the manifest. If no checksums are present, it can still check that the right files exist and that they have the expected sizes. If checksums are present, it can also verify that each file has the expected checksum. Additionally, it calls pg_waldump to verify that the expected WAL files are present and parseable. Only plain format backups can be validated directly, but tar format backups can be validated after extracting them. Robert Haas, with help, ideas, review, and testing from David Steele, Stephen Frost, Andrew Dunstan, Rushabh Lathia, Suraj Kharage, Tushar Ahuja, Rajkumar Raghuwanshi, Mark Dilger, Davinder Singh, Jeevan Chalke, Amit Kapila, Andres Freund, and Noah Misch. Discussion: http://postgr.es/m/CA+TgmoZV8dw1H2bzZ9xkKwdrk8+XYa+DC9H=F7heO2zna5T6qg@mail.gmail.com
Diffstat (limited to 'src/bin/pg_basebackup')
-rw-r--r--src/bin/pg_basebackup/pg_basebackup.c208
-rw-r--r--src/bin/pg_basebackup/t/010_pg_basebackup.pl8
2 files changed, 209 insertions, 7 deletions
diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index c5d95958b29..de098b3558c 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -88,6 +88,12 @@ typedef struct UnpackTarState
FILE *file;
} UnpackTarState;
+typedef struct WriteManifestState
+{
+ char filename[MAXPGPATH];
+ FILE *file;
+} WriteManifestState;
+
typedef void (*WriteDataCallback) (size_t nbytes, char *buf,
void *callback_data);
@@ -136,6 +142,9 @@ static bool temp_replication_slot = true;
static bool create_slot = false;
static bool no_slot = false;
static bool verify_checksums = true;
+static bool manifest = true;
+static bool manifest_force_encode = false;
+static char *manifest_checksums = NULL;
static bool success = false;
static bool made_new_pgdata = false;
@@ -181,6 +190,12 @@ static void ReceiveTarCopyChunk(size_t r, char *copybuf, void *callback_data);
static void ReceiveAndUnpackTarFile(PGconn *conn, PGresult *res, int rownum);
static void ReceiveTarAndUnpackCopyChunk(size_t r, char *copybuf,
void *callback_data);
+static void ReceiveBackupManifest(PGconn *conn);
+static void ReceiveBackupManifestChunk(size_t r, char *copybuf,
+ void *callback_data);
+static void ReceiveBackupManifestInMemory(PGconn *conn, PQExpBuffer buf);
+static void ReceiveBackupManifestInMemoryChunk(size_t r, char *copybuf,
+ void *callback_data);
static void BaseBackup(void);
static bool reached_end_position(XLogRecPtr segendpos, uint32 timeline,
@@ -388,6 +403,11 @@ usage(void)
printf(_(" --no-verify-checksums\n"
" do not verify checksums\n"));
printf(_(" --no-estimate-size do not estimate backup size in server side\n"));
+ printf(_(" --no-manifest suppress generation of backup manifest\n"));
+ printf(_(" --manifest-force-encode\n"
+ " hex encode all filenames in manifest\n"));
+ printf(_(" --manifest-checksums=SHA{224,256,384,512}|CRC32C|NONE\n"
+ " use algorithm for manifest checksums\n"));
printf(_(" -?, --help show this help, then exit\n"));
printf(_("\nConnection options:\n"));
printf(_(" -d, --dbname=CONNSTR connection string\n"));
@@ -1186,6 +1206,31 @@ ReceiveTarFile(PGconn *conn, PGresult *res, int rownum)
}
}
+ /*
+ * Normally, we emit the backup manifest as a separate file, but when
+ * we're writing a tarfile to stdout, we don't have that option, so
+ * include it in the one tarfile we've got.
+ */
+ if (strcmp(basedir, "-") == 0)
+ {
+ char header[512];
+ PQExpBufferData buf;
+
+ initPQExpBuffer(&buf);
+ ReceiveBackupManifestInMemory(conn, &buf);
+ if (PQExpBufferDataBroken(buf))
+ {
+ pg_log_error("out of memory");
+ exit(1);
+ }
+ tarCreateHeader(header, "backup_manifest", NULL, buf.len,
+ pg_file_create_mode, 04000, 02000,
+ time(NULL));
+ writeTarData(&state, header, sizeof(header));
+ writeTarData(&state, buf.data, buf.len);
+ termPQExpBuffer(&buf);
+ }
+
/* 2 * 512 bytes empty data at end of file */
writeTarData(&state, zerobuf, sizeof(zerobuf));
@@ -1657,6 +1702,64 @@ ReceiveTarAndUnpackCopyChunk(size_t r, char *copybuf, void *callback_data)
} /* continuing data in existing file */
}
+/*
+ * Receive the backup manifest file and write it out to a file.
+ */
+static void
+ReceiveBackupManifest(PGconn *conn)
+{
+ WriteManifestState state;
+
+ snprintf(state.filename, sizeof(state.filename),
+ "%s/backup_manifest.tmp", basedir);
+ state.file = fopen(state.filename, "wb");
+ if (state.file == NULL)
+ {
+ pg_log_error("could not create file \"%s\": %m", state.filename);
+ exit(1);
+ }
+
+ ReceiveCopyData(conn, ReceiveBackupManifestChunk, &state);
+
+ fclose(state.file);
+}
+
+/*
+ * Receive one chunk of the backup manifest file and write it out to a file.
+ */
+static void
+ReceiveBackupManifestChunk(size_t r, char *copybuf, void *callback_data)
+{
+ WriteManifestState *state = callback_data;
+
+ if (fwrite(copybuf, r, 1, state->file) != 1)
+ {
+ pg_log_error("could not write to file \"%s\": %m", state->filename);
+ exit(1);
+ }
+}
+
+/*
+ * Receive the backup manifest file and write it out to a file.
+ */
+static void
+ReceiveBackupManifestInMemory(PGconn *conn, PQExpBuffer buf)
+{
+ ReceiveCopyData(conn, ReceiveBackupManifestInMemoryChunk, buf);
+}
+
+/*
+ * Receive one chunk of the backup manifest file and write it out to a file.
+ */
+static void
+ReceiveBackupManifestInMemoryChunk(size_t r, char *copybuf,
+ void *callback_data)
+{
+ PQExpBuffer buf = callback_data;
+
+ appendPQExpBuffer(buf, copybuf, r);
+}
+
static void
BaseBackup(void)
{
@@ -1667,6 +1770,8 @@ BaseBackup(void)
char *basebkp;
char escaped_label[MAXPGPATH];
char *maxrate_clause = NULL;
+ char *manifest_clause;
+ char *manifest_checksums_clause = "";
int i;
char xlogstart[64];
char xlogend[64];
@@ -1674,6 +1779,7 @@ BaseBackup(void)
maxServerMajor;
int serverVersion,
serverMajor;
+ int writing_to_stdout;
Assert(conn != NULL);
@@ -1728,6 +1834,33 @@ BaseBackup(void)
if (maxrate > 0)
maxrate_clause = psprintf("MAX_RATE %u", maxrate);
+ if (manifest)
+ {
+ if (serverMajor < 1300)
+ {
+ const char *serverver = PQparameterStatus(conn, "server_version");
+
+ pg_log_error("backup manifests are not supported by server version %s",
+ serverver ? serverver : "'unknown'");
+ exit(1);
+ }
+
+ if (manifest_force_encode)
+ manifest_clause = "MANIFEST 'force-encode'";
+ else
+ manifest_clause = "MANIFEST 'yes'";
+ if (manifest_checksums != NULL)
+ manifest_checksums_clause = psprintf("MANIFEST_CHECKSUMS '%s'",
+ manifest_checksums);
+ }
+ else
+ {
+ if (serverMajor < 1300)
+ manifest_clause = "";
+ else
+ manifest_clause = "MANIFEST 'no'";
+ }
+
if (verbose)
pg_log_info("initiating base backup, waiting for checkpoint to complete");
@@ -1741,7 +1874,7 @@ BaseBackup(void)
}
basebkp =
- psprintf("BASE_BACKUP LABEL '%s' %s %s %s %s %s %s %s",
+ psprintf("BASE_BACKUP LABEL '%s' %s %s %s %s %s %s %s %s %s",
escaped_label,
estimatesize ? "PROGRESS" : "",
includewal == FETCH_WAL ? "WAL" : "",
@@ -1749,7 +1882,9 @@ BaseBackup(void)
includewal == NO_WAL ? "" : "NOWAIT",
maxrate_clause ? maxrate_clause : "",
format == 't' ? "TABLESPACE_MAP" : "",
- verify_checksums ? "" : "NOVERIFY_CHECKSUMS");
+ verify_checksums ? "" : "NOVERIFY_CHECKSUMS",
+ manifest_clause,
+ manifest_checksums_clause);
if (PQsendQuery(conn, basebkp) == 0)
{
@@ -1837,7 +1972,8 @@ BaseBackup(void)
/*
* When writing to stdout, require a single tablespace
*/
- if (format == 't' && strcmp(basedir, "-") == 0 && PQntuples(res) > 1)
+ writing_to_stdout = format == 't' && strcmp(basedir, "-") == 0;
+ if (writing_to_stdout && PQntuples(res) > 1)
{
pg_log_error("can only write single tablespace to stdout, database has %d",
PQntuples(res));
@@ -1866,6 +2002,19 @@ BaseBackup(void)
ReceiveAndUnpackTarFile(conn, res, i);
} /* Loop over all tablespaces */
+ /*
+ * Now receive backup manifest, if appropriate.
+ *
+ * If we're writing a tarfile to stdout, ReceiveTarFile will have already
+ * processed the backup manifest and included it in the output tarfile.
+ * Such a configuration doesn't allow for writing multiple files.
+ *
+ * If we're talking to an older server, it won't send a backup manifest,
+ * so don't try to receive one.
+ */
+ if (!writing_to_stdout && manifest)
+ ReceiveBackupManifest(conn);
+
if (showprogress)
{
progress_report(PQntuples(res), NULL, true);
@@ -2031,6 +2180,29 @@ BaseBackup(void)
}
}
+ /*
+ * After synchronizing data to disk, perform a durable rename of
+ * backup_manifest.tmp to backup_manifest, if we wrote such a file. This
+ * way, a failure or system crash before we reach this point will leave us
+ * without a backup_manifest file, decreasing the chances that a directory
+ * we leave behind will be mistaken for a valid backup.
+ */
+ if (!writing_to_stdout && manifest)
+ {
+ char tmp_filename[MAXPGPATH];
+ char filename[MAXPGPATH];
+
+ if (verbose)
+ pg_log_info("renaming backup_manifest.tmp to backup_manifest");
+
+ snprintf(tmp_filename, MAXPGPATH, "%s/backup_manifest.tmp", basedir);
+ snprintf(filename, MAXPGPATH, "%s/backup_manifest", basedir);
+
+ /* durable_rename emits its own log message in case of failure */
+ if (durable_rename(tmp_filename, filename) != 0)
+ exit(1);
+ }
+
if (verbose)
pg_log_info("base backup completed");
}
@@ -2069,6 +2241,9 @@ main(int argc, char **argv)
{"no-slot", no_argument, NULL, 2},
{"no-verify-checksums", no_argument, NULL, 3},
{"no-estimate-size", no_argument, NULL, 4},
+ {"no-manifest", no_argument, NULL, 5},
+ {"manifest-force-encode", no_argument, NULL, 6},
+ {"manifest-checksums", required_argument, NULL, 7},
{NULL, 0, NULL, 0}
};
int c;
@@ -2096,7 +2271,7 @@ main(int argc, char **argv)
atexit(cleanup_directories_atexit);
- while ((c = getopt_long(argc, argv, "CD:F:r:RS:T:X:l:nNzZ:d:c:h:p:U:s:wWkvP",
+ while ((c = getopt_long(argc, argv, "CD:F:r:RS:T:X:l:nNzZ:d:c:h:p:U:s:wWkvPm:",
long_options, &option_index)) != -1)
{
switch (c)
@@ -2240,6 +2415,15 @@ main(int argc, char **argv)
case 4:
estimatesize = false;
break;
+ case 5:
+ manifest = false;
+ break;
+ case 6:
+ manifest_force_encode = true;
+ break;
+ case 7:
+ manifest_checksums = pg_strdup(optarg);
+ break;
default:
/*
@@ -2370,6 +2554,22 @@ main(int argc, char **argv)
exit(1);
}
+ if (!manifest && manifest_checksums != NULL)
+ {
+ pg_log_error("--no-manifest and --manifest-checksums are incompatible options");
+ fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+ progname);
+ exit(1);
+ }
+
+ if (!manifest && manifest_force_encode)
+ {
+ pg_log_error("--no-manifest and --manifest-force-encode are incompatible options");
+ fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
+ progname);
+ exit(1);
+ }
+
/* connection in replication mode to server */
conn = GetConnection();
if (!conn)
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index 3c70499febf..63381764e97 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -6,7 +6,7 @@ use File::Basename qw(basename dirname);
use File::Path qw(rmtree);
use PostgresNode;
use TestLib;
-use Test::More tests => 107;
+use Test::More tests => 109;
program_help_ok('pg_basebackup');
program_version_ok('pg_basebackup');
@@ -104,6 +104,7 @@ foreach my $filename (@tempRelationFiles)
$node->command_ok([ 'pg_basebackup', '-D', "$tempdir/backup", '-X', 'none' ],
'pg_basebackup runs');
ok(-f "$tempdir/backup/PG_VERSION", 'backup was created');
+ok(-f "$tempdir/backup/backup_manifest", 'backup manifest included');
# Permissions on backup should be default
SKIP:
@@ -160,11 +161,12 @@ rmtree("$tempdir/backup");
$node->command_ok(
[
- 'pg_basebackup', '-D', "$tempdir/backup2", '--waldir',
- "$tempdir/xlog2"
+ 'pg_basebackup', '-D', "$tempdir/backup2", '--no-manifest',
+ '--waldir', "$tempdir/xlog2"
],
'separate xlog directory');
ok(-f "$tempdir/backup2/PG_VERSION", 'backup was created');
+ok(! -f "$tempdir/backup2/backup_manifest", 'manifest was suppressed');
ok(-d "$tempdir/xlog2/", 'xlog directory was created');
rmtree("$tempdir/backup2");
rmtree("$tempdir/xlog2");