From 23a2846758c3edd9e1c118fa778f474e2b4d0ee7 Mon Sep 17 00:00:00 2001 From: marcel-dempers Date: Sun, 1 Oct 2023 14:54:42 +1100 Subject: [PATCH] postgres replication guide --- .../postgresql/3-replication/README.md | 191 ++++++++++++++++++ .../postgres-1/config/pg_hba.conf | 17 ++ .../postgres-1/config/pg_ident.conf | 42 ++++ .../postgres-1/config/postgresql.conf | 33 +++ .../postgres-2/config/pg_hba.conf | 15 ++ .../postgres-2/config/pg_ident.conf | 42 ++++ .../postgres-2/config/postgresql.conf | 27 +++ .../postgresql/3-replication/summary.png | Bin 0 -> 30678 bytes 8 files changed, 367 insertions(+) create mode 100644 storage/databases/postgresql/3-replication/README.md create mode 100644 storage/databases/postgresql/3-replication/postgres-1/config/pg_hba.conf create mode 100644 storage/databases/postgresql/3-replication/postgres-1/config/pg_ident.conf create mode 100644 storage/databases/postgresql/3-replication/postgres-1/config/postgresql.conf create mode 100644 storage/databases/postgresql/3-replication/postgres-2/config/pg_hba.conf create mode 100644 storage/databases/postgresql/3-replication/postgres-2/config/pg_ident.conf create mode 100644 storage/databases/postgresql/3-replication/postgres-2/config/postgresql.conf create mode 100644 storage/databases/postgresql/3-replication/summary.png diff --git a/storage/databases/postgresql/3-replication/README.md b/storage/databases/postgresql/3-replication/README.md new file mode 100644 index 0000000..72df656 --- /dev/null +++ b/storage/databases/postgresql/3-replication/README.md @@ -0,0 +1,191 @@ +# How to replicate PostgreSQL + +So far we've learnt how to run PostgreSQL as per [chapter 1](../1-introduction/README.md).
+We've also learnt how to configure PostgreSQL with our custom configuration file as per [chapter 2](../2-configuration/README.md).
+ +In this chapter we will setup a second PostgreSQL instance.
+Then we will learn how to configure our first PostgreSQL instance to replicate its data to the new second instance.
+This will essentially give us a primary and a secondary server for better availability in case we lose our primary server.
+ +## Get our Primary PostgreSQL up and running + +Let's start by running our primary PostgreSQL in docker
+ +Few things to note here:
+* We start our instance with a different name to identify it as the first instance with the `--name postgres-1` flag and `2` for the second instance +* Set unique data volumes for data between instances +* Set unique config files for each instance +* Create and run our docker containers on the same network + +Create a new network so our instances can talk with each other: + +``` +docker network create postgres +``` + +Start with instance 1: + +``` +cd storage/databases/postgresql/3-replication + +docker run -it --rm --name postgres-1 ` +--net postgres ` +-e POSTGRES_USER=postgresadmin ` +-e POSTGRES_PASSWORD=admin123 ` +-e POSTGRES_DB=postgresdb ` +-e PGDATA="/data" ` +-v ${PWD}/postgres-1/pgdata:/data ` +-v ${PWD}/postgres-1/config:/config ` +-v ${PWD}/postgres-1/archive:/mnt/server/archive ` +-p 5000:5432 ` +postgres:15.0 -c 'config_file=/config/postgresql.conf' +``` + +## Create Replication User + +In order to take a backup we will use a new PostgreSQL user account which has the permissions to do replication.
+ +Let's create this user account by logging into `postgres-1`: + +``` +docker exec -it postgres-1 bash + +# create a new user +createuser -U postgresadmin -P -c 5 --replication replicationUser + +exit +``` + +# Enable Write-Ahead Log and Replication + +There is quite a lot to read about PostgreSQL when it comes to high availability.
+ +The first thing we want to take a look at is [WAL](https://www.postgresql.org/docs/current/wal-intro.html)
+Basically PostgreSQL has a mechanism of writing transaction logs to file and does not accept the transaction until its been written to the transaction log and flushed to disk.
+ +This ensures that if there is a crash in the system, that the database can be recovered from the transaction log.
+Hence it is "writing ahead".
+ +More documentation for configuration [wal_level](https://www.postgresql.org/docs/current/runtime-config-wal.html) and [max_wal_senders](https://www.postgresql.org/docs/current/runtime-config-replication.html) +``` +wal_level = replica +max_wal_senders = 3 +``` + +# Enable Archive + +More documentation for configuration [archive_mode](https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-ARCHIVE-MODE) + +``` +archive_mode = on +archive_command = 'test ! -f /mnt/server/archive/%f && cp %p /mnt/server/archive/%f' + +``` + +## Take a base backup + +To take a database backup, we'll be using the [pg_basebackup](https://www.postgresql.org/docs/current/app-pgbasebackup.html) utility.
+ +The utility is in the PostgreSQL docker image, so let's run it without running a database as all we need is the `pg_basebackup` utility.
+Note that we also mount our blank data directory as we will make a new backup in there: + +``` +cd storage/databases/postgresql/3-replication + +docker run -it --rm ` +--net postgres ` +-v ${PWD}/postgres-2/pgdata:/data ` +--entrypoint /bin/bash postgres:15.0 +``` + +Take the backup by logging into `postgres-1` with our `replicationUser` and writing the backup to `/data`. + +``` +pg_basebackup -h postgres-1 -p 5432 -U replicationUser -D /data/ -Fp -Xs -R +``` + +Now we should see PostgreSQL data ready for our second instance in `${PWD}/postgres-2/pgdata` + +## Start standby instance + +``` +cd storage/databases/postgresql/3-replication + +docker run -it --rm --name postgres-2 ` +--net postgres ` +-e POSTGRES_USER=postgresadmin ` +-e POSTGRES_PASSWORD=admin123 ` +-e POSTGRES_DB=postgresdb ` +-e PGDATA="/data" ` +-v ${PWD}/postgres-2/pgdata:/data ` +-v ${PWD}/postgres-2/config:/config ` +-v ${PWD}/postgres-2/archive:/mnt/server/archive ` +-p 5001:5432 ` +postgres:15.0 -c 'config_file=/config/postgresql.conf' +``` + +## Test the replication + +Let's test our replication by creating a new table in `postgres-1`
+On our primary instance, lets do that: + +``` +# login to postgres +psql --username=postgresadmin postgresdb + +#create a table +CREATE TABLE customers (firstname text, customer_id serial, date_created timestamp); + +#show the table +\dt +``` + +Now lets log into our `postgres-2` instance and view the table: + +``` +docker exec -it postgres-2 bash + +# login to postgres +psql --username=postgresadmin postgresdb + +#show the tables +\dt +``` + +## Failover + +Now lets say `postgres-1` fails.
+PostgreSQL does not have built-in automated failver and recovery and requires tooling to perform this.
+ +When `postgres-1` fails, we would use a utility called [pg_ctl](https://www.postgresql.org/docs/current/app-pg-ctl.html) to promote our stand-by server to a new primary server.
+ +Then we have to build a new stand-by server just like we did in this guide.
+We would also need to configure replication on the new primary, the same way we did in this guide.
+ +Let's stop the primary server to simulate failure: + +``` +docker rm -f postgres-1 +``` + +Then log into `postgres-2` and promote it to primary: +``` +docker exec -it postgres-2 bash + +# confirm we cannot create a table as its a stand-by server +CREATE TABLE customers (firstname text, customer_id serial, date_created timestamp); + +# run pg_ctl as postgres user (cannot be run as root!) +runuser -u postgres -- pg_ctl promote + +# confirm we can create a table as its a primary server +CREATE TABLE customers (firstname text, customer_id serial, date_created timestamp); +``` + +That's it for chapter three!
+Now we understand how to [run PostgreSQL](../1-introduction/README.md), how to [configure PostgreSQL](../2-configuration/README.md) and how to setup replication for better availability. + +## Summary + + +Summary \ No newline at end of file diff --git a/storage/databases/postgresql/3-replication/postgres-1/config/pg_hba.conf b/storage/databases/postgresql/3-replication/postgres-1/config/pg_hba.conf new file mode 100644 index 0000000..f61f814 --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-1/config/pg_hba.conf @@ -0,0 +1,17 @@ +# TYPE DATABASE USER ADDRESS METHOD + +host replication replicationUser 0.0.0.0/0 md5 + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +host all all all scram-sha-256 \ No newline at end of file diff --git a/storage/databases/postgresql/3-replication/postgres-1/config/pg_ident.conf b/storage/databases/postgresql/3-replication/postgres-1/config/pg_ident.conf new file mode 100644 index 0000000..a5870e6 --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-1/config/pg_ident.conf @@ -0,0 +1,42 @@ +# PostgreSQL User Name Maps +# ========================= +# +# Refer to the PostgreSQL documentation, chapter "Client +# Authentication" for a complete description. A short synopsis +# follows. +# +# This file controls PostgreSQL user name mapping. It maps external +# user names to their corresponding PostgreSQL user names. Records +# are of the form: +# +# MAPNAME SYSTEM-USERNAME PG-USERNAME +# +# (The uppercase quantities must be replaced by actual values.) +# +# MAPNAME is the (otherwise freely chosen) map name that was used in +# pg_hba.conf. SYSTEM-USERNAME is the detected user name of the +# client. PG-USERNAME is the requested PostgreSQL user name. The +# existence of a record specifies that SYSTEM-USERNAME may connect as +# PG-USERNAME. +# +# If SYSTEM-USERNAME starts with a slash (/), it will be treated as a +# regular expression. Optionally this can contain a capture (a +# parenthesized subexpression). The substring matching the capture +# will be substituted for \1 (backslash-one) if present in +# PG-USERNAME. +# +# Multiple maps may be specified in this file and used by pg_hba.conf. +# +# No map names are defined in the default configuration. If all +# system user names and PostgreSQL user names are the same, you don't +# need anything in this file. +# +# This file is read on server startup and when the postmaster receives +# a SIGHUP signal. If you edit the file on a running system, you have +# to SIGHUP the postmaster for the changes to take effect. You can +# use "pg_ctl reload" to do that. + +# Put your actual configuration here +# ---------------------------------- + +# MAPNAME SYSTEM-USERNAME PG-USERNAME diff --git a/storage/databases/postgresql/3-replication/postgres-1/config/postgresql.conf b/storage/databases/postgresql/3-replication/postgres-1/config/postgresql.conf new file mode 100644 index 0000000..ee6c6fc --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-1/config/postgresql.conf @@ -0,0 +1,33 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# + +data_directory = '/data' +hba_file = '/config/pg_hba.conf' +ident_file = '/config/pg_ident.conf' + +port = 5432 +listen_addresses = '*' +max_connections = 100 +shared_buffers = 128MB +dynamic_shared_memory_type = posix +max_wal_size = 1GB +min_wal_size = 80MB +log_timezone = 'Etc/UTC' +datestyle = 'iso, mdy' +timezone = 'Etc/UTC' + +#locale settings +lc_messages = 'en_US.utf8' # locale for system error message +lc_monetary = 'en_US.utf8' # locale for monetary formatting +lc_numeric = 'en_US.utf8' # locale for number formatting +lc_time = 'en_US.utf8' # locale for time formatting + +default_text_search_config = 'pg_catalog.english' + +#replication +wal_level = replica +archive_mode = on +archive_command = 'test ! -f /mnt/server/archive/%f && cp %p /mnt/server/archive/%f' +max_wal_senders = 3 \ No newline at end of file diff --git a/storage/databases/postgresql/3-replication/postgres-2/config/pg_hba.conf b/storage/databases/postgresql/3-replication/postgres-2/config/pg_hba.conf new file mode 100644 index 0000000..8a28f88 --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-2/config/pg_hba.conf @@ -0,0 +1,15 @@ +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +host all all all scram-sha-256 diff --git a/storage/databases/postgresql/3-replication/postgres-2/config/pg_ident.conf b/storage/databases/postgresql/3-replication/postgres-2/config/pg_ident.conf new file mode 100644 index 0000000..a5870e6 --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-2/config/pg_ident.conf @@ -0,0 +1,42 @@ +# PostgreSQL User Name Maps +# ========================= +# +# Refer to the PostgreSQL documentation, chapter "Client +# Authentication" for a complete description. A short synopsis +# follows. +# +# This file controls PostgreSQL user name mapping. It maps external +# user names to their corresponding PostgreSQL user names. Records +# are of the form: +# +# MAPNAME SYSTEM-USERNAME PG-USERNAME +# +# (The uppercase quantities must be replaced by actual values.) +# +# MAPNAME is the (otherwise freely chosen) map name that was used in +# pg_hba.conf. SYSTEM-USERNAME is the detected user name of the +# client. PG-USERNAME is the requested PostgreSQL user name. The +# existence of a record specifies that SYSTEM-USERNAME may connect as +# PG-USERNAME. +# +# If SYSTEM-USERNAME starts with a slash (/), it will be treated as a +# regular expression. Optionally this can contain a capture (a +# parenthesized subexpression). The substring matching the capture +# will be substituted for \1 (backslash-one) if present in +# PG-USERNAME. +# +# Multiple maps may be specified in this file and used by pg_hba.conf. +# +# No map names are defined in the default configuration. If all +# system user names and PostgreSQL user names are the same, you don't +# need anything in this file. +# +# This file is read on server startup and when the postmaster receives +# a SIGHUP signal. If you edit the file on a running system, you have +# to SIGHUP the postmaster for the changes to take effect. You can +# use "pg_ctl reload" to do that. + +# Put your actual configuration here +# ---------------------------------- + +# MAPNAME SYSTEM-USERNAME PG-USERNAME diff --git a/storage/databases/postgresql/3-replication/postgres-2/config/postgresql.conf b/storage/databases/postgresql/3-replication/postgres-2/config/postgresql.conf new file mode 100644 index 0000000..6b49304 --- /dev/null +++ b/storage/databases/postgresql/3-replication/postgres-2/config/postgresql.conf @@ -0,0 +1,27 @@ +# ----------------------------- +# PostgreSQL configuration file +# ----------------------------- +# + +data_directory = '/data' +hba_file = '/config/pg_hba.conf' +ident_file = '/config/pg_ident.conf' + +port = 5432 +listen_addresses = '*' +max_connections = 100 +shared_buffers = 128MB +dynamic_shared_memory_type = posix +max_wal_size = 1GB +min_wal_size = 80MB +log_timezone = 'Etc/UTC' +datestyle = 'iso, mdy' +timezone = 'Etc/UTC' + +#locale settings +lc_messages = 'en_US.utf8' # locale for system error message +lc_monetary = 'en_US.utf8' # locale for monetary formatting +lc_numeric = 'en_US.utf8' # locale for number formatting +lc_time = 'en_US.utf8' # locale for time formatting + +default_text_search_config = 'pg_catalog.english' \ No newline at end of file diff --git a/storage/databases/postgresql/3-replication/summary.png b/storage/databases/postgresql/3-replication/summary.png new file mode 100644 index 0000000000000000000000000000000000000000..591a3598dcc21e1b327cc3a2a387394175d94aff GIT binary patch literal 30678 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