Skip to content

Commit 0202a0b

Browse files
authored
Merge pull request #418 from aiven/alex-delta-backups
Add support for delta basebackups [BF-6] #418
2 parents fbe66b0 + 366467e commit 0202a0b

29 files changed

+1661
-130
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ __pycache__/
1818
/venv/
1919
/.venv/
2020
*.orig
21+
/pghoard-rpm-src/

.pylintrc

+4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ disable=
2828

2929
[FORMAT]
3030
max-line-length=125
31+
max-module-lines=1000
3132

3233
[REPORTS]
3334
output-format=text
3435
reports=no
36+
37+
[TYPECHECK]
38+
extension-pkg-whitelist=pydantic

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
short_ver = 2.1.1
1+
short_ver = $(shell git describe --abbrev=0)
22
long_ver = $(shell git describe --long 2>/dev/null || echo $(short_ver)-0-unknown-g`git describe --always`)
33
generated = pghoard/version.py
44

pghoard.spec

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ License: ASL 2.0
77
Source0: pghoard-rpm-src.tar
88
Requires: systemd
99
Requires: python3-botocore, python3-cryptography >= 0.8, python3-dateutil
10-
Requires: python3-psycopg2, python3-requests, python3-snappy
10+
Requires: python3-psycopg2, python3-requests, python3-snappy, python3-zstandard, python3-pydantic,
1111
Conflicts: pgespresso92 < 1.2, pgespresso93 < 1.2, pgespresso94 < 1.2, pgespresso95 < 1.2
1212
BuildRequires: python3-flake8, python3-pytest, python3-pylint, python3-devel, golang
1313

pghoard/basebackup.py

+121-48
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
import subprocess
1515
import time
1616
from concurrent.futures import ThreadPoolExecutor
17+
from dataclasses import dataclass
1718
from queue import Empty, Queue
1819
from tempfile import NamedTemporaryFile
1920
from threading import Thread
21+
from typing import Optional
2022

2123
import psycopg2
2224

@@ -25,8 +27,10 @@
2527

2628
# pylint: disable=superfluous-parens
2729
from . import common, version, wal
30+
from .basebackup_delta import DeltaBaseBackup
2831
from .common import (
29-
connection_string_using_pgpass, replication_connection_string_and_slot_using_pgpass, set_stream_nonblocking,
32+
BackupFailure, BaseBackupFormat, BaseBackupMode, connection_string_using_pgpass,
33+
replication_connection_string_and_slot_using_pgpass, set_stream_nonblocking,
3034
set_subprocess_stdout_and_stderr_nonblocking, terminate_subprocess
3135
)
3236
from .patchedtarfile import tarfile
@@ -46,14 +50,38 @@
4650
]
4751

4852

49-
class BackupFailure(Exception):
50-
"""Backup failed - post a failure to callback_queue and allow the thread to terminate"""
51-
52-
5353
class NoException(BaseException):
5454
"""Exception that's never raised, used in conditional except blocks"""
5555

5656

57+
@dataclass(frozen=True)
58+
class EncryptionData:
59+
encryption_key_id: Optional[str]
60+
rsa_public_key: Optional[str]
61+
62+
@staticmethod
63+
def from_site_config(site_config) -> "EncryptionData":
64+
encryption_key_id = site_config["encryption_key_id"]
65+
if encryption_key_id:
66+
rsa_public_key = site_config["encryption_keys"][encryption_key_id]["public"]
67+
else:
68+
rsa_public_key = None
69+
70+
return EncryptionData(encryption_key_id=encryption_key_id, rsa_public_key=rsa_public_key)
71+
72+
73+
@dataclass(frozen=True)
74+
class CompressionData:
75+
algorithm: str
76+
level: int
77+
78+
@staticmethod
79+
def from_config(config) -> "CompressionData":
80+
algorithm = config["compression"]["algorithm"]
81+
level = config["compression"]["level"]
82+
return CompressionData(algorithm=algorithm, level=level)
83+
84+
5785
class PGBaseBackup(Thread):
5886
def __init__(
5987
self,
@@ -63,10 +91,12 @@ def __init__(
6391
basebackup_path,
6492
compression_queue,
6593
metrics,
94+
storage,
6695
transfer_queue=None,
6796
callback_queue=None,
6897
pg_version_server=None,
69-
metadata=None
98+
metadata=None,
99+
get_remote_basebackups_info=None
70100
):
71101
super().__init__()
72102
self.log = logging.getLogger("PGBaseBackup")
@@ -84,15 +114,19 @@ def __init__(
84114
self.pid = None
85115
self.pg_version_server = pg_version_server
86116
self.latest_activity = datetime.datetime.utcnow()
117+
self.storage = storage
118+
self.get_remote_basebackups_info = get_remote_basebackups_info
87119

88120
def run(self):
89121
try:
90-
basebackup_mode = self.config["backup_sites"][self.site]["basebackup_mode"]
91-
if basebackup_mode == "basic":
122+
basebackup_mode = self.site_config["basebackup_mode"]
123+
if basebackup_mode == BaseBackupMode.basic:
92124
self.run_basic_basebackup()
93-
elif basebackup_mode == "local-tar":
125+
elif basebackup_mode == BaseBackupMode.local_tar:
94126
self.run_local_tar_basebackup()
95-
elif basebackup_mode == "pipe":
127+
elif basebackup_mode == BaseBackupMode.delta:
128+
self.run_local_tar_basebackup(delta=True)
129+
elif basebackup_mode == BaseBackupMode.pipe:
96130
self.run_piped_basebackup()
97131
else:
98132
raise errors.InvalidConfigurationError("Unsupported basebackup_mode {!r}".format(basebackup_mode))
@@ -129,7 +163,7 @@ def get_paths_for_backup(basebackup_path):
129163

130164
def get_command_line(self, output_name):
131165
command = [
132-
self.config["backup_sites"][self.site]["pg_basebackup_path"],
166+
self.site_config["pg_basebackup_path"],
133167
"--format",
134168
"tar",
135169
"--label",
@@ -139,7 +173,7 @@ def get_command_line(self, output_name):
139173
output_name,
140174
]
141175

142-
if self.config["backup_sites"][self.site]["active_backup_mode"] == "standalone_hot_backup":
176+
if self.site_config["active_backup_mode"] == "standalone_hot_backup":
143177
if self.pg_version_server >= 100000:
144178
command.extend(["--wal-method=fetch"])
145179
else:
@@ -169,9 +203,9 @@ def check_command_success(self, proc, output_file):
169203

170204
def basebackup_compression_pipe(self, proc, basebackup_path):
171205
rsa_public_key = None
172-
encryption_key_id = self.config["backup_sites"][self.site]["encryption_key_id"]
206+
encryption_key_id = self.site_config["encryption_key_id"]
173207
if encryption_key_id:
174-
rsa_public_key = self.config["backup_sites"][self.site]["encryption_keys"][encryption_key_id]["public"]
208+
rsa_public_key = self.site_config["encryption_keys"][encryption_key_id]["public"]
175209
compression_algorithm = self.config["compression"]["algorithm"]
176210
compression_level = self.config["compression"]["level"]
177211
self.log.debug("Compressing basebackup directly to file: %r", basebackup_path)
@@ -461,25 +495,30 @@ def add_entry(archive_path, local_path, *, missing_ok):
461495
yield from add_directory(archive_path, local_path, missing_ok=False)
462496
yield archive_path, local_path, False, "leave"
463497

498+
@property
499+
def site_config(self):
500+
return self.config["backup_sites"][self.site]
501+
502+
@property
503+
def encryption_data(self) -> EncryptionData:
504+
return EncryptionData.from_site_config(self.site_config)
505+
506+
@property
507+
def compression_data(self) -> CompressionData:
508+
return CompressionData.from_config(self.config)
509+
464510
def tar_one_file(
465511
self, *, temp_dir, chunk_path, files_to_backup, callback_queue, filetype="basebackup_chunk", extra_metadata=None
466512
):
467513
start_time = time.monotonic()
468514

469-
site_config = self.config["backup_sites"][self.site]
470-
encryption_key_id = site_config["encryption_key_id"]
471-
if encryption_key_id:
472-
rsa_public_key = site_config["encryption_keys"][encryption_key_id]["public"]
473-
else:
474-
rsa_public_key = None
475-
476515
with NamedTemporaryFile(dir=temp_dir, prefix=os.path.basename(chunk_path), suffix=".tmp") as raw_output_obj:
477516
# pylint: disable=bad-continuation
478517
with rohmufile.file_writer(
479-
compression_algorithm=self.config["compression"]["algorithm"],
480-
compression_level=self.config["compression"]["level"],
481-
compression_threads=site_config["basebackup_compression_threads"],
482-
rsa_public_key=rsa_public_key,
518+
compression_algorithm=self.compression_data.algorithm,
519+
compression_level=self.compression_data.level,
520+
compression_threads=self.site_config["basebackup_compression_threads"],
521+
rsa_public_key=self.encryption_data.rsa_public_key,
483522
fileobj=raw_output_obj
484523
) as output_obj:
485524
with tarfile.TarFile(fileobj=output_obj, mode="w") as output_tar:
@@ -492,7 +531,7 @@ def tar_one_file(
492531
os.link(raw_output_obj.name, chunk_path)
493532

494533
rohmufile.log_compression_result(
495-
encrypted=bool(encryption_key_id),
534+
encrypted=bool(self.encryption_data.encryption_key_id),
496535
elapsed=time.monotonic() - start_time,
497536
original_size=input_size,
498537
result_size=result_size,
@@ -505,16 +544,16 @@ def tar_one_file(
505544
"pghoard.compressed_size_ratio",
506545
size_ratio,
507546
tags={
508-
"algorithm": self.config["compression"]["algorithm"],
547+
"algorithm": self.compression_data.algorithm,
509548
"site": self.site,
510549
"type": "basebackup",
511550
}
512551
)
513552

514553
metadata = {
515-
"compression-algorithm": self.config["compression"]["algorithm"],
516-
"encryption-key-id": encryption_key_id,
517-
"format": "pghoard-bb-v2",
554+
"compression-algorithm": self.compression_data.algorithm,
555+
"encryption-key-id": self.encryption_data.encryption_key_id,
556+
"format": BaseBackupFormat.v2,
518557
"original-file-size": input_size,
519558
"host": socket.gethostname(),
520559
}
@@ -573,9 +612,8 @@ def create_and_upload_chunks(self, chunks, data_file_format, temp_base_dir):
573612
self.chunks_on_disk = 0
574613
i = 0
575614

576-
site_config = self.config["backup_sites"][self.site]
577-
max_chunks_on_disk = site_config["basebackup_chunks_in_progress"]
578-
threads = site_config["basebackup_threads"]
615+
max_chunks_on_disk = self.site_config["basebackup_chunks_in_progress"]
616+
threads = self.site_config["basebackup_threads"]
579617
with ThreadPoolExecutor(max_workers=threads) as tpe:
580618
pending_compress_and_encrypt_tasks = []
581619
while i < len(chunks):
@@ -612,8 +650,9 @@ def create_and_upload_chunks(self, chunks, data_file_format, temp_base_dir):
612650

613651
return chunk_files
614652

615-
def run_local_tar_basebackup(self):
616-
pgdata = self.config["backup_sites"][self.site]["pg_data_directory"]
653+
def run_local_tar_basebackup(self, delta=False):
654+
control_files_metadata_extra = {}
655+
pgdata = self.site_config["pg_data_directory"]
617656
if not os.path.isdir(pgdata):
618657
raise errors.InvalidConfigurationError("pg_data_directory {!r} does not exist".format(pgdata))
619658

@@ -622,7 +661,7 @@ def run_local_tar_basebackup(self):
622661
data_file_format = "{}/{}.{{0:08d}}.pghoard".format(compressed_base, os.path.basename(compressed_base)).format
623662

624663
# Default to 2GB chunks of uncompressed data
625-
target_chunk_size = self.config["backup_sites"][self.site]["basebackup_chunk_size"]
664+
target_chunk_size = self.site_config["basebackup_chunk_size"]
626665

627666
self.log.debug("Connecting to database to start backup process")
628667
connection_string = connection_string_using_pgpass(self.connection_info)
@@ -686,13 +725,45 @@ def run_local_tar_basebackup(self):
686725
self.log.info("Starting to backup %r and %r tablespaces to %r", pgdata, len(tablespaces), compressed_base)
687726
start_time = time.monotonic()
688727

689-
total_file_count, chunks = self.find_and_split_files_to_backup(
690-
pgdata=pgdata, tablespaces=tablespaces, target_chunk_size=target_chunk_size
691-
)
728+
if delta:
729+
delta_backup = DeltaBaseBackup(
730+
storage=self.storage,
731+
site=self.site,
732+
site_config=self.site_config,
733+
transfer_queue=self.transfer_queue,
734+
metrics=self.metrics,
735+
encryption_data=self.encryption_data,
736+
compression_data=self.compression_data,
737+
get_remote_basebackups_info=self.get_remote_basebackups_info,
738+
parallel=self.site_config["basebackup_threads"],
739+
temp_base_dir=temp_base_dir,
740+
compressed_base=compressed_base
741+
)
742+
total_size_plain, total_size_enc, manifest, total_file_count = delta_backup.run(
743+
pgdata=pgdata,
744+
src_iterate_func=lambda: (
745+
item[1]
746+
for item in self.find_files_to_backup(pgdata=pgdata, tablespaces=tablespaces)
747+
if not item[1].endswith(".pem") # Exclude such files like "dh1024.pem"
748+
),
749+
)
750+
751+
chunks_count = total_file_count
752+
control_files_metadata_extra["manifest"] = manifest.jsondict()
753+
self.metadata["format"] = BaseBackupFormat.delta_v1
754+
else:
755+
total_file_count, chunks = self.find_and_split_files_to_backup(
756+
pgdata=pgdata, tablespaces=tablespaces, target_chunk_size=target_chunk_size
757+
)
758+
chunks_count = len(chunks)
759+
# Tar up the chunks and submit them for upload; note that we start from chunk 1 here; chunk 0
760+
# is reserved for special files and metadata and will be generated last.
761+
chunk_files = self.create_and_upload_chunks(chunks, data_file_format, temp_base_dir)
692762

693-
# Tar up the chunks and submit them for upload; note that we start from chunk 1 here; chunk 0
694-
# is reserved for special files and metadata and will be generated last.
695-
chunk_files = self.create_and_upload_chunks(chunks, data_file_format, temp_base_dir)
763+
total_size_plain = sum(item["input_size"] for item in chunk_files)
764+
total_size_enc = sum(item["result_size"] for item in chunk_files)
765+
766+
control_files_metadata_extra["chunks"] = chunk_files
696767

697768
# Everything is now tarred up, grab the latest pg_control and stop the backup process
698769
with open(os.path.join(pgdata, "global", "pg_control"), "rb") as fp:
@@ -709,14 +780,16 @@ def run_local_tar_basebackup(self):
709780
db_conn.commit()
710781
backup_stopped = True
711782

712-
total_size_plain = sum(item["input_size"] for item in chunk_files)
713-
total_size_enc = sum(item["result_size"] for item in chunk_files)
783+
backup_time = time.monotonic() - start_time
784+
self.metrics.gauge(
785+
"pghoard.backup_time_{}".format(self.site_config["basebackup_mode"]),
786+
backup_time,
787+
)
714788

715789
self.log.info(
716790
"Basebackup generation finished, %r files, %r chunks, "
717-
"%r byte input, %r byte output, took %r seconds, waiting to upload", total_file_count, len(chunk_files),
718-
total_size_plain, total_size_enc,
719-
time.monotonic() - start_time
791+
"%r byte input, %r byte output, took %r seconds, waiting to upload", total_file_count, chunks_count,
792+
total_size_plain, total_size_enc, backup_time
720793
)
721794

722795
finally:
@@ -740,13 +813,13 @@ def run_local_tar_basebackup(self):
740813
"backup_end_wal_segment": backup_end_wal_segment,
741814
"backup_start_time": backup_start_time,
742815
"backup_start_wal_segment": backup_start_wal_segment,
743-
"chunks": chunk_files,
744816
"pgdata": pgdata,
745817
"pghoard_object": "basebackup",
746818
"pghoard_version": version.__version__,
747819
"tablespaces": tablespaces,
748820
"host": socket.gethostname(),
749821
}
822+
metadata.update(control_files_metadata_extra)
750823
control_files = list(
751824
self.get_control_entries_for_tar(
752825
metadata=metadata,

0 commit comments

Comments
 (0)