Compilación de Postgresql con la extensión de Spock para cluster Multi-master con dos nodos

· 20 min · linux

Introducción

En este artículo, vamos a explicar cómo compilar Postgresql con la extensión de Spock para crear un cluster Multi-master. Esta configuración es útil para aplicaciones que requieren alta disponibilidad y escalabilidad.

Requisitos previos

Para empezar deberías contar dos máquinas con Ubuntu 24.04 Server por ejemplo. nodo1: 192.168.1.91 nodo2: 192.168.1.92

Repositorio de Scripts de instalación

Repositorio de Scripts

Primeros pasos en ambos nodos

Fichero config.sh

#!/bin/bash

PG_VERSION="17.5"
PG_VERSION_BASE="17"
SRC_DIR="/usr/local/src/postgres17-spock"
INSTALL_PREFIX="/usr/local/pgsql"
DATA_DIR="${INSTALL_PREFIX}/data"
NUM_CORES=$(nproc)
PG_CTL="${INSTALL_PREFIX}/bin/pg_ctl"
PGDATA="${DATA_DIR}"
SERVICE_FILE="/etc/systemd/system/postgresql.service"

Este fichero nos permite definir las variables que vamos a utilizar en el script de compilación e instalación.

Fichero install_postgres_spock.sh

source ./config.sh

set -euo pipefail

install_dependencies() {
    echo "Instalando dependencias..."
    sudo apt update
    sudo apt install -y make build-essential libssl-dev zlib1g-dev libreadline-dev \
        libxml2-dev libxslt-dev libkrb5-dev flex bison libxml2-utils xsltproc \
        ccache pkg-config git libjansson-dev
}

# /usr/local/src/
prepare_source_dir() {
    echo "Preparando directorio de código fuente..."
    sudo mkdir -p "$SRC_DIR"
    sudo chown "$(whoami)" "$SRC_DIR"
    cd "$SRC_DIR"
}

download_postgres() {
    if [ ! -f "postgresql-${PG_VERSION}.tar.gz" ]; then
        echo "Descargando PostgreSQL ${PG_VERSION}..."
        wget "https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.gz"
    fi
}

extract_sources() {
    echo "Extrayendo PostgreSQL..."
    tar -xvzf "postgresql-${PG_VERSION}.tar.gz"
}

clone_spock() {
    if [ ! -d "spock" ]; then
        echo "Clonando Spock..."
        git clone https://github.com/pgEdge/spock
    fi
}

apply_patches() {
    echo "Aplicando parches de Spock..."
    cd "${SRC_DIR}/postgresql-${PG_VERSION}"
    for patch in ../spock/patches/pg${PG_VERSION_BASE}-*.diff; do
        echo "Aplicando $(basename "$patch")"
        patch -p1 < "$patch"
    done
}

configure_postgres() {
    echo "Configurando compilación..."
    ./configure --prefix="$INSTALL_PREFIX" \
                --with-readline --with-zlib --with-icu --with-openssl --with-libxml
}

build_and_install_postgres() {
    echo "Compilando PostgreSQL..."
    make -j"$NUM_CORES"
    sudo make install
}
configure_path() {
    echo "Añadiendo ${INSTALL_PREFIX}/bin al PATH del sistema..."
    sudo tee /etc/profile.d/pgsql.sh >/dev/null <<EOF
#!/bin/sh
export PATH=${INSTALL_PREFIX}/bin:\$PATH
EOF
    sudo chmod +x /etc/profile.d/pgsql.sh
    source /etc/profile.d/pgsql.sh
}

create_postgres_user() {
    if ! id postgres >/dev/null 2>&1; then
        echo "Creando usuario postgres..."
        sudo adduser --system --home /var/lib/postgresql --group --shell /bin/bash postgres
    fi
}

# configurar directorio de datos de postgres
setup_data_dir() {
    echo "Creando directorio de datos..."
    sudo mkdir -p "$DATA_DIR"
    sudo chown postgres:postgres "$DATA_DIR"
}

# inicializar el clúster
init_db_cluster() {
    echo "Inicializando base de datos..."
    sudo -u postgres "${INSTALL_PREFIX}/bin/initdb" -D "$DATA_DIR"
}

install_spock() {
    echo "Compilando e instalando Spock..."
    cd "${SRC_DIR}/spock"
    cp compat${PG_VERSION_BASE}/* .
    env PATH="${INSTALL_PREFIX}/bin:$PATH" make -j"$NUM_CORES"
    sudo env PATH="${INSTALL_PREFIX}/bin:$PATH" make install
}

# configurar postgresql.conf para Spock
# borra y añade al final
configure_spock() {
    echo "🛠️ Configurando postgresql.conf para Spock..."
    CONF_FILE="${DATA_DIR}/postgresql.conf"
    sudo sed -i "/^#*shared_preload_libraries *=/d" "$CONF_FILE"
    sudo sed -i "/^#*track_commit_timestamp *=/d" "$CONF_FILE"
    sudo sed -i "/^#*wal_level *=/d" "$CONF_FILE"
    sudo sed -i "/^#*max_worker_processes *=/d" "$CONF_FILE"
    sudo sed -i "/^#*max_replication_slots *=/d" "$CONF_FILE"
    sudo sed -i "/^#*max_wal_senders *=/d" "$CONF_FILE"
    {
        echo ""
        echo "# Configuración necesaria para Spock"
        echo "shared_preload_libraries = 'spock'"
        echo "track_commit_timestamp = on"
        echo "wal_level = 'logical'"
        echo "max_worker_processes = 10  # one per database needed on provider node"
        echo ""
        echo "max_replication_slots = 10  # one per node needed on provider node"
        echo "max_wal_senders = 10        # one per node needed on provider node"
    } | sudo tee -a "$CONF_FILE" >/dev/null
}

# resumen
show_summary() {
    echo "✅ PostgreSQL ${PG_VERSION} y Spock compilados e instalados correctamente."
    echo "🔧 PostgreSQL instalado en: $INSTALL_PREFIX"
    echo "📁 Datos en: $DATA_DIR"
}

main() {
    install_dependencies
    prepare_source_dir
    download_postgres
    extract_sources
    clone_spock
    apply_patches
    configure_postgres
    build_and_install_postgres
    configure_path
    create_postgres_user
    setup_data_dir
    init_db_cluster
    install_spock
    configure_spock
    show_summary
}

main

Como vemos aquí definimos una serie de funciones que nos permiten realizar la instalación de Postgresql y Spock. Veamos un poco el paso a paso de lo que hace el script:

  1. Instalación de dependencias: Se instalan las dependencias necesarias para compilar Postgresql y Spock.
  2. Preparación del directorio de código fuente: Se crea el directorio donde se va a descargar y compilar Postgresql.
  3. Descarga de Postgresql: Se descarga la versión especificada de Postgresql.
  4. Extracción de fuentes: Se extraen los archivos de Postgresql.
  5. Clonación de Spock: Se clona el repositorio de Spock desde GitHub.
  6. Aplicación de parches: Se aplican los parches necesarios para integrar Spock con Postgresql.
  7. Configuración de Postgresql: Se configura la compilación de Postgresql con las opciones necesarias.
  8. Compilación e instalación de Postgresql: Se compila e instala Postgresql en el directorio especificado.
  9. Configuración del PATH: Se añade el directorio de Postgresql al PATH del sistema para que sea accesible.
  10. Creación del usuario Postgres: Se crea el usuario postgres si no existe.
  11. Configuración del directorio de datos: Se crea el directorio de datos para Postgresql y se le asigna el propietario correcto.
  12. Inicialización del clúster de base de datos: Se inicializa el clúster de base de datos en el directorio de datos.
  13. Instalación de Spock: Se compila e instala Spock en el directorio de Postgresql.
  14. Configuración de Spock en postgresql.conf: Se configura el archivo postgresql.conf para habilitar Spock y sus parámetros necesarios.
  15. Resumen: Se muestra un resumen de la instalación y configuración realizada.

Lanzamiento del script de compilación e instalación

Para lanzar el script, primero debemos darle permisos de ejecución:

chmod +x config.sh
chmod +x install_postgres_spock.sh
./instal_postgres_spock.sh

fichero setup_postgres_service.sh

Para configurar Postgresql podemos ejecutar el siguiente script llamado setup_postgres_service.sh:

#!/bin/bash

# variables de configuración
source ./config.sh

set -euo pipefail

# servicio systemd
create_service_file() {
    echo "Creando archivo de servicio systemd..."
    sudo tee "$SERVICE_FILE" > /dev/null <<EOF
[Unit]
Description=PostgreSQL database server
After=network.target

[Service]
Type=forking
User=postgres
Group=postgres

ExecStart=${PG_CTL} start -D ${PGDATA} -l ${PGDATA}/logfile
ExecStop=${PG_CTL} stop -D ${PGDATA}
ExecReload=${PG_CTL} reload -D ${PGDATA}

Environment=PATH=${INSTALL_PREFIX}/bin:/usr/bin:/bin
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
}

enable_service() {
    echo "Recargando systemd y habilitando el servicio..."
    sudo systemctl daemon-reexec
    sudo systemctl daemon-reload
    sudo systemctl enable postgresql
    sudo systemctl start postgresql
}

main() {
    create_service_file
    enable_service
    echo "✅ Servicio PostgreSQL configurado y iniciado."
}

main

Este script crea un archivo de servicio para Postgresql en systemd, lo habilita y lo inicia.

Lanzamiento del script de configuración del servicio

Para lanzar el script, primero debemos darle permisos de ejecución:

chmod +x setup_postgres_service.sh
./setup_postgres_service.sh

Verificación del lanzamiento del servicio

Para verificar que el servicio se ha iniciado correctamente, podemos ejecutar el siguiente comando:

systemctl status postgresql

Acceso a la consola de Postgresql

Para acceder a la consola de Postgresql, nos cambiamos al usuario postgres:

sudo -iu postgres

Y luego ejecutamos el comando:

psql -h localhost -p 5432 -U postgres

Si ha ido todo bien, deberíamos ver el prompt de Postgresql y poder ejecutar comandos SQL. Como por ejemplo, podemos crear una base de datos de prueba:

CREATE DATABASE testdb;

o listar las bases de datos existentes:

\l

Ficheros de configuración

Los ficheros de configuración de Postgresql se encuentran en el directorio de datos que hemos especificado en el script de instalación. Por defecto, este directorio es /usr/local/pgsql/data. Los ficheros más importantes son:

Configuración de Spock

Como vemos en el fichero install_postgres_spock.sh, hemos configurado Spock en el archivo postgresql.conf añadiendo las siguientes líneas:

# Add settings for extensions here

# Configuración necesaria para Spock
shared_preload_libraries = 'spock'
track_commit_timestamp = on
wal_level = 'logical'
max_worker_processes = 10  # one per database needed on provider node

max_replication_slots = 10  # one per node needed on provider node
max_wal_senders = 10        # one per node needed on provider node

Esto habilita Spock y configura los parámetros necesarios para su funcionamiento.

Configuración del cluster Multi-master

Para configurar un cluster Multi-master con Spock, debemos realizar los siguientes pasos en ambos nodos:

  1. Configurar el archivo postgresql.conf: Asegurarnos de que el puerto y la dirección IP están configurados correctamente. Por ejemplo:
listen_addresses = '*'

O bien expecificar la IP del nodo 1:

listen_addresses = '192.168.1.91'

O bien expecificar la IP del nodo 2:

listen_addresses = '192.168.1.92'
  1. Configurar el archivo pg_hba.conf: Permitir conexiones entre los nodos en ambos nodos. Por ejemplo:
# Permitir conexiones desde la ip del nodo 1 y la ip del nodo 2
host    all             all             192.168.1.91/32         trust
host    all             all             192.168.1.92/32         trust
  1. Reiniciar el servidor::
sudo systemctl restart postgresql
  1. Comprobar que podemos conectarnos a la IP del nodo 1 del servidor del nodo 1 o el nodo 2:
psql -h 192.168.1.91 -p 5432 -U postgres
  1. Comprobar que podemos conectarnos a la IP del nodo 2 del servidor del nodo 1 o el nodo 2:
psql -h 192.168.1.92 -p 5432 -U postgres
  1. Crear un usuario de replicación: En ambos nodos, creamos un usuario de replicación que se utilizará para la replicación entre los nodos:
-- Crea el usuario con SUPERUSER y REPLICATION
CREATE ROLE replicator
  WITH LOGIN
  SUPERUSER
  ENCRYPTED PASSWORD 'TuPasswordSegura';
  1. Crear una misma base de datos en ambos nodos:
CREATE DATABASE testdb;
  1. Comprobar que se ha creado correctamente el usuario en ambos nodos: En ambos nodos, debemos verificar que se ha creado correctamente el usuario. Esto se hace ejecutando el siguiente comando en la consola de Postgresql:
\du
  1. Comprobar que tenemos conectividad con el usuario replicator a ambos nodos:
psql -h 192.168.1.91 -p 5432 -U replicator -W testdb

Debería pedirnos la contraseña y conectarnos correctamente a la base de datos testdb en el nodo 1. Recuerda que la contraseña que hemos definido es TuPasswordSegura.

Ahora comprobamos el nodo 2:

psql -h 192.168.1.92 -p 5432 -U replicator -W testdb

Debería pedirnos la contraseña y conectarnos correctamente a la base de datos testdb en el nodo 2. 8. Crea la extensión de Spock en la bbdd creada en ambos nodos: Primero conextamos a la base de datos testdb:

\c testdb

Para luego crear la extensión de Spock en esa base de datos:

CREATE EXTENSION spock;
  1. Registramos los nodos en Spock: Desde el nodo 1, ejecutamos el siguiente comando:
SELECT spock.node_create(
  'proveedor1',
  'host=192.168.1.91 user=replicator dbname=testdb'
);

Desde el nodo 2, ejecutamos el siguiente comando:

SELECT spock.node_create(
  'proveedor2',
  'host=192.168.1.92 user=replicator dbname=testdb'
);
  1. Comprobamos que los nodos se han registrado correctamente: Desde el nodo 1, ejecutamos el siguiente comando:
SELECT * FROM spock.node;

Desde el nodo 2, ejecutamos el siguiente comando:

SELECT * FROM spock.node;

Deberíamos ver ambos nodos registrados correctamente en la tabla spock.node. 11. Creamos una tabla de ejemplo: Desde el nodo 1, ejecutamos el siguiente comando:

-- Crear tabla de ejemplo
CREATE TABLE public.clientes (
    id SERIAL PRIMARY KEY,
    nombre TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    creado_en TIMESTAMP DEFAULT now()
);

-- Insertar datos de prueba
INSERT INTO public.clientes (nombre, email)
VALUES
    ('Ana', 'ana@example.com'),
    ('Juan', 'juan@example.com');
  1. Comprobamos que la tabla se ha creado correctamente: Desde el nodo 1, ejecutamos el siguiente comando:
SELECT * FROM public.clientes;
  1. Configurar el conjunto de replicación: Desde el nodo 1, ejecutamos el siguiente comando:
-- Añadir tabla al conjunto de replicación
SELECT spock.repset_add_table(
    set_name := 'default',
    relation := 'public.clientes',
    synchronize_data := true
);

O bien incluir toda la base de datos:

-- Alternativa: replicar todas las tablas del esquema public
SELECT spock.repset_add_all_tables('default', ARRAY['public']);
  1. Crear la subscripción desde el nodo 2: Desde el nodo 2, ejecutamos el siguiente comando:
-- Crear suscripción con sincronización completa
SELECT spock.sub_create(
    subscription_name := 'sub_2_to_1',
    provider_dsn := 'host=192.168.1.91 user=replicator dbname=testdb',
    replication_sets := ARRAY['default', 'ddl_sql'],
    synchronize_structure := true,  -- Por defecto viene a false
    synchronize_data := true
);
  1. Comprobar el estado de la suscripción: Desde el nodo 2, ejecutamos el siguiente comando:
SELECT spock.sub_wait_for_sync('sub_2_to_1');

Ahora comprobamos que la subscripción existe en su tabla:

SELECT * FROM spock.sub_show_status();

Si todo ha ido bien, deberíamos ver que la suscripción se ha sincronizado correctamente y que los datos de la tabla clientes se han replicado al nodo 2. 16. Comprobar que los datos se han replicado correctamente: Desde el nodo 2, ejecutamos el siguiente comando:

SELECT * FROM public.clientes;

Y deberían aparecer los mismos datos que insertamos en el nodo 1:

testdb=# SELECT * FROM public.clientes;
 id | nombre |      email       |         creado_en
----+--------+------------------+---------------------------
  1 | Ana    | ana@example.com  | 2025-06-22 19:04:52.09408
  2 | Juan   | juan@example.com | 2025-06-22 19:04:52.09408
(2 rows)
  1. Configurar la subscripción desde el nodo 1 al nodo 2: Desde el nodo 1, ejecutamos el siguiente comando:
-- Crear suscripción con sincronización completa
SELECT spock.sub_create(
    subscription_name := 'sub_1_to_2',
    provider_dsn := 'host=192.168.1.92 user=replicator dbname=testdb',
    replication_sets := ARRAY['default', 'ddl_sql'],
    synchronize_structure := true,  -- Por defecto viene a false
    synchronize_data := true
);
  1. Comprobar el estado de la suscripción: Desde el nodo 1, ejecutamos el siguiente comando:
SELECT spock.sub_wait_for_sync('sub_1_to_2');

Ahora comprobamos que la subscripción existe en su tabla:

SELECT * FROM spock.sub_show_status();
  1. Comprobar que replica desde el nodo 2 al nodo 1: Desde el nodo 2, ejecutamos el siguiente comando:
\c testdb
INSERT INTO public.clientes (nombre, email)
VALUES ('Marta', 'marta@example.com');
SELECT * FROM public.clientes;
  1. Comprobar que los datos se han replicado correctamente al nodo 1: Desde el nodo 1, ejecutamos el siguiente comando:
\c testdb
select * from public.clientes;

Y deberían aparecer los mismos datos que insertamos en el nodo 2.

Limitaciones

Conclusión

En este artículo hemos visto cómo compilar Postgresql con la extensión de Spock para crear un cluster Multi-master. Hemos configurado dos nodos y hemos verificado que la replicación funciona correctamente entre ellos. Esta configuración es útil para aplicaciones que requieren alta disponibilidad y escalabilidad, permitiendo que ambos nodos puedan recibir escrituras y lecturas de forma simultánea.