• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Disaster Recovery 101: Architecting Auto-Failovers for PostgreSQL and Perl Deployments on Linode

Disaster Recovery 101: Architecting Auto-Failovers for PostgreSQL and Perl Deployments on Linode

Establishing a High-Availability PostgreSQL Cluster with Patroni

For critical applications, a single PostgreSQL instance is a single point of failure. Architecting for high availability (HA) necessitates a robust failover mechanism. Patroni, a template for PostgreSQL HA, leverages distributed configuration stores like etcd, Consul, or ZooKeeper to manage cluster state and orchestrate automated failovers. This section details setting up a Patroni-managed PostgreSQL cluster on Linode, focusing on etcd for state management.

We’ll deploy three Linode instances. One will host etcd, and the other two will run PostgreSQL nodes managed by Patroni. For simplicity, we’ll use Debian 11 (Bullseye) on all instances.

1. Setting up etcd Cluster

A highly available etcd cluster is paramount. For a minimal setup, three etcd nodes are recommended. For this example, we’ll consolidate etcd onto a single Linode instance for simplicity, acknowledging that a production deployment would distribute etcd across multiple nodes for its own HA. Install etcd on your designated Linode:

First, add the etcd repository and install the package:

curl -L https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
echo "deb https://packages.cloud.google.com/apt/ cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
apt-get update
apt-get install etcd -y

Next, configure etcd. The default configuration is often sufficient for initial testing, but for production, you’ll want to secure it with TLS and configure peer discovery. For this example, we’ll use a basic configuration. Ensure the etcd service is enabled and started:

systemctl enable etcd
systemctl start etcd

Verify etcd is running and accessible:

etcdctl member list

If you are running etcd on a separate node, ensure its firewall allows traffic on ports 2379 (client API) and 2380 (peer API).

2. Installing and Configuring Patroni

Patroni is typically installed via pip. Ensure you have Python 3 and pip installed on your PostgreSQL nodes.

apt-get update
apt-get install python3-pip python3-dev build-essential libpq-dev postgresql-server-dev-13 -y
pip3 install --upgrade pip
pip3 install "patroni[etcd]" psycopg2-binary

Now, create a Patroni configuration file (e.g., /etc/patroni/patroni.yml) on each PostgreSQL node. This configuration tells Patroni how to connect to etcd, how to manage PostgreSQL, and replication settings.

# /etc/patroni/patroni.yml
scope: my_pg_cluster
namespace: /service/ # Optional: prefix for keys in etcd

restapi:
  listen: 0.0.0.0:8008
  connect_address: &connect_address :8008

etcd:
  host: :2379
  protocol: http # Use https for TLS secured etcd

# PostgreSQL configuration
postgresql:
  listen: 0.0.0.0:5432
  connect_address: *connect_address
  data_dir: /var/lib/postgresql/13/main
  pg_hba:
    - host replication replicator 0.0.0.0/0 md5
    - host all all 0.0.0.0/0 md5
  replication:
    username: replicator
    password: your_replication_password
    ssl: false
  parameters:
    max_connections: 100
    shared_buffers: 256MB
    wal_level: replica
    hot_standby: "on"
    max_wal_senders: 10
    max_replication_slots: 10

# Tags for node identification
tags:
  nofailover: false
  clonefrom: false

# Bootstrap configuration (only for the initial primary)
bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576 # 1MB
    postgresql:
      use_pg_rewind: true
      parameters:
        recovery_target_timeline: "latest"
        hot_standby: "on"
        max_connections: 100
        shared_buffers: 256MB
        wal_level: replica
        max_wal_senders: 10
        max_replication_slots: 10
      replication_slots:
        - name: patroni_slot_1
          database: template1
          plugin: pgoutput
        - name: patroni_slot_2
          database: template1
          plugin: pgoutput
      users:
        replicator:
          password: your_replication_password
          options:
            - REPLICATION
        admin:
          password: your_admin_password
          options:
            - SUPERUSER
            - CREATEDB
            - CREATEROLE
            - REPLICATION
            - LOGIN

# Patroni REST API configuration
# This is for the Patroni API itself, not the PostgreSQL API
# If you want to expose Patroni API to other services, configure it here
# api:
#   listen: 0.0.0.0:8008
#   ssl: false

# Logging configuration
log:
  level: INFO
  dir: /var/log/patroni
  file: patroni.log

Replace <IP_ADDRESS_OF_THIS_NODE> with the actual IP address of the Linode instance where this configuration is being applied. Also, replace <ETCD_NODE_IP> with the IP address of your etcd node. Ensure the PostgreSQL data directory (/var/lib/postgresql/13/main) exists and has the correct ownership (typically postgres:postgres).

Create the log directory and set permissions:

mkdir -p /var/log/patroni
chown -R postgres:postgres /var/log/patroni

Now, create a systemd service file for Patroni (e.g., /etc/systemd/system/patroni.service):

[Unit]
Description=Patroni PostgreSQL High-Availability Manager
After=network.target

[Service]
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
Restart=on-failure
KillMode=process
TimeoutSec=30

[Install]
WantedBy=multi-user.target

Enable and start the Patroni service on all PostgreSQL nodes:

systemctl daemon-reload
systemctl enable patroni
systemctl start patroni

Monitor the Patroni logs (/var/log/patroni/patroni.log) for any errors. The first node to start will bootstrap the cluster, initializing PostgreSQL and creating the necessary replication user and slots. Subsequent nodes will join as replicas.

3. Verifying Cluster Status and Failover

You can check the cluster status by querying the Patroni REST API. On any node, you can use curl:

curl http://localhost:8008/cluster

This will output JSON detailing the cluster state, including the current primary and replicas. To test failover, you can stop the Patroni service on the current primary node:

systemctl stop patroni

Within a short period (determined by the ttl and loop_wait in your patroni.yml), Patroni will detect the primary’s absence, elect a new primary from the available replicas, and reconfigure the remaining replicas. You can observe this process in the logs of the remaining nodes. After the failover, query the cluster status again to confirm the new primary.

For applications connecting to PostgreSQL, it’s crucial to use a mechanism that can dynamically discover the current primary. This can be achieved by querying the Patroni API or by using a load balancer that integrates with Patroni (e.g., HAProxy with a script to check Patroni’s API).

Integrating Perl Applications with Auto-Failover PostgreSQL

Connecting Perl applications to a highly available PostgreSQL cluster requires a strategy to handle primary failovers. Hardcoding the primary’s IP address is a recipe for disaster. Instead, applications should be designed to query for the current primary’s endpoint dynamically.

1. Dynamic Connection String Resolution

The most straightforward approach is to have your Perl application query the Patroni REST API to determine the current primary’s IP address and port. This can be done within your application’s connection logic or via a wrapper script.

Here’s a Perl script snippet demonstrating how to fetch the primary’s connection details from Patroni:

use strict;
use warnings;
use LWP::UserAgent;
use JSON;

sub get_pg_primary_connection {
    my ($patroni_api_url) = @_;

    my $ua = LWP::UserAgent->new;
    $ua->timeout(10); # Set a reasonable timeout

    my $response = $ua->get($patroni_api_url . '/cluster');

    if ($response->is_success) {
        my $json_data = $response->decoded_content;
        my $cluster_info = decode_json($json_data);

        if ($cluster_info && $cluster_info->{primary}) {
            my $primary_node = $cluster_info->{primary};
            my $host = $primary_node->{host};
            my $port = $primary_node->{port};
            my $dbname = 'your_database'; # Replace with your database name
            my $user = 'your_user';       # Replace with your database user
            my $password = 'your_password'; # Replace with your database password

            # Construct DSN (Data Source Name)
            return "dbi:Pg:host=$host;port=$port;dbname=$dbname", $user, $password;
        } else {
            warn "Could not find primary node in Patroni cluster info.\n";
            return undef, undef, undef;
        }
    } else {
        warn "Failed to fetch cluster info from Patroni API: " . $response->status_line . "\n";
        return undef, undef, undef;
    }
}

# --- Usage Example ---
my $patroni_base_url = 'http://localhost:8008'; # Or the IP of a Patroni API endpoint

my ($dsn, $user, $password) = get_pg_primary_connection($patroni_base_url);

if ($dsn) {
    print "Successfully retrieved connection details:\n";
    print "DSN: $dsn\n";
    # Now you can use this DSN to connect to PostgreSQL
    # Example using DBI:
    # use DBI;
    # my $dbh = DBI->connect($dsn, $user, $password, { RaiseError => 1, AutoCommit => 1 });
    # print "Connected to PostgreSQL!\n";
    # $dbh->disconnect;
} else {
    print "Failed to get PostgreSQL primary connection details.\n";
}

This function get_pg_primary_connection takes the base URL of the Patroni REST API, queries the /cluster endpoint, parses the JSON response, and extracts the host and port of the current primary. It then constructs a DBI DSN string.

2. Implementing Retry Logic and Connection Pooling

Even with dynamic endpoint resolution, network glitches or brief moments during failover can cause connection failures. Robust Perl applications should implement retry logic for database connections.

use strict;
use warnings;
use DBI;
use LWP::UserAgent;
use JSON;
use constant MAX_RETRIES => 5;
use constant RETRY_DELAY => 5; # seconds

# (Include the get_pg_primary_connection function from above)

sub connect_to_postgres {
    my ($patroni_api_url, $db_name, $db_user, $db_pass) = @_;
    my $dbh = undef;
    my $attempt = 0;

    while ($attempt < MAX_RETRIES) {
        my ($dsn, $user, $password) = get_pg_primary_connection($patroni_api_url);

        if ($dsn) {
            eval {
                $dbh = DBI->connect($dsn, $user, $password, {
                    RaiseError => 1,
                    AutoCommit => 1,
                    PrintError => 0, # Let RaiseError handle it
                    pg_enable_utf8 => 1,
                });
            };

            if ($@) {
                # Connection failed
                warn "Attempt $attempt: Failed to connect to PostgreSQL: $@\n";
                $dbh = undef; # Ensure $dbh is undef on failure
            } else {
                # Connection successful
                print "Successfully connected to PostgreSQL!\n";
                return $dbh;
            }
        } else {
            warn "Attempt $attempt: Could not resolve PostgreSQL primary.\n";
        }

        $attempt++;
        if ($attempt < MAX_RETRIES) {
            print "Retrying in " . RETRY_DELAY . " seconds...\n";
            sleep(RETRY_DELAY);
        }
    }

    die "Failed to connect to PostgreSQL after $MAX_RETRIES attempts.\n";
}

# --- Usage Example ---
my $patroni_base_url = 'http://localhost:8008';
my $db_name = 'your_database';
my $db_user = 'your_user';
my $db_pass = 'your_password';

my $dbh = connect_to_postgres($patroni_base_url, $db_name, $db_user, $db_pass);

# Now you can use $dbh for database operations
# Example:
# my $sth = $dbh->prepare("SELECT version()");
# $sth->execute;
# my ($version) = $sth->fetchrow_array;
# print "PostgreSQL Version: $version\n";

# $dbh->disconnect;

This enhanced connect_to_postgres function wraps the connection attempt in a loop, retrying up to MAX_RETRIES times with a delay of RETRY_DELAY seconds between attempts. This significantly improves resilience during transient network issues or during the brief window of a failover event.

For applications with high connection churn or that need to maintain consistent performance, consider implementing connection pooling. Libraries like DBI::Pool can be used, but ensure your pooling strategy correctly invalidates and re-establishes connections when the primary changes. A common pattern is to re-resolve the primary and obtain a new connection from the pool when a query fails due to a connection error.

3. Application-Level Considerations

Beyond connection management, your Perl application should be designed to tolerate brief periods of database unavailability. This might involve:

  • Implementing idempotent operations where possible, so retrying a failed write operation doesn’t cause duplicate data.
  • Queuing writes if the database is temporarily unavailable, to be replayed once connectivity is restored.
  • Gracefully degrading functionality rather than crashing entirely when database access is lost.

By combining Patroni for automated PostgreSQL failover with intelligent connection handling in your Perl applications, you can build a resilient and highly available system on Linode.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Migrating Legacy WordPress to Headless with Laravel: A Performance and Security Deep Dive
  • Leveraging PHP 8’s JIT Compiler and Vector APIs for Extreme Web Application Performance
  • Leveraging PHP 8 JIT and AWS Lambda for High-Performance, Serverless WordPress REST API Backends
  • Beyond the Basics: Leveraging PHP 8.3’s JIT Compiler and Fibers for High-Concurrency Laravel Applications
  • Zero-Downtime Deployments with Docker, Laravel, and AWS ECS: A Deep Dive into Blue/Green Strategies

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (664)
  • Desktop Applications (14)
  • DevOps (11)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (6)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (15)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (10)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (650)
  • SEO & Growth (492)
  • Server (118)
  • Softwares (1)
  • Ubuntu (9)
  • Uncategorized (19)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (25)
  • WordPress Plugin Development (728)
  • WordPress Theme Development (357)

Recent Posts

  • Migrating Legacy WordPress to Headless with Laravel: A Performance and Security Deep Dive
  • Leveraging PHP 8's JIT Compiler and Vector APIs for Extreme Web Application Performance
  • Leveraging PHP 8 JIT and AWS Lambda for High-Performance, Serverless WordPress REST API Backends

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (728)
  • Debugging & Troubleshooting (664)
  • Security & Compliance (650)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala