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 0000000..591a359 Binary files /dev/null and b/storage/databases/postgresql/3-replication/summary.png differ