Implementación de entorno de alta disponibilidad para PostgreSQL con Patroni

Arquitectura de alta disponibilidad basada en Patroni para PostgreSQL

Introducción

PostgreSQL es una base de datos de código abierto cuyas características, rendimiento y confiabilidad pueden competir con bases de datos comerciales maduras a nivel internacional. Además, su licencia y ecosistema son completamente abiertos, sin control por parte de una única empresa o país, garantizando que los usuarios no tengan preocupaciones futuras. Cada vez más empresas nacionales están adoptando PostgreSQL para reemplazar costosas bases de datos comerciales extranjeras.

Cuando se implementa PostgreSQL en entornos de producción, elegir una solución adecuada de alta disponibilidad es un trabajo indispensable. Este documento presenta el método de despliegue de alta disponibilidad para PostgreSQL basado en Patroni como referencia.

Existen múltiples herramientas de alta disponibilidad de código abierto para PostgreSQL, entre las cuales las siguientes son relativamente comunes:

  • PAF (PostgreSQL Automatic Failover)
  • repmgr
  • Patroni

Para comparaciones detalladas, consultar: https://scalegrid.io/blog/managing-high-availability-in-postgresql-part-1/

Entre ellas, Patroni destaca por ser tanto fácil de usar como potente en funcionalidades:

  • Soporte para failover automático y switchover bajo demanda
  • Compatibilidad con uno o múltiples nodos esclavos
  • Replicación en cascada
  • Replicación síncrona y asíncrona
  • Degradación automática de replicación síncrona a asíncrona cuando fallan los esclavos (similar a la replicación semisíncrona de MySQL pero más inteligente)
  • Control sobre qué nodos participan en elecciones, balanceo de carga y pueden ser esclavos síncronos
  • Reparación automática del antiguo maestro mediante pg_rewind
  • Múltiples métodos para inicializar clústeres y reconstruir esclavos, incluyendo pg_basebackup y scripts personalizados compatibles con herramientas como wal_e, pgBackRest, barman
  • Scripts callback externos personalizables
  • API REST
  • Prevención de split-brain mediante watchdog
  • Despliegue en entornos contenedorizados como k8s y docker
  • Soporte para múltiples almacenes DCS (Distributed Configuration Store) comunes, incluyendo etcd, ZooKeeper, Consul, Kubernetes

Por lo tanto, salvo en casos donde solo existan 2 máquinas sin recursos adicionales para desplegar DCS, Patroni es una herramienta de alta disponibilidad altamente recomendable para PostgreSQL. A continuación se detallarán los pasos para construir un entorno de alta disponibilidad PostgreSQL basado en Patroni.

Ambiente experimental

Software principle:

  • CentOS 7.8
  • PostgreSQL 12
  • Patroni 1.6.5
  • etcd 3.3.25

Máquinas y recursos VIP:

  • PostgreSQL
    • nodo1: 192.168.234.201
    • nodo2: 192.168.234.202
    • nodo3: 192.168.234.203
  • etcd
    • nodo4: 192.168.234.204
  • VIP
    • VIP lectura/escritura: 192.168.234.210
    • VIP solo lectura: 192.168.234.211

Preparación del ambiente:

Sincronización de reloj en todos los nodos:

yum install -y ntpdate
ntpdate time.windows.com && hwclock -w

Si se utiliza firewall, se deben abrir los puertos de postgres, etcd y patroni:

  • postgres: 5432
  • patroni: 8008
  • etcd: 2379/2380

O simplemente desactivar el firewall:

setenforce 0
sed -i.bak "s/SELINUX=enforcing/SELINUX=permissive/g" /etc/selinux/config
systemctl disable firewalld.service
systemctl stop firewalld.service
iptables -F

Despliegue de etcd

Dado que este documento no se centra en la alta disponibilidad de etcd, solo se desplegará un nodo único de etcd en nodo4 para pruebas. En entornos de producción se requieren al menos 3 nodos, que pueden ser máquinas independientes o compartidas con la base de datos. Pasos de despliegue:

Instalar paquetes necesarios:

yum install -y gcc python-devel epel-release

Instalar etcd:

yum install -y etcd

Editar archivo de configuración de etcd /etc/etcd/etcd.conf, ejemplo de configuración:

ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_PEER_URLS="http://192.168.234.204:2380"
ETCD_LISTEN_CLIENT_URLS="http://localhost:2379,http://192.168.234.204:2379"
ETCD_NAME="etcd0"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.234.204:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.234.204:2379"
ETCD_INITIAL_CLUSTER="etcd0=http://192.168.234.204:2380"
ETCD_INITIAL_CLUSTER_TOKEN="cluster1"
ETCD_INITIAL_CLUSTER_STATE="new"

Iniciar etcd:

systemctl start etcd

Habilitar inicio automático de etcd:

systemctl enable etcd

Despliegue de alta disponibilidad PostgreSQL + Patroni

Instalar software relevante en instancias donde se ejecutará PostgreSQL:

Instalar PostgreSQL 12:

yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm

yum install -y postgresql12-server postgresql12-contrib

Instalar Patroni:

yum install -y gcc epel-release
yum install -y python-pip python-psycopg2 python-devel

pip install --upgrade pip
pip install --upgrade setuptools
pip install patroni[etcd]

Crear directorio de datos de PostgreSQL:

mkdir -p /pgsql/data
chown postgres:postgres -R /pgsql
chmod -R 700 /pgsql/data

Crear archivo de configuración del servicio Patroni /etc/systemd/system/patroni.service:

[Unit]
Description=Orquestador para alta disponibilidad de PostgreSQL
After=syslog.target network.target

[Service]
Type=simple
User=postgres
Group=postgres
#StandardOutput=syslog
ExecStart=/usr/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no

[Install]
WantedBy=multi-user.target

Crear archivo de configuración de Patroni /etc/patroni.yml, ejemplo de configuración para nodo1:

scope: pgsql
namespace: /service/
name: pg1

restapi:
  listen: 0.0.0.0:8008
  connect_address: 192.168.234.201:8008

etcd:
  host: 192.168.234.204:2379

bootstrap:
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    master_start_timeout: 300
    synchronous_mode: false
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        listen_addresses: "0.0.0.0"
        port: 5432
        wal_level: logical
        hot_standby: "on"
        wal_keep_segments: 100
        max_wal_senders: 10
        max_replication_slots: 10
        wal_log_hints: "on"

  initdb:
  - encoding: UTF8
  - locale: C
  - lc-ctype: zh_CN.UTF-8
  - data-checksums

  pg_hba:
  - host replication repl 0.0.0.0/0 md5
  - host all all 0.0.0.0/0 md5

postgresql:
  listen: 0.0.0.0:5432
  connect_address: 192.168.234.201:5432
  data_dir: /pgsql/data
  bin_dir: /usr/pgsql-12/bin

  authentication:
    replication:
      username: repl
      password: "123456"
    superuser:
      username: postgres
      password: "123456"

  basebackup:
    max-rate: 100M
    checkpoint: fast

tags:
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false

Los parámetros completos pueden referirse a YAML Configuration Settings en el manual de Patroni, donde los parámetros de PostgreSQL pueden complementarse según sea necesario.

Para otros nodos PG, el patroni.yml debe modificar estos 3 parámetros:

  • name
    • Configurar pg1~pg4 respectivamente para nodo1~nodo4
  • restapi.connect_address
    • Configurar según la IP de cada nodo
  • postgresql.connect_address
    • Configurar según la IP de cada nodo

Iniciar Patroni:

Primero iniciar Patroni en nodo1:

systemctl start patroni

Al iniciar Patroni por primera vez, creará automáticamente la instancia de PostgreSQL y los usuarios:

[root@nodo1 ~]# systemctl status patroni
● patroni.service - Orquestador para alta disponibilidad de PostgreSQL
   Loaded: loaded (/etc/systemd/system/patroni.service; disabled; vendor preset: disabled)
   Active: active (running) since Sat 2020-09-05 14:41:03 CST; 38min ago
 Main PID: 1673 (patroni)
   CGroup: /system.slice/patroni.service
           ├─1673 /usr/bin/python2 /usr/bin/patroni /etc/patroni.yml
           ├─1717 /usr/pgsql-12/bin/postgres -D /pgsql/data --config-file=/pgsql/data/postgresql.conf --listen_addresses=0.0.0.0 --max_worker_processe...
           ├─1719 postgres: pgsql: logger
           ├─1724 postgres: pgsql: checkpointer
           ├─1725 postgres: pgsql: background writer
           ├─1726 postgres: pgsql: walwriter
           ├─1727 postgres: pgsql: autovacuum launcher
           ├─1728 postgres: pgsql: stats collector
           ├─1729 postgres: pgsql: logical replication launcher
           └─1732 postgres: pgsql: postgres postgres 127.0.0.1(37154) idle

Luego iniciar Patroni en nodo2. nodo2 se unirá al clúster como réplica, copiando datos automáticamente del líder y estableciendo replicación:

[root@nodo2 ~]# systemctl status patroni
● patroni.service - Orquestador para alta disponibilidad de PostgreSQL
   Loaded: loaded (/etc/systemd/system/patroni.service; disabled; vendor preset: disabled)
   Active: active (running) since Sat 2020-09-05 16:09:06 CST; 3min 41s ago
 Main PID: 1882 (patroni)
   CGroup: /system.slice/patroni.service
           ├─1882 /usr/bin/python2 /usr/bin/patroni /etc/patroni.yml
           ├─1898 /usr/pgsql-12/bin/postgres -D /pgsql/data --config-file=/pgsql/data/postgresql.conf --listen_addresses=0.0.0.0 --max_worker_processe...
           ├─1900 postgres: pgsql: logger
           ├─1901 postgres: pgsql: startup   recovering 000000010000000000000003
           ├─1902 postgres: pgsql: checkpointer
           ├─1903 postgres: pgsql: background writer
           ├─1904 postgres: pgsql: stats collector
           ├─1912 postgres: pgsql: postgres postgres 127.0.0.1(35924) idle
           └─1916 postgres: pgsql: walreceiver   streaming 0/3000060

Ver estado del clúster:

[root@nodo2 ~]# patronictl -c /etc/patroni.yml list
+ Cluster: pgsql (6868912301204081018) -------+----+-----------+
| Member |       Host      |  Role  |  State  | TL | Lag in MB |
+--------+-----------------+--------+---------+----+-----------+
|  pg1   | 192.168.234.201 | Leader | running |  1 |           |
|  pg2   | 192.168.234.202 |        | running |  1 |       0.0 |
+--------+-----------------+--------+---------+----+-----------+

Para operaciones diarias, configurar variable de entorno global PATRONICTL_CONFIG_FILE:

echo 'export PATRONICTL_CONFIG_FILE=/etc/patroni.yml' >/etc/profile.d/patroni.sh

Agregar las siguientes variables de entorno a ~postgres/.bash_profile:

export PGDATA=/pgsql/data
export PATH=/usr/pgsql-12/bin:$PATH

Configurar privilegios sudo sin contraseña para postgres:

echo 'postgres        ALL=(ALL)       NOPASSWD: ALL'> /etc/sudoers.d/postgres

Conmutación automática y prevención de split-brain

Patroni ejecuta automáticamente failover cuando falla el servidor maestro para garantizar la alta disponibilidad del servicio. Sin embargo, si el failover automático no se controla adecuadamente, existe riesgo de split-brain. Por lo tanto, bajo objetivos duales de garantizar disponibilidad del servicio y prevenir split-brain, Patroni ejecuta ciertas acciones automatizadas en escenarios específicos.

Ubicación de fallo Escenario Acción de Patroni
Nodo esclavo Detención de PG esclavo Detener PG esclavo
Nodo esclavo Detención de Patroni esclavo Detener PG esclavo
Nodo esclavo Kill forzado de Patroni esclavo (o crash de Patroni) Sin acción
Nodo esclavo Esclavo no puede conectar a etcd Sin acción
Nodo esclavo No rol de Leader pero PG en modo producción Reiniciar PG y cambiar a modo recuperación como esclavo
Nodo maestro Detención de PG maestro Reiniciar PG, si excede master_start_timeout realizar cambio maestro-esclavo
Nodo maestro Detención de Patroni maestro Detener PG maestro y disparar failover
Nodo maestro Kill forzado de Patroni maestro (o crash de Patroni) Disparar failover, aparece "doble maestro"
Nodo maestro Maestro no puede conectar a etcd Degradar maestro a esclavo y disparar failover
- Fallo de clúster etcd Degradar maestro a esclavo, todo el clúster son esclavos
- Sin esclavos síncronos disponibles en modo síncrono Cambiar temporalmente maestro a replicación asíncrona, failover automático suspendido hasta restaurar modo síncrono

Cómo Patroni previene split-brain

El proceso patroni desplegado en nodos de base de datos ejecuta operaciones protectoras para evitar múltiples "maestros":

  1. Cuando nodos no líder tienen PG en modo producción, reinicia PG y cambia a modo recuperación como esclavo
  2. Cuando patroni líder no puede conectar a etcd, no puede asegurar ser aún líder, degrada PG local a esclavo
  3. Al detener patroni normalmente, también detiene proceso PG local

Sin embargo, cuando el proceso patroni mismo no funciona correctamente, estas medidas protectoras son difíciles de aplicar. Por ejemplo, terminación anormal del proceso patroni o hang temporal del host.

Para prevenir split-brain de manera más confiable, Patroni soporta monitoreo del proceso patroni mediante watchdog de Linux. Cuando el proceso patroni no puede escribir latidos al dispositivo watchdog, el watchdog dispara reinicio de Linux. Método de configuración:

Configurar archivo de servicio systemd de Patroni /etc/systemd/system/patroni.service:

[Unit]
Description=Orquestador para alta disponibilidad de PostgreSQL
After=syslog.target network.target

[Service]
Type=simple
User=postgres
Group=postgres
#StandardOutput=syslog
ExecStartPre=-/usr/bin/sudo /sbin/modprobe softdog
ExecStartPre=-/usr/bin/sudo /bin/chown postgres /dev/watchdog
ExecStart=/usr/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no

[Install]
WantedBy=multi-user.target

Habilitar inicio automático de Patroni:

systemctl enable patroni

Modificar archivo de configuración de Patroni /etc/patroni.yml, agregar contenido:

watchdog:
  mode: automatic # Valores permitidos: off, automatic, required
  device: /dev/watchdog
  safety_margin: 5

safety_margin indica cuánto tiempo antes de expirar la clave líder el watchdog dispara reinicio si Patroni no actualiza watchdog oportunamente. Con esta configuración (ttl=30, loop_wait=10, safety_margin=5), el proceso patroni actualiza clave líder y watchdog cada 10 segundos (loop_wait). Si el nodo líder falla y patroni no puede actualizar watchdog oportunamente, se dispara reinicio 5 segundos antes de expirar clave líder. Si reinicio se completa en 5 segundos, nodo líder tiene oportunidad de obtener nuevamente bloqueo líder, de lo contrario clave líder expira y esclavo elegido mediante votación se convierte en nuevo líder.

Este mecanismo básicamente garantiza no aparecer "doble maestro", pero esta garantía depende de confiabilidad del watchdog. En práctica productiva esta garantía probablemente sea suficiente para mayoría de escenarios, pero teóricamente difícil probar 100% confiabilidad.

Por otro lado, ¿método de reinicio automático de máquina es demasiado agresivo causando "asesinatos accidentales"? Por ejemplo, debido a acceso repentino de negocio causando carga alta en máquina, proceso patroni no puede asignar recursos CPU oportunamente, en este caso reinicio automático de máquina puede no ser comportamiento esperado.

¿Entonces existen otros medios más confiables para prevenir split-brain?

Utilizar replicación síncrona de PostgreSQL para prevenir split-brain

Otro medio para prevenir split-brain es configurar clúster PostgreSQL en modo replicación síncrona. Utilizando característica de bloqueo de escritura en maestro cuando no hay esclavo síncrono respondiendo registros, se asegura internamente en base de datos que incluso apareciendo "doble maestro" no ocurra "doble escritura". Esta forma de prevenir split-brain es más confiable y segura, costo es ligera reducción de rendimiento comparado con replicación asíncrona. Método específico de configuración:

Al ejecutar Patroni inicialmente, en archivo de configuración de Patroni /etc/patroni.yml configurar modo síncrono:

synchronous_mode: true

Para Patroni ya desplegado, modificar configuración mediante comando patronictl:

patronictl edit-config -s 'synchronous_mode=true'

En esta configuración, si esclavo síncrono temporalmente no disponible, Patroni degrada modo replicación de maestro a asíncrono, garantizando no interrupción de servicio. Efecto similar a replicación semisíncrona de MySQL, pero comparado con MySQL usando tiempo límite fijo para controlar degradación replicación, esta forma es más inteligente y además tiene efecto preventivo contra split-brain.

En modo síncrono, solo esclavos síncronos tienen calificación para ser promovidos a maestros. Por lo tanto, si maestro se degrada a replicación asíncrona, debido a no existir esclavo síncrono como candidato maestro, failover no se dispara, evitando "doble maestro". Si maestro no se degrada a replicación asíncrona, incluso apareciendo "doble maestro", debido a antiguo maestro en modo replicación síncrona, datos no pueden escribirse, evitando "doble escritura".

Patroni controla cambio entre replicación síncrona y asíncrona ajustando dinámicamente parámetro synchronous_standby_names de PostgreSQL. Además, Patroni registra estado síncrono en etcd, garantizando consistencia de estado síncrono en clúster Patroni.

Ejemplo de metadatos normales en modo síncrono:

[root@nodo4 ~]# etcdctl get /service/cn/sync
{"leader":"pg1","sync_standby":"pg2"}

Metadatos cuando esclavo falla causando degradación temporal de maestro a replicación asíncrona:

[root@nodo4 ~]# etcdctl get /service/cn/sync
{"leader":"pg1","sync_standby":null}

Si clúster contiene más de 3 nodos, puede considerarse estrategia síncrona más estricta, prohibiendo Patroni degradar modo síncrono a asíncrono. Esto garantiza cualquier dato escrito exista al menos en 2 nodos. Negocios con requisitos extremos de seguridad de datos pueden adoptar esta forma:

synchronous_mode: true
synchronous_mode_strict: true

Si clúster contiene nodos de respaldo desastre remoto, puede configurar ese nodo para no participar en elecciones, no participar en balanceo de carga, ni ser esclavo síncrono:

tags:
    nofailover: true
    noloadbalance: true
    clonefrom: false
    nosync: true

Efecto de inaccesibilidad de etcd

Cuando Patroni no puede acceder a etcd, no puede confirmar rol propio. Para prevenir split-brain en este estado, si PG local es maestro, Patroni degrada PG a esclavo. Si todos nodos Patroni del clúster no pueden acceder a etcd, todo clúster será esclavo, negocio no podrá escribir datos. Esto requiere alta disponibilidad del clúster etcd, especialmente cuando usamos clúster etcd central para gestionar cientos o miles de clústeres PG.

Cuando usamos clúster etcd centralizado para gestionar muchos clústeres PG, para prevenir impacto severo de fallo de clúster etcd, puede considerarse configurar parámetro retry_timeout ultra grande, por ejemplo 10000 días, combinado con modo replicación síncrona para prevenir split-brain:

retry_timeout: 864000000
synchronous_mode: true

retry_timeout controla tiempo límite de reintento para operaciones DCS y PostgreSQL. Para operaciones que requieren reintento, Patroni tiene restricciones temporales y límites de número de reintentos. Para operaciones PostgreSQL, actualmente solo llamada a API REST GET /patroni se reintenta, y máximo 1 vez, por lo que aumentar retry_timeout no trae efectos secundarios adicionales.

Operaciones diarias

En mantenimiento diario puede controlar Patroni y PostgreSQL mediante comando patronictl, por ejemplo modificar parámetros PotgreSQL:

[postgres@nodo2 ~]$ patronictl --help
Usage: patronictl [OPTIONS] COMMAND [ARGS]...

Options:
  -c, --config-file TEXT  Archivo de configuración
  -d, --dcs TEXT          Usar este DCS
  -k, --insecure          Permitir conexiones a sitios SSL sin certificados
  --help                  Mostrar este mensaje y salir.

Commands:
  configure    Crear archivo de configuración
  dsn          Generar dsn para miembro proporcionado, por defecto dsn de...
  edit-config  Editar configuración de clúster
  failover     Failover a réplica
  flush        Descartar eventos programados (solo reinicios actualmente)
  history      Mostrar historial de failovers/switchovers
  list         Listar miembros Patroni para Patroni dado
  pause        Deshabilitar failover automático
  query        Consultar miembro PostgreSQL Patroni
  reinit       Reinicializar miembro de clúster
  reload       Recargar configuración de miembro de clúster
  remove       Eliminar clúster de DCS
  restart      Reiniciar miembro de clúster
  resume       Reanudar failover automático
  scaffold     Crear estructura para clúster en DCS
  show-config  Mostrar configuración de clúster
  switchover   Switchover a réplica
  version      Salida versión de comando patronictl o Patroni en ejecución...

Modificar parámetros PostgreSQL

Para modificar parámetros de nodos individuales, puede ejecutar comendo SQL ALTER SYSTEM SET ..., por ejemplo abrir temporalmente registro debug en algún nodo. Para parámetros que requieren configuración uniforme debe usar patronictl edit-config para garantizar consistencia global, por ejemplo modificar número máximo de conexiones:

patronictl edit-config -p 'max_connections=300'

Después de modificar número máximo de conexiones necesita reiniciar para tomar efecto, por lo que Patroni establece marca Pending restart en estados de nodos relacionados:

[postgres@nodo2 ~]$ patronictl list
+ Cluster: pgsql (6868912301204081018) -------+----+-----------+-----------------+
| Member |       Host      |  Role  |  State  | TL | Lag in MB | Pending restart |
+--------+-----------------+--------+---------+----+-----------+-----------------+
|  pg1   | 192.168.234.201 | Leader | running | 25 |           |        *        |
|  pg2   | 192.168.234.202 |        | running | 25 |       0.0 |        *        |
+--------+-----------------+--------+---------+----+-----------+-----------------+

Después de reiniciar todas instancias PG del clúster, parámetros toman efecto:

patronictl restart pgsql

Ver estado de nodos Patroni

Normalmente podemos ver estado de cada nodo mediante patronictl list. Pero si queremos ver información más detallada de estado de nodos, necesitamos llamar API REST. Por ejemplo, cuando bloque líder expira pero nodo vivo no puede convertirse en líder, ver información detallada de estado de nodos ayuda investigar causa:

curl -s http://127.0.0.1:8008/patroni | jq

Ejemplo de salida:

[root@nodo2 ~]# curl -s http://127.0.0.1:8008/patroni | jq
{
  "database_system_identifier": "6870146304839171063",
  "postmaster_start_time": "2020-09-13 09:56:06.359 CST",
  "timeline": 23,
  "cluster_unlocked": true,
  "watchdog_failed": true,
  "patroni": {
    "scope": "cn",
    "version": "1.6.5"
  },
  "state": "running",
  "role": "replica",
  "xlog": {
    "received_location": 201326752,
    "replayed_timestamp": null,
    "paused": false,
    "replayed_location": 201326752
  },
  "server_version": 120004
}

"watchdog_failed": true anterior representa uso de watchdog pero incapacidad de acceder dispositivo watchdog, ese nodo no puede ser promovido a líder.

Configuración de acceso cliente

Maestro de clúster HA es dinámico, cuando ocurre cambio maestro-esclavo, acceso cliente a base de datos también necesita conectar dinámicamente a nuevo maestro. Existen varios métodos comunes de implementación, introducidos a continuación:

  • URL múltiple
  • vip
  • haproxy

URL múltiple

Controladores pgjdbc y libpq pueden configurar múltiples IPs en cadena de conexión, controlador identifica roles maestro-esclavo de base de datos y conecta nodo apropiado.

JDBC

Función URL múltiple de JDBC es completa, soporta failover, separación lectura-escritura y balanceo de carga. Puede configurar diferentes estrategias de conexión mediante parámetros:

  • jdbc:postgresql://192.168.234.201:5432,192.168.234.202:5432,192.168.234.203:5432/postgres?targetServerType=primary Conectar a nodo maestro (realmente nodo escribible). Cuando aparece "doble maestro" o "multi-maestro", controlador conecta primer maestro disponible encontrado
  • jdbc:postgresql://192.168.234.201:5432,192.168.234.202:5432,192.168.234.203:5432/postgres?targetServerType=preferSecondary&loadBalanceHosts=true Priorizar conexión a nodos esclavos, conectar a maestro si no hay esclavos disponibles, conectar aleatoriamente a uno si hay múltiples esclavos disponibles
  • jdbc:postgresql://192.168.234.201:5432,192.168.234.202:5432,192.168.234.203:5432/postgres?targetServerType=any&loadBalanceHosts=true Conectar aleatoriamente a cualquier nodo disponible

libpq

Función URL múltiple de libpq es relativamente débil comparada con pgjdbc, solo soporta failover:

  • postgres://192.168.234.201:5432,192.168.234.202:5432,192.168.234.203:5432/postgres?target_session_attrs=read-write Conectar a nodo maestro (realmente nodo escribible)
  • postgres://192.168.234.201:5432,192.168.234.202:5432,192.168.234.203:5432/postgres?target_session_attrs=any Conectar a cualquier nodo disponible

Otros lenguajes basados en libpq también pueden soportar URL múltiple, por ejemplo python y php. Ejemplo de programa python usando URL múltiple para crear conexión:

import psycopg2

conn=psycopg2.connect("postgres://192.168.234.201:5432,192.168.234.202:5432/postgres?target_session_attrs=read-write&password=123456")

VIP (implementar migración VIP mediante scripts callback de Patroni)

Método URL múltiple es simple de desplegar, pero no todos lenguajes soportan controladores, y si base de datos tiene "doble maestro" accidental, clientes configurados con URL múltiple tienen mayor probabilidad de escribir simultáneamente en múltiples maestros, mientras clientes accediendo mediante VIP tienen protección adicional (este riesgo generalmente ocurre cuando componente HA de base de datos no protege bien, como introducido anteriormente, si configuramos modo síncrono de Patroni, básicamente no tenemos esta preocupación).

Patroni soporta configuración de scripts callback que se disparan en eventos específicos. Por lo tanto podemos configurar script callback que cargue dinámicamente VIP después de cambio maestro-esclavo.

Preparar script callback /pgsql/loadvip.sh para cargar VIP:

#!/bin/bash

VIP=192.168.234.210
GATEWAY=192.168.234.2
DEV=ens33

accion=$1
rol=$2
clúster=$3

log()
{
  echo "loadvip: $*"|logger
}

cargar_vip()
{
ip a|grep -w ${DEV}|grep -w ${VIP} >/dev/null
if [ $? -eq 0 ] ;then
  log "vip existe, omitir carga vip"
else
  sudo ip addr add ${VIP}/32 dev ${DEV} >/dev/null
  rc=$?
  if [ $rc -ne 0 ] ;then
    log "fallo añadir vip ${VIP} en dispositivo ${DEV} rc=$rc"
    exit 1
  fi

  log "añadido vip ${VIP} en dispositivo ${DEV}"

  arping -U -I ${DEV} -s ${VIP} ${GATEWAY} -c 5 >/dev/null
  rc=$?
  if [ $rc -ne 0 ] ;then
    log "fallo llamar arping a gateway ${GATEWAY} rc=$rc"
    exit 1
  fi

  log "llamado arping a gateway ${GATEWAY}"
fi
}

descargar_vip()
{
ip a|grep -w ${DEV}|grep -w ${VIP} >/dev/null
if [ $? -eq 0 ] ;then
  sudo ip addr del ${VIP}/32 dev ${DEV} >/dev/null
  rc=$?
  if [ $rc -ne 0 ] ;then
    log "fallo eliminar vip ${VIP} en dispositivo ${DEV} rc=$rc"
    exit 1
  fi

  log "eliminado vip ${VIP} en dispositivo ${DEV}"
else
  log "vip no existe, omitir eliminación vip"
fi
}

log "inicio loadvip args:'$*'"

case $accion in
  on_start|on_restart|on_role_change)
    case $rol in
      master)
        cargar_vip
        ;;
      replica)
        descargar_vip
        ;;
      *)
        log "rol incorrecto '$rol'"
        exit 1
        ;;
    esac
    ;;
  *)
    log "acción incorrecta '$accion'"
    exit 1
    ;;
esac

Modificar archivo de configuración de Patroni /etc/patroni.yml, configurar funciones callback:

postgresql:
...
  callbacks:
    on_start: /bin/bash /pgsql/loadvip.sh
    on_restart: /bin/bash /pgsql/loadvip.sh
    on_role_change: /bin/bash /pgsql/loadvip.sh

Después de modificar archivo de configuración de Patroni en todos nodos, recargar archivo de configuración de Patroni:

patronictl reload pgsql

Después de ejecutar switchover, puede verse migración de VIP:

/var/log/messages:

Sep  5 21:32:24 localvm postgres: loadvip: inicio loadvip args:'on_role_change master pgsql'
Sep  5 21:32:24 localvm systemd: Sesión c7 iniciada para usuario root.
Sep  5 21:32:24 localvm postgres: loadvip: añadido vip 192.168.234.210 en dispositivo ens33
Sep  5 21:32:25 localvm patroni: 2020-09-05 21:32:25,415 INFO: Dueño bloque: pg1; Soy pg1
Sep  5 21:32:25 localvm patroni: 2020-09-05 21:32:25,431 INFO: sin acción. soy líder con bloque
Sep  5 21:32:28 localvm postgres: loadvip: llamado arping a gateway 192.168.234.2

Nota: si se detiene directamente Patroni en maestro, script anterior no elimina VIP. Después de detener Patroni en maestro se dispara failover de esclavo convirtiéndose en nuevo maestro, en este momento ambas máquinas vieja y nueva tienen VIP, pero debido a que nuevo maestro ejecuta arping, generalmente no afecta acceso aplicación. A pesar de ello, operativamente se debe tener cuidado para evitarlo.

VIP (implementar migración VIP mediante keepalived)

Patroni proporciona API REST para verificacinoes de salud, puede devolver códigos de estado HTTP normales (200) y anormales según rol de nodo:

  • GET / o GET /leaderNodo en ejecución y rol líder
  • GET /replicaRol réplica en ejecución, sin etiqueta noloadbalance configurada
  • GET /read-onlySimilar a GET /replica, pero incluye nodo líder

Usando API REST, Patroni puede combinarse con componentes externos. Por ejemplo, puede configurar keepalived para vincular VIP dinámicamente en maestro o esclavo.

Para detalles de interfaz API REST de Patroni, referirse a Patroni REST API.

Ejemplo siguiente vincula VIP solo lectura (192.168.234.211) dinámicamente en nodo esclavo en clúster maestro-esclavo (nodo1 y nodo2), cuando nodo esclavo falla vincula VIP solo lectura en nodo maestro.

Instalar keepalived:

yum install -y keepalived

Preparar archivo de configuración de keepalived /etc/keepalived/keepalived.conf:

global_defs {
    router_id LVS_DEVEL
}
vrrp_script verificar_lider {
    script "/usr/bin/curl -s http://127.0.0.1:8008/leader -v 2>&1|grep '200 OK' >/dev/null"
    interval 2
    weight 10
}
vrrp_script verificar_replica {
    script "/usr/bin/curl -s http://127.0.0.1:8008/replica -v 2>&1|grep '200 OK' >/dev/null"
    interval 2
    weight 5
}
vrrp_script verificar_puede_leer {
    script "/usr/bin/curl -s http://127.0.0.1:8008/read-only -v 2>&1|grep '200 OK' >/dev/null"
    interval 2
    weight 10
}
vrrp_instance VI_1 {
    state BACKUP
    interface ens33
    virtual_router_id 211
    priority 100
    advert_int 1
    track_script {
        verificar_puede_leer
        verificar_replica
    }
    virtual_ipaddress {
       192.168.234.211
    }
}

Iniciar keepalived:

systemctl start keepalived

Método de configuración anterior también puede usarse para migración de vip lectura-escritura, solo necesita cambiar scripts en track_script a verificar_lider. Pero en perturbaciones de red u otras fallas temporales, VIP gestionado por keepalived tiende a migrar fácilmente, por lo que personalmente recomiendo más usar scripts callback de Patroni para vincular dinámicamente VIP lectura-escritura. Si hay múltiples esclavos, también puede configurar LVS en keepalived para balanceo de carga en todos esclavos, proceso no se expande.

haproxy

haproxy como proxy de servicio combinado con Patroni puede soportar convenientemente failover, separación lectura-escritura y balanceo de carga, también es solución demo de comunidad Patroni. Desventaja es que haproxy también consume recursos, todo tráfico de datos pasa por haproxy, habrá cierta pérdida de rendimiento.

Configuración siguiente ejemplo de acceso a clúster PG maestro-dos esclavos mediante haproxy.

Instalar haproxy:

yum install -y haproxy

Editar archivo de configuración de haproxy /etc/haproxy/haproxy.cfg:

global
    maxconn 100
    log     127.0.0.1 local2

defaults
    log global
    mode tcp
    retries 2
    timeout client 30m
    timeout connect 4s
    timeout server 30m
    timeout check 5s

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

listen pgsql
    bind *:5000
    option httpchk
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_192.168.234.201_5432 192.168.234.201:5432 maxconn 100 check port 8008
    server postgresql_192.168.234.202_5432 192.168.234.202:5432 maxconn 100 check port 8008
    server postgresql_192.168.234.203_5432 192.168.234.203:5432 maxconn 100 check port 8008

listen pgsql_read
    bind *:6000
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
    server postgresql_192.168.234.201_5432 192.168.234.201:5432 maxconn 100 check port 8008
    server postgresql_192.168.234.202_5432 192.168.234.202:5432 maxconn 100 check port 8008
    server postgresql_192.168.234.203_5432 192.168.234.203:5432 maxconn 100 check port 8008

Si solo hay 2 nodos, GET /replica anterior necesita cambiarse a GET /read-only, de lo contrario cuando esclavo falla no podrá proporcionar acceso solo lectura, pero esta configuración hace que maestro también participe en lectura, no separando completamente carga lectura de maestro.

Iniciar haproxy:

systemctl start haproxy

haproxy mismo también necesita alta disponibilidad, puede desplegar haproxy en 2 máquinas nodo1 y nodo2, controlar migración de VIP (192.168.234.210) en nodo1 y nodo2 mediante keepalived.

Preparar archivo de configuración de keepalived /etc/keepalived/keepalived.conf:

global_defs {
    router_id LVS_DEVEL
}
vrrp_script verificar_haproxy {
    script "pgrep -x haproxy"
    interval 2
    weight 10
}
vrrp_instance VI_1 {
    state BACKUP
    interface ens33
    virtual_router_id 210
    priority 100
    advert_int 1
    track_script {
        verificar_haproxy
    }
    virtual_ipaddress {
       192.168.234.210
    }
}

Iniciar keepalived:

systemctl start keepalived

Prueba simple siguiente. Desde nodo4 acceder a PG mediante puerto 5000 de haproxy, se conectará a maestro:

[postgres@nodo4 ~]$ psql "host=192.168.234.210 port=5000 password=123456" -c 'select inet_server_addr(),pg_is_in_recovery()'
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
 192.168.234.201  | f
(1 fila)

Acceder a PG mediante puerto 6000 de haproxy, conectará cíclicamente a 2 esclavos:

[postgres@nodo4 ~]$ psql "host=192.168.234.210 port=6000 password=123456" -c 'select inet_server_addr(),pg_is_in_recovery()'
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
 192.168.234.202  | t
(1 fila)


[postgres@nodo4 ~]$ psql "host=192.168.234.210 port=6000 password=123456" -c 'select inet_server_addr(),pg_is_in_recovery()'
 inet_server_addr | pg_is_in_recovery
------------------+-------------------
 192.168.234.203  | t
(1 fila)

Después de desplegar haproxy, puede ver estadísticas mediante interfaz web http://192.168.234.210:7000/

Replicación en cascada

Normalmente todos esclavos del clúster replican datos desde maestro, pero en escenarios específicos podemos necesitar desplegar replicación en cascada. Clúster PG construido basado en Patroni soporta 2 formas de replicación en cascada.

Replicación en cascada interna del clúster

Puede especificar que cierto esclavo prefiera replicar datos desde miembro específico en lugar de nodo líder. Ejemplo de configuración correspondiente:

tags:
    replicatefrom: pg2

replicatefrom solo es válido cuando nodo está en rol Replica, no afecta participación de ese nodo en elecciones líder ni convertirse en líder. Cuando nodo fuente replicación especificado por replicatefrom falla, Patroni modifica automáticamente PG para cambiar replicación desde nodo líder.

Replicación en cascada entre clústeres

También podemos crear clúster esclavo solo lectura que replique datos desde instancia PostgreSQL específica. Esto puede usarse para crear clúster de respaldo desastre entre centros de datos. Ejemplo de configuración correspondiente:

Para crear inicialmente clúster esclavo, puede agregar siguiente configuración en archivo de configuración de Patroni /etc/patroni.yml:

bootstrap:
  dcs:
    standby_cluster:
      host: 192.168.234.210
      port: 5432
      primary_slot_name: slot1
      create_replica_methods:
      - basebackup

host y port anteriores son host y puerto de fuente replicación upstream, si base de datos upstream está configurada con VIP lectura-escritura de clúster PG, puede usar VIP lectura-escritura como host para evitar impacto en clúster esclavo cuando ocurre cambio maestro-esclavo en clúster maestro.

Opción ranura replicación primary_slot_name es opcional, si se configura ranura replicación, debe configurar simultáneamente slot persistente en clúster maestro para garantizar mantener siempre slot en nuevo maestro:

slots:
  slot1:
    type: physical

Para clúster en cascada ya configurado, puede usar comando patronictl edit-config para agregar dinámicamente configuración standby_cluster y convertir clúster maestro en esclavo; así como eliminar configuración standby_cluster y convertir clúster esclavo en maestro:

standby_cluster:
  host: 192.168.234.210
  port: 5432
  primary_slot_name: slot1
  create_replica_methods:
  - basebackup

Referencias

Etiquetas: PostgreSQL Patroni alta-disponibilidad etcd base-de-datos

Publicado el 10-11 06:07