High Availability Deployment with Patroni
1. Overview
Streaming replication (see Cluster) keeps a standby in sync with the primary, but failover is manual: a human must promote the standby after the primary fails. A high-availability (HA) cluster removes the human from the loop.
Patroni is the de-facto standard HA controller for PostgreSQL-compatible databases. Each IvorySQL node runs a Patroni daemon; the daemons coordinate through a distributed configuration store (DCS) such as etcd. Only the node that holds the cluster lock acts as the primary; all others stay connected as streaming-standby replicas. When the primary’s Patroni loses its heartbeat, the remaining nodes elect a new leader and promote it automatically — typically within tens of seconds.
| Component | Purpose |
|---|---|
Patroni |
Cluster controller: starts/stops IvorySQL, manages roles, performs failover |
etcd (DCS) |
Consensus store for cluster state and the leader lock |
IvorySQL |
The database; each node is a full IvorySQL instance with streaming replication |
Everything in this guide was verified on IvorySQL 5beta1 (PostgreSQL 19beta1) with Patroni 4.1.5 and etcd 3.5.
All command outputs below are captured verbatim from the verification environment (both nodes ran
on a single host, hence the localhost addresses in the outputs).
2. Prerequisites
-
IvorySQL installed from source or packages, with
pg_ctl,initdbandpg_basebackupavailable in the installationbindirectory -
Python 3 and
pip(for Patroni) -
etcd
-
Network ports planned per node:
| Port (example) | Usage |
|---|---|
5432 / 5433 |
PostgreSQL-protocol listener of node1 / node2 |
15432 / 15433 |
Oracle-compatibility listener ( |
8008 / 8009 |
Patroni REST API of node1 / node2 |
2379 |
etcd client port |
|
Each IvorySQL instance listens on two ports: |
3. Deployment
3.1. Install Patroni
sudo apt install etcd-server etcd-client python3-pip pip3 install 'patroni[etcd3]' psycopg2-binary
3.2. Start etcd
For a lab environment a single-node etcd is sufficient; production deployments should use a three-node etcd cluster.
etcd \ --data-dir /data/etcd \ --listen-client-urls http://192.168.1.10:2379 \ --advertise-client-urls http://192.168.1.10:2379 \ --listen-peer-urls http://192.168.1.10:2380 \ --initial-advertise-peer-urls http://192.168.1.10:2380 \ --initial-cluster node1=http://192.168.1.10:2380 & etcdctl endpoint health --endpoints=192.168.1.10:2379
3.3. Configure the Patroni Nodes
Create /etc/patroni/node1.yml on the first node:
namespace: /ivorysql-ha/
scope: ivory_cluster
name: node1
restapi:
listen: 192.168.1.10:8008
etcd3:
hosts: 192.168.1.10:2379
bootstrap:
method: initdb
initdb:
- encoding: UTF8
- auth: trust
dcs:
postgresql:
parameters:
max_wal_senders: 10
hot_standby: 'true'
postgresql:
listen: 192.168.1.10:5432
bin_dir: /usr/local/ivorysql/bin
data_dir: /data/ivorysql/node1
parameters:
port: 5432
ivorysql.port: 15432
ivorysql.listen_addresses: '192.168.1.10'
unix_socket_directories: '/var/run/ivorysql/node1'
pg_hba:
- local all all trust
- host all all 192.168.1.0/24 scram-sha-256
- host replication replicator 192.168.1.0/24 scram-sha-256
authentication:
superuser:
username: postgres
password: <superuser-password>
replication:
username: replicator
password: <replication-password>
Create /etc/patroni/node2.yml on the second node with the same structure, changing: name: node2, the REST listen port (8009), listen: 192.168.1.11:5433, port: 5433, ivorysql.port: 15433, and data_dir.
|
3.4. Start the Cluster
# node1 bootstraps the cluster and becomes the leader patroni /etc/patroni/node1.yml & # node2 clones the leader via pg_basebackup and joins as a replica patroni /etc/patroni/node2.yml &
3.5. Verify
$ patronictl -c /etc/patroni/node1.yml list + Cluster: ivory_cluster (7700535459881935048) -+----+-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +--------+----------------+---------+-----------+----+-------------+-----+------------+-----+ | n1 | localhost:5432 | Leader | running | 1 | | | | | | n2 | localhost:5433 | Replica | streaming | 1 | 0/4000118 | 0 | 0/4000118 | 0 | +--------+----------------+---------+-----------+----+-------------+-----+------------+-----+
Write on the leader and read from the replica:
$ psql -h 192.168.1.10 -p 5432 -U postgres -d postgres \
-c "CREATE TABLE ha_demo (id int primary key, note text); INSERT INTO ha_demo VALUES (1,'written on leader');"
$ psql -h 192.168.1.11 -p 5433 -U postgres -d postgres -tAc \
"SELECT note FROM ha_demo WHERE id=1;"
written on leader
Replication view on the leader:
$ psql -h 192.168.1.10 -p 5432 -U postgres -d postgres -tAc \
"SELECT application_name, state, sync_state FROM pg_stat_replication;"
node2 | streaming | async
Oracle compatible features work through the cluster as usual:
$ psql -h 192.168.1.10 -p 5432 -U postgres -d postgres -tAc \
"SELECT sys.last_day('2024-02-10'::sys.oradate);"
2024-02-29
4. Failover
4.1. Automatic Failover
Stop the Patroni process on the leader (simulating a host failure). The surviving node acquires the lock and promotes itself; the timeline increases, which proves a genuine failover:
# on node1's host pkill -f 'patroni.*node1.yml' # ~40 seconds later, on any node $ patronictl -c /etc/patroni/node2.yml list + Cluster: ivory_cluster (7700535459881935048) ---+-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +--------+----------------+--------+---------+----+-------------+-----+------------+-----+ | n2 | localhost:5433 | Leader | running | 2 | | | | | +--------+----------------+--------+---------+----+-------------+-----+------------+-----+
With asynchronous replication a failover does not by itself guarantee zero data loss: transactions committed on the old primary but not yet replicated can be lost. In this drill nothing was lost, because writes had already ceased and all committed transactions were replicated; the new leader accepts writes immediately:
$ psql -h 192.168.1.11 -p 5433 -U postgres -d postgres -tAc \
"INSERT INTO ha_demo VALUES (3,'after failover'); SELECT count(*) FROM ha_demo;"
3
|
The cluster above runs with asynchronous replication (the default, as shown by
|
4.2. Node Rejoin
Restart Patroni on the failed node. It notices the higher timeline, follows it and rejoins automatically as a replica:
$ patroni /etc/patroni/node1.yml & $ patronictl -c /etc/patroni/node1.yml list + Cluster: ivory_cluster (7700535459881935048) -+----+-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +--------+----------------+---------+-----------+----+-------------+-----+------------+-----+ | n1 | localhost:5432 | Replica | streaming | 2 | 0/403D688 | 0 | 0/403D688 | 0 | | n2 | localhost:5433 | Leader | running | 2 | | | | | +--------+----------------+---------+-----------+----+-------------+-----+------------+-----+
|
The rejoin above is a plain timeline follow, which works because the failed node had not accepted
any writes of its own while it was down. If the old primary committed transactions that were never
replicated before the failover, its timeline has diverged and Patroni needs |
5. Monitoring and Maintenance
| Command | Purpose |
|---|---|
|
Cluster topology, roles, timelines, replication lag |
|
Restart a member (rolling restarts for maintenance) |
|
Reload Patroni configuration |
|
Remove a permanently retired member |
REST health probe (for load balancers / monitoring) |
|
Returns 200 only on a healthy replica (for read-only routing) |
Monitoring essentials:
-
Watch
patronictl listoutput: any member that is notrunning/streamingneeds attention. -
The REST API exposes
/health(any member),/master(200 only on the leader) and/replicafor HTTP checks; combine/masterwith a floating IP or HAProxy for automatic connection routing. -
PostgreSQL logs of each node record promotion events (
redo done at …,selected new timeline). -
etcd health is a cluster-wide single point of failure in single-node setups — use three etcd nodes in production.
6. Production Checklist
The deployment above runs all daemons as background shell processes, which is convenient for learning but is not how a production HA cluster should run:
-
Supervise every daemon. Run etcd, Patroni (and optionally a local HAProxy) as systemd services so they come back after crashes or reboots:
# /etc/systemd/system/patroni@.service
[Unit]
Description=Patroni IvorySQL %i
After=network.target etcd.service
[Service]
User=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/%i.yml
Restart=always
[Install]
WantedBy=multi-user.target
-
Enable the Patroni watchdog. Set
watchdog: mode: requiredso a node that loses quorum cannot keep serving as the primary. -
Put a load balancer in front. HAProxy with the Patroni REST health checks gives clients a single endpoint that always routes to the current primary:
listen ivorysql_primary
bind *:5000
option httpchk GET /master
http-check expect status 200
default-server inter 3s fall 3 rise 2
server node1 192.168.1.10:5432 check port 8008
server node2 192.168.1.11:5433 check port 8009
-
Secure the control plane. The Patroni REST API and etcd should use TLS and authentication (
restapi: verify_client,etcd3: cacert/cert/key) in shared networks. -
Use three etcd nodes. A single-node etcd is itself a single point of failure.
7. Troubleshooting
| Symptom | Cause and fix |
|---|---|
Replica fails to start: |
Two nodes share the default Oracle-mode port. Set a unique |
Patroni crashes at startup: |
|
|
Data directory permissions are not |
|
IvorySQL installed in a custom prefix. Register the library path once:
|