This project demonstrates read load balancing for PostgreSQL using Pgpool-II and Docker Compose. It distributes SELECT queries across multiple streaming replicas while directing all write operations to the primary server.
Note: This setup focuses on read scaling and connection pooling. It does not include automatic primary failover (promoting a replica to primary).
βββββββββββββββββββ
β Application β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Pgpool-II β β Port 5433 (Load Balancer)
β (Connection β
β Pooler + β
β Read LB) β
ββββββββββ¬βββββββββ
β
βββββββββββββββββββββΌββββββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
β pg-primary β β pg-replica1 β β pg-replica2 β
β (Write Only) β β (Read Only) β β (Read Only) β
β weight = 0 β β weight = 1 β β weight = 1 β
βββββββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ
- βοΈ Read Load Balancing: SELECT queries distributed across replicas (primary has weight 0)
- π Streaming Replication: WAL-based replication using physical replication slots
- π₯ Health Check: Backend health monitoring every 1 second
- π Replication Lag Detection: Replicas with lag > 1 second are excluded from load balancing
- π Auto Failback: Recovered replicas automatically re-added to the read pool
- π Connection Pooling: 64 child processes, 2 connections per backend each (max 384 connections)
- π³ Custom Dockerfile: Full control over Pgpool configuration
| Container | Image | Description |
|---|---|---|
| pg-primary | postgres:17 |
Official PostgreSQL 17 image (primary) |
| pg-replica1 | postgres:17 |
Official PostgreSQL 17 image (streaming replica) |
| pg-replica2 | postgres:17 |
Official PostgreSQL 17 image (streaming replica) |
| pgpool-read | Custom build | postgres:17-alpine + pgpool (ARM64/AMD64 compatible) |
- Official
pgpool/pgpool: Only AMD64, not ARM64 compatible (fails on M1/M2 Macs) - Bitnami
bitnami/pgpool: Discontinued from Docker Hub (Aug 2025) - Our solution: Custom Dockerfile based on
postgres:15-alpinewith pgpool package
FROM postgres:17-alpine
RUN apk add --no-cache pgpool pgpool-openrc bash
COPY pgpool.conf pool_hba.conf pool_passwd pcp.conf /etc/pgpool-II/
EXPOSE 5432 9898This provides:
- Full control over
pgpool.conf - ARM64 & AMD64 compatibility
- No dependency on third-party images
- Docker & Docker Compose
- psql client (for test scripts)
docker-compose up -dThis starts:
pg-primary- Primary PostgreSQL (Port 5432)pg-replica1- Streaming replicapg-replica2- Streaming replicapgpool-read- Custom-built Pgpool-II load balancer (Port 5433)
PGPASSWORD=secret psql -h localhost -p 5433 -U postgres -d appdb -c "SHOW POOL_NODES"# Via Pgpool (recommended)
PGPASSWORD=secret psql -h localhost -p 5433 -U postgres -d appdb
# Direct to Primary
PGPASSWORD=secret psql -h localhost -p 5432 -U postgres -d appdb| Script | Description |
|---|---|
./monitor_load_balancing.sh |
Real-time query distribution monitor |
./monitor_connections.sh |
Backend connection stats (idle/active) |
./check_lag.sh |
Detailed replication lag info |
./check_replication.sh |
Full replication test |
./check_pgpool_nodes.sh |
Pgpool node status |
./test_failover_timing.sh |
Failover detection timing |
./test_artificial_lag.sh |
Test lag detection on one replica |
./test_replica_down.sh |
Test replica down scenario |
./test_both_replicas_down.sh |
Test all replicas down |
./test_both_replicas_lag.sh |
Test lag on all replicas |
| Parameter | Value | Description |
|---|---|---|
num_init_children |
64 | Max concurrent client connections |
max_pool |
2 | Connections per backend per child |
connection_life_time |
600 | Connection lifetime (10 min) |
health_check_period |
1 | Health check interval (seconds) |
health_check_max_retries |
1 | Retries before marking down |
sr_check_period |
1 | Replication lag check interval |
delay_threshold_by_time |
1000 | Max allowed lag (1 second = 1000ms) |
prefer_lower_delay_standby |
on | Prefer replica with lower lag |
auto_failback |
on | Auto re-add recovered replicas |
Primary has weight 0 = read queries only go to replicas:
backend_weight0 = 0 # pg-primary (write only)
backend_weight1 = 1 # pg-replica1 (read)
backend_weight2 = 1 # pg-replica2 (read)
pgpool/
βββ docker-compose.yml # Container orchestration
βββ README.md
β
βββ pg-primary/ # Primary PostgreSQL
β βββ init.sql # Replication user, slots, test table
β βββ 01_hba.sh # pg_hba.conf settings
β βββ postgresql.conf
β
βββ replica/ # Replica configuration
β βββ init_replica.sh # Base backup and replication setup
β βββ postgresql.conf
β βββ recovery.conf
β
βββ pgpool/ # Custom Pgpool-Read build
β βββ Dockerfile # Custom ARM64-compatible image
β βββ entrypoint.sh # Startup script
β βββ pgpool.conf # Full configuration
β βββ pool_hba.conf # Client authentication
β βββ pool_passwd # User passwords
β βββ pcp.conf # PCP admin config
β
βββ scripts (root)
βββ monitor_load_balancing.sh # Real-time LB monitor
βββ monitor_connections.sh # Connection stats monitor
βββ check_lag.sh # Replication lag check
βββ check_replication.sh # Full replication test
βββ check_pgpool_nodes.sh # Pool nodes status
βββ test_failover_timing.sh # Failover timing test
βββ test_artificial_lag.sh # Lag detection test
βββ test_replica_down.sh # Replica down test
βββ test_both_replicas_down.sh # All replicas down test
βββ test_both_replicas_lag.sh # All replicas lag test
docker-compose logs -f pgpool-read
docker-compose logs -f pg-primarydocker-compose down -v
docker-compose up -d --builddocker exec pg-primary psql -U postgres -c "SELECT * FROM pg_replication_slots;"This project is intended for educational and development purposes.