PostgreSQL BDR: Cómo mantener un servicio

Recientemente cambié de proveedor de servidores de Hetzner a una mezcla entre Scaleway y DigitalOcean. La ventaja ha sido disponer de muchos servidores pequeños por la mitad del precio de uno grande pero, ¿cómo conseguir que una base de datos como PostgreSQL trabaje en multi-datacenter?

En realidad es bastante fácil y con una respuesta muy corta: a través del proyecto BDR de 2nd Quadrant. BDR son las siglas para Bi-Direccional Replication. Lo que quiere decir que configura una replicación maestro-maestro para PostgreSQL y cada cambio realizado en cualquiera de los PostgreSQL pertenecientes al cluster es replicado al resto.

BDR es una extensión para PostgreSQL. Se configura por base de datos independiente y configura una conexión entre los diferentes PostgreSQL. Estos sistemas de base de datos se sincronizan y deciden por consenso el master.

¿Cómo instalar PostgreSQL con BDR?

Para comenzar necesitamos tener compilada y disponible la extensión BDR para PostgreSQL. La empresa 2ndQuadrant nos facilita una versión de PostgreSQL para diferentes distribuciones de GNU/Linux en su web principalmente para instalar vía YUM o APT. La última versión disponible de momento es PostgreSQL 9.4 mientras que la última estable es PostgreSQL 10.4.

Si prefieres instalar desde código fuente puedes seguir esta guía.

La configuración a través de los ficheros postgresql.conf and pg_hba.conf varía también. Debemos configurar los accesos de replicación entre los servidores para evitar que otros tengan acceso a los datos.

Siguiendo los consejos de esta publicación de Lê Yên Thanh (en inglés) debemos agregar en el fichero postgresql.conf lo siguiente:

listen_addresses = '*'
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on
max_connections = 100
max_wal_senders = 10
max_replication_slots = 10
max_worker_processes = 10

Y teniendo en cuenta el ejemplo en el que tenemos dos servidores en las IPs 10.0.0.1 y 10.0.0.2 podemos ver la configuración de pg_hba.conf como sigue:

local replication postgres              trust
host  replication postgres 127.0.0.1/32 trust
host  replication postgres ::1/128      trust

host all all 0.0.0.0/0  password

host replication postgres 10.0.0.1/32 trust
host replication postgres 10.0.0.2/32 trust

host replication bdrsync 10.0.0.1/32 password
host replication bdrsync 10.0.0.2/32 password

Nos queda levantar la base de datos y crear los usuarios. En principio el usuario que se encargará de la replicación principalmente:

createuser -s -P bdrsync

La clave será preguntada al momento de crear el usuario. Recuerda la clave, la necesitarás para más adelante.

Por último y por mi parte agregué además unas reglas de iptables para evitar que ningún otro sistema pueda acceder al puerto de PostgreSQL (5432) desde fuera.

¿Cómo creamos el cluster?

Esto está genial. Todo está configurado en la base pero aún no tenemos ninguna base de datos que se replique en nuestro cluster. Debemos hacer lo siguiente por cada base de datos para agregarla en el cluster:

  1. Crear la base de datos.
  2. Crear las extensiones btree_gist y bdr dentro de la base de datos a replicar.
  3. Ejecutar una de las siguientes consultas dependiendo si es el PostgreSQL de partida o los siguientes que se vayan uniendo.

La SQL para el primer PostgreSQL es:

SELECT bdr.bdr_group_create(
    local_node_name := 'node1',
    node_external_dsn := 'host=10.0.0.1 user=bdrsync dbname=test_db password=12345#'
);

Ten en cuenta que tendrás que cambiar el nombre del nodo por el nombre de la máquina donde esté instalado PostgreSQL.

El valor de node_external_dsn es una línea de conexión para la base de datos a replicar.

La SQL para el otro nodo será:

SELECT bdr.bdr_group_join(
    local_node_name := 'node2',
    node_external_dsn := 'host=10.0.0.2 user=bdrsync dbname=test_db password=12345#',
    join_using_dsn := 'host=10.0.0.1 user=bdrsync dbname=test_db password=12345#'
);

En este caso puedes ver que node_external_dsn se conecta igualmente a la base de datos local y se especifica un nuevo campo join_using_dsn donde se especifica la base de datos remota a la que unirse.

Si todo sale bien podrás ver el resultado de la consulta con dos entradas y el node_status como r (ready):

SELECT * FROM bdr.bdr_nodes;

¿Qué pasa cuando todo se rompe?

Me pasó al principio que cree tres bases de datos para replicar y tengo un cluster de tres nodos:

La configuración por defecto para max_replication_slots es muy pequeño. Digamos que con tres PostgreSQL y 3 bases de datos necesitamos 6 conexiones y tenía tan solo 5 por defecto.

Esto hizo que hubiese problemas en la sincronización de una de las bases de datos y finalmente tuviese que reconfigurar y rehacerla. No hubo mucho problema en realidad porque pude dejar una de ellas funcionando mientras corregía las otras dos y cuando las dos estuvieron operativas entonces corregí la última.

¿Y Cuando el proveedor falla?

Realmente no fue intencionado, me enfadó bastante que pasara pero Scaleway Amsterdam estuvo caído 6 horas por un error en uno de sus routers. La genialidad es que mi sistema de base de datos estaba aún funcionando gracias a los servidores de Scaleway París y DigitalOcean Amsterdam.

Obviamente esto no podría haber sido posible sin un sistema de DNS con balanceo de carga configurable apuntando a todos los PostgreSQL disponibles.

Fue genial comprobar cuando Scaleway Amsterdam volvió a la vida que la sincronización se llevó a cabo sin problemas y volvieron a estar los tres PostgreSQL operativos sin mediación por mi parte.

Algunas partes negativas

Una de las partes negativas que comprobé es el uso de claves primarias. PostgreSQL tiene posibilidad de usar secuencias (lo más habitual) o UUID para claves primarias. BDR suma a estas su secuencia distribuida. El problema es que esta secuencia distribuida no es totalmente compatible con las secuencias nativas y ciertas funciones no retornan los valores esperados (como currval).

Lo ideal si estás diseñando la base de datos pensando en realizar un cluster es pensar en utilizar UUID para evitar problemas desde el principio. El problema con las secuencias nativas es la posibilidad de encontrar colisiones.

Otro de los problemas es la naturaleza de la replicación. Un sistema con escritura constante y rápida podría saturar el sistema de replicación. Si tu sistema requiere de un gran volumen de escrituras por segundo puede no ser un problema si se trata de inserciones nuevas pero sí podría convertirse en un problema si son modificaciones (UPDATE) y los campos a modificar implican claves únicas o primarias.

Conclusiones

Me ha parecido una experiencia genial poder poner en práctica este sistema de multi-maestro para PostgreSQL. Ha resultado muy sencilla su configuración y puesta en producción y los problemas encontrados han sido fácilmente resueltos gracias a la información en logs además de toda la documentación disponible para PostgreSQL BDR.

No obstante hay que ser cauteloso y estudiar bien la base de datos a implementar y los cambios a llevar a cabo para evitar problemas al momento de poner el sistema en producción.

¿Y tú?, ¿usas PostgreSQL en tus proyectos?, ¿has pensado en replicar tus datos en varias bases de datos para mantener una alta disponibilidad?, ¿tienes otras soluciones para estos problemas? ¿necesitas ayuda?, ¡Déjanos un comentario!