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_basebackupy scripts personalizados compatibles con herramientas comowal_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~pg4respectivamente paranodo1~nodo4
- Configurar
- 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":
- Cuando nodos no líder tienen PG en modo producción, reinicia PG y cambia a modo recuperación como esclavo
- Cuando patroni líder no puede conectar a etcd, no puede asegurar ser aún líder, degrada PG local a esclavo
- 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 /oGET /leaderNodo en ejecución y rol líderGET /replicaRol réplica en ejecución, sin etiqueta noloadbalance configuradaGET /read-onlySimilar aGET /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
- https://patroni.readthedocs.io/en/latest/
- http://blogs.sungeek.net/unixwiz/2018/09/02/centos-7-postgresql-10-patroni/
- https://scalegrid.io/blog/managing-high-availability-in-postgresql-part-1/
- https://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters
- https://www.percona.com/blog/2019/10/23/seamless-application-failover-using-libpq-features-in-postgresql/