Skip to content

Commit

Permalink
Add partial restore for Greenplum (wal-g#1489)
Browse files Browse the repository at this point in the history
* Add partial restore for Greenplum

* fix interface

* fix style

* naive try

* always restore aoseg tables

* little optimization: skip aoseg table for skipped database

* always set skipRedundatTars to true

* rebasing master

* add test for aovisimap

* fixes

* remove embedding impl

And move FilterFilesToUnwrap function to RestoreDesc struct

* move RestoreDescMaker into private field
  • Loading branch information
nikifkon authored Jun 26, 2023
1 parent 68afeb2 commit 61e2466
Show file tree
Hide file tree
Showing 12 changed files with 362 additions and 69 deletions.
7 changes: 6 additions & 1 deletion cmd/gp/backup_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
fetchModeDescription = "Backup fetch mode. default: do the backup unpacking " +
"and prepare the configs [unpack+prepare], unpack: backup unpacking only, prepare: config preparation only."
inPlaceFlagDescription = "Perform the backup fetch in-place (without the restore config)"
restoreOnlyDescription = `[Experimental] Downloads only databases specified by passed names from default tablespace.
Always downloads system databases.`
)

var fetchTargetUserData string
Expand All @@ -30,6 +32,7 @@ var restoreConfigPath string
var fetchContentIds *[]int
var fetchModeStr string
var inPlaceRestore bool
var partialRestoreArgs []string

var backupFetchCmd = &cobra.Command{
Use: "backup-fetch [backup_name | --target-user-data <data> | --restore-point <name>]",
Expand Down Expand Up @@ -72,7 +75,8 @@ var backupFetchCmd = &cobra.Command{
tracelog.ErrorLogger.FatalOnError(err)

internal.HandleBackupFetch(folder, targetBackupSelector,
greenplum.NewGreenplumBackupFetcher(restoreConfigPath, inPlaceRestore, logsDir, *fetchContentIds, fetchMode, restorePoint))
greenplum.NewGreenplumBackupFetcher(restoreConfigPath, inPlaceRestore, logsDir, *fetchContentIds, fetchMode, restorePoint,
partialRestoreArgs))
},
}

Expand Down Expand Up @@ -109,6 +113,7 @@ func init() {
"", restoreConfigPathDescription)
backupFetchCmd.Flags().BoolVar(&inPlaceRestore, "in-place", false, inPlaceFlagDescription)
fetchContentIds = backupFetchCmd.Flags().IntSlice("content-ids", []int{}, fetchContentIdsDescription)
backupFetchCmd.Flags().StringSliceVar(&partialRestoreArgs, "restore-only", nil, restoreOnlyDescription)

backupFetchCmd.Flags().StringVar(&fetchModeStr, "mode", "default", fetchModeDescription)
cmd.AddCommand(backupFetchCmd)
Expand Down
12 changes: 10 additions & 2 deletions cmd/gp/backup_fetch_segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,15 @@ var segBackupFetchCmd = &cobra.Command{
internal.UseReverseUnpackSetting, internal.SkipRedundantTarsSetting)
}

pgFetcher := postgres.GetPgFetcherOld(args[0], fileMask, restoreSpec,
&greenplum.ExtractProviderImpl{})
var extractProv postgres.ExtractProvider

if partialRestoreArgs != nil {
extractProv = greenplum.NewExtractProviderDBSpec(partialRestoreArgs)
} else {
extractProv = greenplum.ExtractProviderImpl{}
}

pgFetcher := postgres.GetPgFetcherOld(args[0], fileMask, restoreSpec, extractProv)
internal.HandleBackupFetch(folder, targetBackupSelector, pgFetcher)
},
}
Expand Down Expand Up @@ -81,6 +88,7 @@ func init() {
"", targetUserDataDescription)
segBackupFetchCmd.PersistentFlags().IntVar(&contentID, "content-id", 0, "segment content ID")
_ = segBackupFetchCmd.MarkFlagRequired("content-id")
segBackupFetchCmd.Flags().StringSliceVar(&partialRestoreArgs, "restore-only", nil, restoreOnlyDescription)
// Since this is a utility command called by backup-fetch, it should not be exposed to the end user.
segBackupFetchCmd.Hidden = true
cmd.AddCommand(segBackupFetchCmd)
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ services:
&& mkdir -p /export/gpdeletebeforetimebucket
&& mkdir -p /export/gpaostoragetestbucket
&& mkdir -p /export/gpdeltabackuptestbucket
&& mkdir -p /export/gppartialdatabasebucket
&& mkdir -p /export/gppartialtablebucket
&& mkdir -p /export/createrestorepointbucket
&& mkdir -p /export/storagetoolsbucket
&& mkdir -p /export/sttransferbucket
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"WALE_S3_PREFIX": "s3://gppartialdatabasebucket",
"WALG_DELTA_MAX_STEPS": "2"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"WALE_S3_PREFIX": "s3://gppartialtablebucket",
"WALG_DELTA_MAX_STEPS": "2",
"WALG_LOG_LEVEL": "DEVEL"
78 changes: 78 additions & 0 deletions docker/gp_tests/scripts/tests/partial_database_restore_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/bin/bash
set -e -x

CONFIG_FILE="/tmp/configs/partial_database_restore_test_config.json"
COMMON_CONFIG="/tmp/configs/common_config.json"
TMP_CONFIG="/tmp/configs/tmp_config.json"
cat ${CONFIG_FILE} > ${TMP_CONFIG}
echo "," >> ${TMP_CONFIG}
cat ${COMMON_CONFIG} >> ${TMP_CONFIG}
/tmp/pg_scripts/wrap_config_file.sh ${TMP_CONFIG}
source /tmp/tests/test_functions/util.sh

wal-g --config=${TMP_CONFIG} delete everything FORCE --confirm

bootstrap_gp_cluster
sleep 3
enable_pitr_extension
setup_wal_archiving

# insert_data
psql -p 6000 -c "DROP DATABASE IF EXISTS to_restore"
psql -p 6000 -c "CREATE DATABASE to_restore"
psql -p 6000 -d to_restore -c "CREATE TABLE heap AS SELECT a FROM generate_series(1,2) AS a;"
psql -p 6000 -d to_restore -c "CREATE TABLE ao(a int, b int) WITH (appendoptimized = true) DISTRIBUTED BY (a);"
psql -p 6000 -d to_restore -c "CREATE TABLE co(a int, b int) WITH (appendoptimized = true, orientation = column) DISTRIBUTED BY (a);"
psql -p 6000 -d to_restore -c "INSERT INTO ao select i, i FROM generate_series(1,2)i;"
psql -p 6000 -d to_restore -c "INSERT INTO co select i, i FROM generate_series(1,2)i;"

psql -p 6000 -c "DROP DATABASE IF EXISTS to_skip"
psql -p 6000 -c "CREATE DATABASE to_skip"
psql -p 6000 -d to_skip -c "CREATE TABLE heap AS SELECT a FROM generate_series(1,2) AS a;"
psql -p 6000 -d to_skip -c "CREATE TABLE ao(a int, b int) WITH (appendoptimized = true) DISTRIBUTED BY (a);"
psql -p 6000 -d to_skip -c "CREATE TABLE co(a int, b int) WITH (appendoptimized = true, orientation = column) DISTRIBUTED BY (a);"
psql -p 6000 -d to_skip -c "INSERT INTO ao select i, i FROM generate_series(1,2)i;"
psql -p 6000 -d to_skip -c "INSERT INTO co select i, i FROM generate_series(1,2)i;"

run_backup_logged ${TMP_CONFIG} ${PGDATA}

psql -p 6000 -d to_restore -c "INSERT INTO heap select i FROM generate_series(3,4)i;"
psql -p 6000 -d to_restore -c "INSERT INTO ao select i, i FROM generate_series(3,4)i;"
psql -p 6000 -d to_restore -c "INSERT INTO co select i, i FROM generate_series(3,4)i;"

psql -p 6000 -d to_skip -c "INSERT INTO heap select i FROM generate_series(3,4)i;"
psql -p 6000 -d to_skip -c "INSERT INTO ao select i, i FROM generate_series(3,4)i;"
psql -p 6000 -d to_skip -c "INSERT INTO co select i, i FROM generate_series(3,4)i;"

stop_and_delete_cluster_dir

wal-g --config=${TMP_CONFIG} backup-fetch ${PGDATA} LATEST --in-place --restore-only=to_restore

start_cluster

if [ "$(psql -p 6000 -t -c "select a from heap order by a;" -d to_restore -A)" != "$(printf '1\n2')" ]; then
echo "Error: Heap table in to_restore database must be restored after partial fetch"
exit 1
elif [ "$(psql -p 6000 -t -c "select a, b from ao order by a, b;" -d to_restore -A)" != "$(printf '1|1\n2|2')" ]; then
echo "Error: Append optimized table in to_restore database must be restored after partial fetch"
exit 1
elif [ "$(psql -p 6000 -t -c "select a, b from co order by a, b;" -d to_restore -A)" != "$(printf '1|1\n2|2')" ]; then
echo "Error: Column oriented table in to_restore database must be restored after partial fetch"
exit 1
fi

if ! psql -p 6000 -t -c "select a from heap order by a;" -d to_skip -A 2>&1 | grep -q "is not a valid data directory"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo "$(psql -p 6000 -t -c "select a from heap order by a;" -d to_skip -A)"
exit 1
elif ! psql -p 6000 -t -c "select a, b from ao order by a, b;" -d to_skip -A 2>&1 | grep "is not a valid data directory"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo "$(psql -p 6000 -t -c "select a, b from ao order by a, b;" -d to_skip -A)"
exit 1
elif ! psql -p 6000 -t -c "select a, b from co order by a, b;" -d to_skip -A 2>&1 | grep "is not a valid data directory"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo "$(psql -p 6000 -t -c "select a, b from co order by a, b;" -d to_skip -A)"
exit 1
fi

cleanup
100 changes: 100 additions & 0 deletions docker/gp_tests/scripts/tests/partial_table_restore_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/bin/bash
set -e -x

CONFIG_FILE="/tmp/configs/partial_table_restore_test_config.json"
COMMON_CONFIG="/tmp/configs/common_config.json"
TMP_CONFIG="/tmp/configs/tmp_config.json"
cat ${CONFIG_FILE} > ${TMP_CONFIG}
echo "," >> ${TMP_CONFIG}
cat ${COMMON_CONFIG} >> ${TMP_CONFIG}
/tmp/pg_scripts/wrap_config_file.sh ${TMP_CONFIG}
source /tmp/tests/test_functions/util.sh

wal-g --config=${TMP_CONFIG} delete everything FORCE --confirm

bootstrap_gp_cluster
sleep 3
enable_pitr_extension
setup_wal_archiving

# insert_data
n=10000
it=10
expected_count=$(($n + $it * 5))

psql -p 6000 -c "DROP DATABASE IF EXISTS db"
psql -p 6000 -c "CREATE DATABASE db"
psql -p 6000 -d db -c "CREATE TABLE heap_to_restore AS SELECT a FROM generate_series(1,$n) AS a;"
psql -p 6000 -d db -c "CREATE TABLE heap_to_skip AS SELECT a FROM generate_series(1,$n) AS a;"
psql -p 6000 -d db -c "CREATE TABLE ao_to_restore(a int, b int) WITH (appendoptimized = true) DISTRIBUTED BY (a);"
psql -p 6000 -d db -c "CREATE TABLE ao_to_skip(a int, b int) WITH (appendoptimized = true) DISTRIBUTED BY (a);"
psql -p 6000 -d db -c "CREATE TABLE co_to_restore(a int, b int) WITH (appendoptimized = true, orientation = column) DISTRIBUTED BY (a);"
psql -p 6000 -d db -c "CREATE TABLE co_to_skip(a int, b int) WITH (appendoptimized = true, orientation = column) DISTRIBUTED BY (a);"
psql -p 6000 -d db -c "INSERT INTO ao_to_restore SELECT i, i FROM generate_series(1,$n)i;"
psql -p 6000 -d db -c "INSERT INTO ao_to_skip SELECT i, i FROM generate_series(1,$n)i;"
psql -p 6000 -d db -c "INSERT INTO co_to_restore SELECT i, i FROM generate_series(1,$n)i;"
psql -p 6000 -d db -c "INSERT INTO co_to_skip SELECT i, i FROM generate_series(1,$n)i;"

# check aovisimap
insert_10_delete_5() {
start_val=$1
stop_val=$(($start_val + 9))
stop_val_d=$(($start_val + 4))
psql -p 6000 -d db -c "INSERT INTO ao_to_restore SELECT i, i FROM generate_series($start_val,$stop_val)i;"
psql -p 6000 -d db -c "INSERT INTO ao_to_skip SELECT i, i FROM generate_series($start_val,$stop_val)i;"
psql -p 6000 -d db -c "INSERT INTO co_to_restore SELECT i, i FROM generate_series($start_val,$stop_val)i;"
psql -p 6000 -d db -c "INSERT INTO co_to_skip SELECT i, i FROM generate_series($start_val,$stop_val)i;"

psql -p 6000 -d db -c "DELETE FROM ao_to_restore WHERE a >= $start_val and a <= $stop_val_d;"
psql -p 6000 -d db -c "DELETE FROM ao_to_skip WHERE a >= $start_val and a <= $stop_val_d;"
psql -p 6000 -d db -c "DELETE FROM co_to_restore WHERE a >= $start_val and a <= $stop_val_d;"
psql -p 6000 -d db -c "DELETE FROM co_to_skip WHERE a >= $start_val and a <= $stop_val_d;"
}

for i in $(seq 1 $it);
do
insert_10_delete_5 $(($n + 1 + 10*($i-1)))
done

run_backup_logged ${TMP_CONFIG} ${PGDATA}
stop_and_delete_cluster_dir

wal-g --config=${TMP_CONFIG} backup-fetch LATEST --in-place --restore-only=db/heap_to_restore,db/ao_to_restore,db/co_to_restore

start_cluster

if [ "$(psql -p 6000 -t -c "SELECT count(*) FROM heap_to_restore;" -d db -A)" != $n ]; then
echo "Error: Heap table in db database must be restored after partial fetch"
exit 1
elif [ "$(psql -p 6000 -t -c "SELECT count(*) FROM ao_to_restore;" -d db -A)" != $expected_count ]; then
echo "Error: Append optimized table in db database must be restored after partial fetch"
exit 1
elif [ "$(psql -p 6000 -t -c "SELECT count(*) FROM co_to_restore;" -d db -A)" != $expected_count ]; then
echo "Error: Column oriented table in db database must be restored after partial fetch"
exit 1
fi

EXPECTED_HEAP_ERROR_MSG="could not open file"
EXPECTED_AO_ERROR_MSG="append-Only storage read could not open segment file"

set +e
heap_output=$(psql -p 6000 -t -c "SELECT count(*) FROM heap_to_skip;" -d db -A 2>&1)
ao_output=$(psql -p 6000 -t -c "SELECT count(*) FROM ao_to_skip;" -d db -A 2>&1)
aocs_output=$(psql -p 6000 -t -c "SELECT count(*) FROM co_to_skip;" -d db -A 2>&1)
set -e

if ! echo $heap_output | grep -q "$EXPECTED_HEAP_ERROR_MSG"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo $heap_output
exit 1
elif ! echo $ao_output | grep -q "$EXPECTED_AO_ERROR_MSG"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo $ao_output
exit 1
elif ! echo $aocs_output | grep -q "$EXPECTED_AO_ERROR_MSG"; then
echo "Error: to_skip database directory must be emtpy after partial fetch"
echo $aocs_output
exit 1
fi

cleanup
28 changes: 28 additions & 0 deletions docs/Greenplum.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,34 @@ wal-g backup-fetch LATEST --content-ids=3,5,7 --restore-config=restore-config.js
wal-g backup-fetch LATEST --mode=unpack --restore-config=restore-config.json --config=/etc/wal-g/wal-g.yaml
```

#### Restore only specific databases
During partial restore wal-g restores only specified databases' files. Use 'database' or 'database/namespace.table' as a parameter ('public' namespace can be omitted).

Require files metadata with database names data, which is automatically collected during local backup. With remote backup this option does not work.

Restores system databases, tables and aoseg tables automatically.

```bash
wal-g backup-fetch LATEST --restore-only=db, "db with spaces" --restore-config=restore-config.json --config=/etc/wal-g/wal-g.yaml
```

Note: Double quotes are only needed to insert spaces and will be ignored

Example:

`--restore-only=my_db,"another db"`

is equivalent to

`--restore-only=my_db,another" "db`

or even

`--restore-only=my_db,anoth"e"r" "d"b"`

Because of unrestored databases' remains are still in system tables, it is recommended to drop them.


#### In-place restore
WAL-G can also do in-place backup restoration without the restore config. It might be useful when restoring to the same hosts that were used to make a backup:
```bash
Expand Down
15 changes: 10 additions & 5 deletions internal/databases/greenplum/backup_fetch_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ type FetchHandler struct {
contentIDsToFetch map[int]bool
fetchMode BackupFetchMode
restorePoint string
partialRestoreArgs []string
}

// nolint:gocritic
func NewFetchHandler(
backup internal.Backup, sentinel BackupSentinelDto,
segCfgMaker SegConfigMaker, logsDir string,
fetchContentIds []int, mode BackupFetchMode,
restorePoint string,
restorePoint string, partialRestoreArgs []string,
) *FetchHandler {
backupIDByContentID := make(map[int]string)
segmentConfigs := make([]cluster.SegConfig, 0)
Expand Down Expand Up @@ -91,6 +92,7 @@ func NewFetchHandler(
contentIDsToFetch: prepareContentIDsToFetch(fetchContentIds, segmentConfigs),
fetchMode: mode,
restorePoint: restorePoint,
partialRestoreArgs: partialRestoreArgs,
}
}

Expand Down Expand Up @@ -246,17 +248,20 @@ func (fh *FetchHandler) buildFetchCommand(contentID int) string {
fmt.Sprintf("--content-id=%d", segment.ContentID),
fmt.Sprintf("--target-user-data=%s", segUserData.QuotedString()),
fmt.Sprintf("--config=%s", internal.CfgFile),
// forward STDOUT& STDERR to log file
">>", formatSegmentLogPath(contentID), "2>&1",
}
if fh.partialRestoreArgs != nil {
cmd = append(cmd, fmt.Sprintf("--restore-only=%s", strings.Join(fh.partialRestoreArgs[:], ",")))
}
// forward STDOUT& STDERR to log file
cmd = append(cmd, ">>", formatSegmentLogPath(contentID), "2>&1")

cmdLine := strings.Join(cmd, " ")
tracelog.DebugLogger.Printf("Command to run on segment %d: %s", contentID, cmdLine)
return cmdLine
}

func NewGreenplumBackupFetcher(restoreCfgPath string, inPlaceRestore bool, logsDir string,
fetchContentIds []int, mode BackupFetchMode, restorePoint string,
fetchContentIds []int, mode BackupFetchMode, restorePoint string, partialRestoreArgs []string,
) func(folder storage.Folder, backup internal.Backup) {
return func(folder storage.Folder, backup internal.Backup) {
tracelog.InfoLogger.Printf("Starting backup-fetch for %s", backup.Name)
Expand All @@ -270,7 +275,7 @@ func NewGreenplumBackupFetcher(restoreCfgPath string, inPlaceRestore bool, logsD
segCfgMaker, err := NewSegConfigMaker(restoreCfgPath, inPlaceRestore)
tracelog.ErrorLogger.FatalOnError(err)

err = NewFetchHandler(backup, sentinel, segCfgMaker, logsDir, fetchContentIds, mode, restorePoint).Fetch()
err = NewFetchHandler(backup, sentinel, segCfgMaker, logsDir, fetchContentIds, mode, restorePoint, partialRestoreArgs).Fetch()
tracelog.ErrorLogger.FatalOnError(err)
}
}
Expand Down
Loading

0 comments on commit 61e2466

Please sign in to comment.