Building PostgreSQL HA the Quick and Clean Way

2025-09-15 10:09:34 - No Comments

This video demonstrates how to set up a highly available PostgreSQL cluster using Patroni, ETCD, and HAProxy on Debian, covering installation, configuration, environment variables, database initialization, user setup, security rules, and Patroni management commands for failover and switchover, while also showcasing read/write splitting with HAProxy for improved scalability.

Patroni installation

First we install all the dependencies for HAProxy, Patroni and PostgreSQL.

sudo apt update 
sudo apt install -y postgresql-15 postgresql-contrib-15 patroni python3-pysyncobj haproxy etcd-server etcd-client python3-etcd python3-etcd3

Environment variables for creation

Next we setup some environment variables we can later use to create configuration files.

export NODE_NAME=`hostname -f`
export NODE_IP=`hostname -i | awk '{print $1}'`
export PG_DATA_DIR="/var/lib/postgresql/15/main"
export PG_BIN_DIR="/usr/lib/postgresql/15/bin"
export PG_CONFIG_DIR="/etc/postgresql/15/main"
export CLUSTER="cluster_1"

export ETCDCTL_API=3 
export ETCD_NAME=`hostname`
export HOST_1=192.168.1.1
export HOST_2=192.168.1.2
export HOST_3=192.168.1.3
export ENDPOINTS=$HOST_1:2379,$HOST_2:2379,$HOST_3:2379

ETCD config

Echo a new ETCD configuration file for this particular host. Do that for all the hosts in your ETCD cluster.

echo "
ETCD_NAME="${ETCD_NAME}"
ETCD_DATA_DIR="/var/lib/etcd/mycluster"
ETCD_INITIAL_CLUSTER="per1=http://${HOST_1}:2380,per2=http://${HOST_2}:2380,per3=http://${HOST_3}:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="patroni_cluster_1"
ETCD_ADVERTISE_CLIENT_URLS="http://${NODE_IP}:2379"
ETCD_LISTEN_PEER_URLS="http://${NODE_IP}:2380"
ETCD_LISTEN_CLIENT_URLS="http://${NODE_IP}:2379,http://localhost:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://${NODE_IP}:2380"
" | sudo tee /etc/default/etcd

Somewhat quickly start all the services in your cluster so they can find each other and bootstrap the system.

sudo systemctl enable --now etcd
sudo systemctl restart etcd
sudo journalctl -fu etcd

Next we need to check that the cluster is available, all nodes are visible and one of them have been chosen as leader.

sudo etcdctl --endpoints=$ENDPOINTS -w table member list
sudo etcdctl --endpoints=$ENDPOINTS -w table endpoint status

PostgreSQL configuration

We need to create a couple of accounts for our server and set passwords so Patroni intern can do important replication and rewinding tasks.

sudo -u postgres psql
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
ALTER ROLE postgres WITH LOGIN PASSWORD 'qwerty';
CREATE USER replicator WITH LOGIN PASSWORD 'replPasswd';
ALTER ROLE replicator WITH REPLICATION;
CREATE USER rewind WITH LOGIN PASSWORD 'rewindPasswd';

PostgreSQL allowed access

Setting up which networks we allow access from to this server. This is mostly required for servers that we want to keep the database. If we let Patroni bootstrap it the configuration values in the Patroni config file is used.

sudo vi /etc/postgresql/15/main/pg_hba.conf
local    all               all                          scram-sha-256
host     replication       replicator    127.0.0.1/32   trust
host     replication       replicator    192.168.0.0/16 scram-sha-256
host     all               all           0.0.0.0/0      scram-sha-256

PostgreSQL stopping

Stopping and disabling PostgreSQL service, Patroni vill start PostgreSQL for you so we don't want the systemd service blocking that process.

sudo systemctl stop postgresql
sudo systemctl disable postgresql

Patroni config

This small script will echo a configuration into a file using our previously defined environment variables. This script contains passwords that of course needs changing if used in a production environment.

echo "
scope: ${CLUSTER}
name: ${NODE_NAME}

restapi:
  listen: 0.0.0.0:8008
  connect_address: ${NODE_IP}:8008

etcd3: 
  host: ${NODE_IP}:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: on
        wal_log_hints: on
        logging_collector: on
        max_wal_size: "10GB"        
  initdb:
    - encoding: UTF8
    - data-checksums
  pg_hba:
    - local all all scram-sha-256
    - host replication replicator 127.0.0.1/32 trust 
    - host replication replicator 192.168.0.0/16 scram-sha-256 
    - host all all 0.0.0.0/0 scram-sha-256

postgresql:
  listen: 0.0.0.0:5432
  connect_address: ${NODE_IP}:5432
  data_dir: ${PG_DATA_DIR}
  bin_dir: ${PG_BIN_DIR}
  config_dir: ${PG_CONFIG_DIR}
  authentication:
    superuser: { username: postgres, password: 'qwerty' }
    replication: { username: replicator, password: 'replPasswd' }  # change me
    rewind: { username: rewind, password: 'rewindPasswd' }  # change me
  parameters:
    unix_socket_directories: '/var/run/postgresql'
    password_encryption: 'scram-sha-256'

watchdog:
  mode: 'off' # Allowed values: off, automatic, required 
  device: /dev/watchdog 
  safety_margin: 5

" | sudo tee /etc/patroni/config.yml

Run a quick validation on your configuration to fix eventual issues that will occur 🙂

sudo -u postgres patroni --validate-config /etc/patroni/config.yml

If you bootstrap the database

In some cases you don't care about the data or some nodes will replicate others, in that case removing the old database and let Patroni bootstrap a database is the best approach.

sudo rm -rf /var/lib/postgresql/15/main/

Bringing up nodes

Now we start Patroni one host at a time. Ensuring that it starts, finds and bootstraps the database correctly without any problem.

sudo systemctl enable --now patroni
sudo systemctl restart patroni
sudo journalctl -fu patroni

After starting each host we can check the status by listing the members of the cluster.

patronictl -c /etc/patroni/config.yml list $CLUSTER

HAProxy

Setting up HAProxy for primary and standby databases so we can send write and read workloads to different ports and always be sure that we are talking to the right databases.

sudo vi /etc/haproxy/haproxy.cfg

This configuration will setup a status page on port 7000 where you can see which databases are in standby mode an which is the primary node that you need to use for writing.

listen stats
    mode http
    bind *:7000
    stats enable
    stats uri /

listen primary
    bind *:5000
    mode tcp
    option tcplog
    option httpchk /primary 
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server per1 per1:5432 maxconn 100 check port 8008
    server per2 per2:5432 maxconn 100 check port 8008
    server per3 per3:5432 maxconn 100 check port 8008

listen standbys
    balance roundrobin
    bind *:5001
    mode tcp
    option tcplog
    option httpchk /replica 
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server per1 per1:5432 maxconn 100 check port 8008
    server per2 per2:5432 maxconn 100 check port 8008
    server per3 per3:5432 maxconn 100 check port 8008

Check the configuration file for any issues and then reload your configuration.

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

Testing connection

Now you should be able to connect to your HAProxy on either port 5000 for writing/reading tasks and 5001 for reading tasks.

psql -U postgres -h localhost -p 5000
psql -U postgres -h localhost -p 5001

Troubleshooting

In this section you will find some commands that you can use to trouble shoot problems or reset your cluster in order to start over.

If you need to reset you can remove cluster and data directory.

patronictl -c /etc/patroni/config.yml remove $CLUSTER
sudo rm -rf /var/lib/postgresql/15/main/

These are commands for PostgreSQL to see if you have a read only database, if it's in recovery mode. Promote it to primary and reloading configuration.

sudo -u postgres psql

SHOW transaction_read_only;
SELECT pg_is_in_recovery();

SELECT pg_promote();

SELECT pg_reload_conf();

Be the first to leave a comment!


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.