Vor kurzem haben wir die Datenbank eines Kunden aufgeräumt. Dabei wurde ein halbes Terabyte an Speicherplatz frei. Dank partitionierter Tabellen dauerte der Löschvorgang nur wenige Sekunden. Das Problem: verwaltete Datenbanken von AWS Relation Database Service (RDS) können automatisch wachsen aber niemals schrumpfen. Hier zeigen wir, wie wir den freien Speicherplatz wieder loswurden und somit unnötige Kosten vermeiden.

Voraussetzungen und Vorbereitung

Da RDS Laufwerke nicht schrumpfen können, liegt die Lösung darin, eine neue Datenbankinstanz mit kleinerem Laufwerk zu erzeugen, die Daten zu migrieren und anschließend die Originalinstanz zu löschen. In unserem Fall handelt es sich um eine RDS for PostgreSQL Instanz, die wesentlichen Schritte sollten sich allerdings auf andere Datenbanksysteme wie MySQL oder MariaDB übertragen lassen, wenn die entsprechenden Migrationswerkzeuge anstelle der hier verwendeten Postgres-Tools verwendet werden.

Tatsächliche Datenbankgröße herausfinden

Über die RDS Konsole lässt sich die momentane Größe des RDS Laufwerks herausfinden und wieviel freier Speicherplatz darauf zur Verfügung steht. Um allerdings die tatsächliche Größe der zu migrierenden Datenbank herauszufinden, eignet sich die Funktion pg_database_size besser. Die berechnete Größe wird außerdem genauer, wenn zuvor ein vollständiger Vacuum Durchgang ausgeführt wurde:

VACUUM FULL ANALYZE VERBOSE;

Dabei werden die Tabellen am Stück neu geschrieben, wodurch zuvor lediglich als frei gekennzeichneter Speicherplatz tatsächlich freigegeben wird. Dieser Vorgang kann allerdings einige Stunden dauern, in unserem Fall ca. eineinhalb Stunden für eine anfängliche Datenbankgröße von 217 GB und eine resultierende Größe von 166 GB.

Die zu erwartende Größe der Datenbank my_database lässt sich dann bestimmen mit:

SELECT pg_size_pretty(pg_database_size('my_database'));

Ziel RDS Instanz zur Wiederherstellung der Daten erstellen

Mit bekannter Datenbankgröße können wir jetzt eine neue RDS Instanz erstellen und die Laufwerksgröße entsprechend klein wählen, wobei zumindest einige GB Puffer ratsam sind. Achten Sie darauf, dass sich die neue Instanz in derselben VPC befindet, wie die Originalinstanz und Sie außerdem dieselben Security Groups zuweisen. Achten Sie außerdem auf die gewählte PostgreSQL-Version der neuen Instanz. Falls Sie die Gelegenheit nutzen möchten, um auf eine neue Hauptversion upzudaten, stellen Sie sicher, dass AWS alle benötigten Erweiterungen auch für die neue Version zur Verfügung stellt. In unserem Fall haben wir von PostgreSQL 12 auf 14 upgedated.

EC2 Instanz für die Datenmigration erstellen

Für den Migrationsprozess brauchen wir vorübergehend eine EC2 Instanz mit ausreichend Speicherplatz, um das Datenbankbackup zwischenzuspeichern. Je nach Art der Daten kann das Backup deutlich kleiner ausfallen, wenn ein komprimiertes Datenformat gewählt wird. In unserem Fall benötigte das Backup der 166 GB Datenbank lediglich 23 GB, allerdings erzeugt die Kompression zusätzlichen Performance Overhead. Dafür dürfte sie wiederum bei den verfügbaren IOPS für EBS Laufwerke einsparen. In unserem Fall ging der Backup-Prozess jedenfalls trotz Kompression hinreichend schnell.

Stellen Sie sicher, dass:

  • die EC2 Instanz über genügend Speicherplatz für das Backup verfügt.
  • die EC2 Instanz auf die Quell-Datenbank zugreifen kann (VPC und Security Groups).
  • die EC2 Instanz auf die Ziel-Datenbank zugreifen kann (VPC und Security Groups).
  • die Postgres Client Tools verfügbar sind. (Unter Ubuntu können diese mit apt install postgresql-client installiert werden.)
  • der screen Befehl installiert ist.

Schreib- und Updatevorgänge auf der Datenbank verhindern

Beenden Sie alle Dienste, die neue Daten in die Datenbank einfügen oder Daten verändern. Falls dies nicht möglich ist, verwenden Sie eine andere Migrationsmethode, wie z. B. logische Replikation. Lesezugriff ist unproblematisch und während des gesamten Vorgangs weiter gewährt werden.

Datenbankbackup erstellen

Loggen Sie sich per SSH auf der EC2 Instanz ein, die Sie für die Datenmigration erstellt haben. Da das Backup einige Zeit in Anspruch nehmen wird, ist es wichtig, dass der Prozess auch dann nicht unterbrochen wird, wenn beispielsweise die Internetverbindung abreist. Starten Sie deshalb eine neue screen Sitzung. Sollte die Verbindung abreisen, läuft das Backup auf der EC2 Instanz trotzdem weiter und Sie können die Sitzung jederzeit mit screen -r wiederherstellen.

Globale Daten sichern

Da Nutzer und Rollen nicht Teil der eigentlichen Datenbank sind, sondern global auf dem Postgres Server zur Verfügung stehen, müssen wir diese separat abspeichern. Eine Besonderheit bei RDS ist, dass selbst der postgres Nutzer nur über die eingeschränkte Rolle rds_superuser verfügt und damit keine Passwörter auslesen kann. Diese müssen deshalb explizit aus dem Backup ausgeschlossen und später manuell gesetzt werden:

pg_dumpall --host <source host> \
           --no-role-passwords \
           --globals-only \
           --username postgres > globals.sql

Datenbank sichern

Sichern Sie jetzt die Datenbank an sich (in unserem Fall my_database). Dies beinhaltet alle Erweiterungen, Tabellen, Partitionen, benutzerdefinierte Funktionen (UDFs), Sequenzen, Indizes etc. Nicht enthalten sind Daten, die über Foreign Data Wrappers aus externen Quellen angebunden sind.

pg_dump -h <source host> -p 5432 -U postgres -F c -v -f db.backup my_database

Hier wählt die Option -F c ein proprietäres, komprimiertes Datenformat aus, das sich insbesondere auch dann eignet, wenn das Backup zwischen verschiedenen VMs, Accounts oder auch Cloud-Anbietern kopiert werden soll. Wenn das (wie hier) nicht der Fall ist und Speicherplatz kein Problem ist, wäre es einen Versuch wert, das Verzeichnis-Format von pg_dump zu verwenden, da es mehrere Backup-Worker parallel erlaubt und damit vermutlich schneller geht. In unserem Fall funktionierte das komprimierte Dateiformat jedenfalls problemlos und das Backup der 166 GB Datenbank dauerte ca. eineinhalb Stunden, wobei sich EC2 und RDS Instanz in derselben Availabilty Zone befanden. Das komprimierte Backup benötigte lediglich 23 GB.

Datenbank einspielen

Globale Daten wiederherstellen

Zunächst müssen die globalen Daten wiederhergestellt werden, um die Nutzer und Rollen zu erzeugen, die für die Wiederherstellung der Daten existieren müssen, damit die Berechtigungen entsprechend gesetzt werden können. Dieser Prozess wird einige Fehler erzeugen, da die rds_* rollen bereits existieren. Diese Fehler können getrost ignoriert werden.

psql --host <destination host> \
     --dbname postgres \
     --username postgres -f globals.sql

Da keine Passwörter im Backup enthalten sind, muss für jeden User das Passwort und die WITH LOGIN Eigenschaft gesetzt werden. Mit psql ist dies beispielsweise für einen Nutzer namens read_only folgendermaßen möglich:

psql --host <destination host> --dbname postgres --username postgres
\password read_only;

ALTER ROLE read_only WITH LOGIN;

Leere Datenbank erzeugen

Da das Backup zwar die Daten, nicht aber die Datenbank selbst enthält, muss die Zieldatenbank (hier my_database) bereits existieren, um die Daten dort später einspielen zu können.

psql --host <destination host> \
     --dbname postgres \
     --username postgres -c "create database my_database;"

Nachdem diese Datenbank jedoch manuell erstellt wurde, fehlen die ursprünglichen Berechtigungen, bspw. um eine Verbindung zur Datenbank herzustellen. Wir müssen diese also manuell erzeugen. Die Berechtigungen der Originaldatenbank können in psql mithilfe des Kurzbefehls \l+ angezeigt werden. Ungünstiger Weise werden hier allerdings Access Control List (ACL) Abkürzungen statt der SQL-Namen der entsprechenden Berechtigungen angezeigt. Die für Datenbanken relevanten Einträge sind

ACL Abkürzung SQL Name
C CREATE
T TEMPORARY
c CONNECT

In unserem Fall erlauben wir dem Nutzer read_only sich mit der Datenbank my_database zu verbinden:

psql --host <destination host> --dbname my_database --username postgres
GRANT CONNECT ON DATABASE my_database to read_only;

Daten wiederherstellen

Jetzt, da die leere Datenbank bereitsteht, können wir endlich die Daten einspielen:

pg_restore -h <destination host>  -p 5432 -U postgres -d my_database -v db.backup 

Für uns dauerte der Import der 166 GB großen (23 GB komprimiert) Datenbank knapp 5 Stunden. Dieser Vorgang sollte sich deutlich beschleunigen lassen, wenn die Konfiguration der Postgres-Parameter für den Wiederherstellungsprozess kurzfristig angepasst werden. Für den produktiven Einsatz sollten diese Änderungen aber unbedingt wieder rückgängig gemacht werden.

Austausch der beiden RDS Instanzen

Damit Ihre Anwendung in Zukunft die neue RDS Instanz verwendet, tauschen wir nun die beiden Instanzen, indem wir sie umbenennen. Dadurch kommt es zu einem kurzen Ausfall für alle Anwendungen, die noch auf die Datenbank zugreifen.

  1. Benennen Sie die alte RDS Instanz über die AWS Konsole um (z. B. my-instance in my-instance-old). Das hat zur Folge, dass sich der DNS-Name der Instanz ebenfalls ändert. RDS wird beim Übernehmen des neuen Namen die Instanz neustarten. (Achten Sie darauf, dass Sie Änderungen sofort anwenden und nicht erst beim nächsten Wartungsfenster.)
  2. Benennen Sie die neue RDS Instanz über die AWS Konsole um und verwenden Sie dabei denselben Namen, den zuvor die alte Instanz hatte (z. B. my-instance-new in my-instance)
  3. Halten Sie die alte RDS Instanz über die AWS Konsole an.

Anwendungen wieder starten

Wir können jetzt alle Anwendungen, wieder hochfahren und ausgiebig testen. Funktioniert alles, wie erwartet können wir (evtl. erst nach einigen Tagen) die ursprüngliche RDS Instanz löschen. Zur Sicherheit können wir einen finalen Snapshot erstellen, wie von der AWS Konsole empfohlen.