Ampliando la capacidad de nuestro PostgreSQL con Pgpool-II (y II)

Como ya vimos en el post anterior tenemos nuestro PostgreSQL en una composición maestro-esclavo con replicación, podemos realizar peticiones de lectura a nuestro esclavo para no sobrecargar al servidor principal pero tenemos que hacerlo a mano y si se cae alguno de los servidores tendremos que cambiar el comportamiento de nuestra aplicación para que apunte a la dirección correcta.

Con el middleware Pgpool-II que vamos a montar sobre esta configuración tendremos un único punto sobre el que realizar las peticiones, tanto de lectura como de escritura y él mismo se encargará de repartirlas, evidentemente las escrituras siempre las realizará sobre el principal pero en caso de caída de alguno de los nodos podríamos seguir trabajando casi sin tocar nada ya que la ip frontal se mantendría.

La infraestrucutra que vamos a montar difiere un poco de la explicada en el post en el que me he basado para estos dos artículos. En nuestro caso vamos a utilizar dos contenedores diferentes a los utilizados para los servidores PostgreSQL, mi colega montó los dos nodos de Pgpool-II sobre los servidores de base de datos, lo que tendría sentido para un montaje real, pero dado lo barato que es montar un contenedor, optaremos por separar la infraestructura. De esta forma tendremos dos nodos de Pgpool-II, ubuntu-pgpool-master, en el que tendremos la configuración principal, y ubuntu-pgpool-slave con una configuración de respaldo por si cae el otro.

Para no tener que intervenir en este caso utilizaremos una ip virtual que levantaremos sobre la ip principal del nodo y mediante el servicio watchdog, incluido en el propio pgpool-II, configuraremos éste para que en caso de caída del nodo vivo el otro tome el control de esta ip y pueda seguir prestando el servicio.

Vamos con la instalación, el paquete se llama pgpool2 y sólo necesitaremos ejecutar el siguiente comando:

# apt install pgpool2

Esto lo haremos en los dos nodos sobre los que montaremos Pgpool-II. En los artículos referidos, la instalación la realiza mediante la compilación del código fuente porque en el momento de su redacción en los repositorios oficiales de PostgreSQL para las distribuciones con paquetes .deb iban un poco retrasados, pero actualmente podemos utilizarlos sin problemas ya que disponen de las versiones más modernas de toda la suite.

En esa serie de artículos también explican cómo montar scripts para pasar un nodo secundario a principal cuando este último ha caído y no se puede recuperar, cómo instalar una herramienta de administración web para Pgpool-II, que quizás hagamos más adelante, pero yo prefiero centrarme en cómo podemos montar el esqueleto principal, probar a tirar alguno de los nodos que tenemos y que todo siga funcionando hasta que nos dé tiempo a restaurar todo el chiringuito.

Ahora vamos a editar el archivo principal de configuración de Pgpool-II, /etc/pgpool2/pgpool.conf:

listen_addresses = '*'  
port = 5432  
socket_dir = '/var/run/postgresql'  

Comentaremos las variables que tenemos que cambiar por partes para poder explicar con detalle a qué se debe cada cambio. En este primer bloque no hay ningún secreto, son configuraciones idénticas a las que hacemos cuando instalamos PostgreSQL, las que vienen a continuación sí que son más interesantes:

backend_hostname0 = '192.168.1.210'  
backend_port0 = 5432  
backend_weight0 = 1  
backend_data_directory0 = '/var/lib/postgresql/9.6/main/'  
backend_flag0 = 'ALLOW_TO_FAILOVER'

backend_hostname1 = '192.168.1.211'  
backend_port1 = 5432  
backend_weight1 = 1  
backend_data_directory1 = '/var/lib/postgresql/9.6/main/'  
backend_flag1 = 'ALLOW_TO_FAILOVER'  

Este conjunto de configuraciones le dice al pool qué nodos están disponibles para repartir la carga, sus ips, sus puertos y el peso que van a tener dentro de la distribución de la carga. En este caso ambos tienen un 1 lo que indica que las peticiones de lectura se repartirán equitativamente, evidentemente las de escritura sólo irán al nodo principal. Mediante la variable ALLOW_TO_FAILOVER se le indica que cualquiera de los nodos puede fallar. Quizás lo entendáis mejor si os explico que este flag sólo tiene dos posibilidades y la contraria a ésta se debe utilizar cuando la alta disponibilidad ya se ha configurado en el nivel de base de datos directamente, por lo que en el caso que nos ocupa hemos elegido la primera de ellas. Sigamos:

load_balance_mode = on  
master_slave_mode = on  
master_slave_sub_mode = 'stream'  
sr_check_period = 5  
sr_check_user = 'libreadmin_health_check'  
sr_check_password = 'libreadmin_health_check'  
sr_check_database = 'libreadmin_health_check'  
health_check_period = 5  
health_check_timeout = 0  
health_check_user = 'libreadmin_health_check'  
health_check_password = 'libreadmin_health_check'  
health_check_database = 'libreadmin_health_check'  

Todas esta variables parecen lógicas. Activamos el balanceo de carga y le decimos a Pgpool-II que la base de datos subyacente está configurada en modo maestro-esclavo y con una replicación en modo stream. Las otras variables nos sirven para indicar en qué forma se verificará que un nodo está funcionando correctamente. Para esto último tenemos que crear la base de datos que le indicamos, por supuesto, lo haremos en el nodo maestro:

# su - postgres
$ createuser -DRP libreadmin_health_check
Enter password for new role: ***********************  
Enter it again: ***********************  
$ createdb -O libreadmin_health_check libreadmin_health_check

Los modificadores del comando createuser indican que el usuario no podrá crear bases de datos ni más usuarios y a continuación nos pedirá una contraseña, en este caso los tres valores serán iguales para simplificar la configuración.

Como ya he dicho más arriba, en el tutorial que estamos siguiendo hay varios scripts que automatizarán algunas tareas en caso de caída de cualquiera de los nodos, tanto los de la propia base de datos como los del propio pool, pero para montar esta infraestructura y comprobar cómo funciona no he considerado oportuno añadirlo aquí aunque no descarto refinar un poco la configuración en posteriores posts.

Ahora viene una parte bien interesante. Supongo que más de uno habrá utilizado ips virtuales en sus configuraciones, para los que no sepáis de qué estamos hablando el tema es muy sencillo. Simplemente es una ip levantada sobre cualquier interfaz del servidor que estemos utilizando. Pues bien, este concepto es el que utiliza la siguiente pieza de ese puzle que es Pgpool-II, watchdog. Los desarrolladores de este software han implementado un mecanisno por el cuál cualquiera de los nodos de Pgpool-II podrá levantar una ip de este tipo y el resto de nodos comprobarán que este nodo esté funcionando para no tener que hacerla suya. Esto es una gran ventaja ya que si cualquier extremo cae, nuestras aplicaciones no se enterarían y podrían seguir realizando peticiones a la misma dirección sin interrupción del servicio, aunque en realidad esta ip esté en otro servidor. La configuración implica las siguientes partes en el archivo /etc/pgpool2/pgpool.conf, estas primeras líneas deben aparecer en la configuración de todos los nodos:

use_watchdog = on  
trusted_servers = '192.168.1.1,192.168.1.2'  

Aquí sólo indicamos que activamos el uso de watchdog y cuáles serán los servidores de referencia para que el propio nodo sepa si tiene salida al exterior para así determinar si tiene que levantar la ip principal de Pgpool-II. Como se explica en el tutorial tenemos que poner dos servidores conocidos para evitar situaciones extrañas en los que, por ejemplo, dos nodos se consideren maestros. Para unas pruebas simples como las que estamos haciendo esto no es muy decisivo aunque no está de más ponerlo para asegurarnos de que todo funciona correctamente. Las siguientes variables las pondremos en el maestro:

wd_hostname = '192.168.1.212'  
wd_port = 9000  
wd_priority = 2  

Y las siguientes en el esclavo:

wd_hostname = '192.168.1.213'  
wd_port = 9000  
wd_priority = 1  

Con esto conseguimos decirle al servicio que el nodo principal es el de la ip 192.168.1.212. Evidentemente no todos los usuarios pueden jugar así como así con las ips de un servidor y el usuario postgres no es una excepción así que tendremos que otorgarle tan alto honor mediante la edición del fichero sudoers. Para ello invocamos el editor mediante el comando visudo y añadimos estas dos líneas:

# Allow pgpool-II watchdog to configure the virtual IP
postgres        ALL=NOPASSWD:   /sbin/ip  
postgres        ALL=NOPASSWD:   /usr/sbin/arping  

Con esta configuración tan sencilla permitiremos que el servicio Pgpool-II levante correctamente las ips virtuales que le indicamos en la configuración siguiente (esto lo haremos en ambos nodos y en el archivo que llevamos editando todo este rato, /etc/pgpool2/pgpool.conf):

wd_ipc_socket_dir = '/var/run/postgresql'  
delegate_IP = '192.168.1.215'  
if_cmd_path = ''  
if_up_cmd = '/usr/bin/sudo /sbin/ip addr add $_IP_$/24 dev eth0 label eth0:0'  
if_down_cmd = '/usr/bin/sudo /sbin/ip addr del $_IP_$/24 dev eth0'  
arping_path = ''  
arping_cmd = '/usr/bin/sudo /usr/sbin/arping -U $_IP_$ -w 1'  
wd_lifecheck_method = 'heartbeat'  
wd_interval = 3  
wd_heartbeat_port = 9694  

Como habéis visto tenemos que indicar cuál es la ip sobre la que el servicio va a responder y cuáles son los comandos necesarios para que él mismo la levante en el momento indicado. El método mediante el que la salud del resto de nodos será comprobada lo hemos establecido al valor heartbeat, que es el método implementado por el propio proyecto. Las siguientes ariables sólo tendremos que establecerlas en el nodo principal de Pgpool-II:

heartbeat_destination0 = '192.168.1.213'  
heartbeat_destination_port0 = 9694  
other_pgpool_hostname0 = '192.168.1.213'  
other pgpool 0  
other_pgpool_port0 = 5432  
other_wd_port0 = 9000  

Y de forma análoga en el esclavo:

heartbeat_destination0 = '192.168.1.213'  
heartbeat_destination_port0 = 9694  
other_pgpool_hostname0 = '192.168.1.213'  
other pgpool 0  
other_pgpool_port0 = 5432  
other_wd_port0 = 9000  

Ya sólo nos queda un último paso y es permitir que el pool pueda ejecutar comprobaciones en los nodos de PostgreSQL subyacentes para que la infraestructura se comporte como nosotros le hemos indicado. Para ello tenemos que editar el fichero /etc/postgresql/9.6/main/pg_hba.conf en ambos nodos y añadir la siguiente línea:

host    all     all     192.168.1.0/24      trust  

Con esto debería funcionar todo perfectamente, para comprobarlo podemos ir reiniciando los servicios de los cuatro nodos implicados, los dos servicios de PostgreSQL y los dos de Pgpool-II. Un consejo, para poder ver los posibles errores cometidos podemos activar la salida de logs en el pool cambiando la siguiente variable en su archivo de configuración, /etc/pgpool2/pgpool.conf:

log_destination = 'syslog'  

Reiniciando el servicio podremos ver todos los logs en el archivo de syslog del sistema.

Como una parte importante de todo desarrollo que realicéis no olvidéis implantar una serie de testeos funcionales y, por supuesto, de rendimiento, así vuestros compañeros de la parte de desarrollo no os podrán echar la culpa de que algo no funciona correctamente.

Yo voy a contaros el más simple que se puede realizar mediante la herramienta pgbench, en este caso sólo lo vamos a utilizar para comprobar si hay pérdida de rendimiento pasando de realizar consultas directamente a la base de datos o realizándolas al servicio Pgpool-II. El primer paso es crear, desde el nodo maestro de PostgreSQL, una base de datos de prueba:

# su - postgres
$ createdb performance_tests
$ pgbench -i -s 10 -h localhost -U postgres performance_tests
NOTICE:  table "pgbench_history" does not exist, skipping  
NOTICE:  table "pgbench_tellers" does not exist, skipping  
NOTICE:  table "pgbench_accounts" does not exist, skipping  
NOTICE:  table "pgbench_branches" does not exist, skipping  
creating tables...  
100000 of 1000000 tuples (10%) done (elapsed 0.08 s, remaining 0.72 s)  
200000 of 1000000 tuples (20%) done (elapsed 0.16 s, remaining 0.64 s)  
300000 of 1000000 tuples (30%) done (elapsed 0.25 s, remaining 0.59 s)  
400000 of 1000000 tuples (40%) done (elapsed 0.61 s, remaining 0.91 s)  
500000 of 1000000 tuples (50%) done (elapsed 0.94 s, remaining 0.94 s)  
600000 of 1000000 tuples (60%) done (elapsed 1.27 s, remaining 0.85 s)  
700000 of 1000000 tuples (70%) done (elapsed 1.64 s, remaining 0.70 s)  
800000 of 1000000 tuples (80%) done (elapsed 2.04 s, remaining 0.51 s)  
900000 of 1000000 tuples (90%) done (elapsed 2.41 s, remaining 0.27 s)  
1000000 of 1000000 tuples (100%) done (elapsed 2.82 s, remaining 0.00 s)  
vacuum...  
set primary keys...  
done.  

Si no os deja lanzar este comando en local probablemente sea por falta de permisos, editad el archivo /etc/postgresql/9.6/main/pg_hba.conf y cambiad el método de autenticación de md5 a trust, por lo menos en los accesos desde la interfaz local ya que así no os pedirá un password, que por otra parte no hemos definido en nuestro servidor.

Ya tenemos la base de datos de test en el sistema, ahora lanzaremos una prueba apuntando a la ip del maestro del propio PostgreSQL:

$ pgbench -c 16 -j 16 -T 60 -h 192.168.1.210 -U postgres performance_tests
starting vacuum...end.  
transaction type: <builtin: TPC-B (sort of)>  
scaling factor: 10  
query mode: simple  
number of clients: 16  
number of threads: 16  
duration: 60 s  
number of transactions actually processed: 66590  
latency average = 14.422 ms  
tps = 1109.415731 (including connections establishing)  
tps = 1110.052172 (excluding connections establishing)  

Y ahora lanzamos la misma prueba pero apuntando a la ip del propio Pgpool-II:

$ pgbench -c 16 -j 16 -T 60 -h 192.168.1.215 -U postgres performance_tests
starting vacuum...end.  
transaction type: <builtin: TPC-B (sort of)>  
scaling factor: 10  
query mode: simple  
number of clients: 16  
number of threads: 16  
duration: 60 s  
number of transactions actually processed: 65841  
latency average = 14.589 ms  
tps = 1096.720683 (including connections establishing)  
tps = 1097.262793 (excluding connections establishing)  

Lo sé, los números parecen pobres y sólo es una prueba puntual, bien, ésa es la actitud ante cualquier prueba de rendimiento. Respecto a que los números sean pobres es evidente porque estamos tirando de una configuración por defecto. También hay que tener en cuenta que nuestras bases de datos están corriendo sobre dos contenedores, en teoría la pérdida de rendimiento no debería ser apreciable ante otra base de datos configurada de forma similar y sobre el mismo sistema de archivos en el que lanzamos los contenedores, pero sí, tenéis razón, habría que realizar pruebas más amplias y variadas para llegar a una conclusión, pero ése no es el alcance de este post.

Y por último, antes de dejaros juguetear con vuestras nuevas y mejoradas bases de datos, durante la configuración de estos contenedores hice prubas de caídas de todos los nodos y en algún punto Pgpool-II ya no supo que la infraestructura subyacente, es decir el streaming de PostgreSQL, se había recuperado correctamente.

En estos casos entran en juego los mecanismos tan bien explicados por mi colega en el post del que he sacado gran parte de esta información. Os recomiendo encarecidamente que exploréis todas las posibilidades de caídas e implementéis las soluciones, automáticas o manuales, necesarias en cada uno de los escenarios de fallo, sin eso y sin unas buenas pruebas de rendimiento ni me plantearía utilizar este montaje en producción.

Como curiosidad os diré que para recuperar el estado inicial del pool después de varias caídas, tuve que borrar a mano el fichero en el que se guardan los estados de cada uno de los nodos de PostgreSQL, /var/log/postgresql/pgpool_status. Según la documentación hay una opción en el reinicio del servicio para ignorar este archivo, pero no conseguí llevarla a cabo por lo que opté por este atajo. Por otra parte, si queréis comprobar el estado del pool en cualquier momento, podéis ejecutar el siguiente comando SQL en el frontal:

$ psql -h 192.168.1.215 -U postgres
psql (9.6.3)  
Digite «help» para obtener ayuda.

postgres=# show pool_nodes;  
 node_id |   hostname    | port | status | lb_weight |  role   | select_cnt | load_balance_node | replication_delay 
---------+---------------+------+--------+-----------+---------+------------+-------------------+-------------------
 0       | 192.168.1.210 | 5432 | up     | 0.500000  | primary | 185804     | false             | 0
 1       | 192.168.1.211 | 5432 | up     | 0.500000  | standby | 2          | true              | 0
(2 filas)

Ahora os dejo en mejor compañía, Héroes Del Silencio, con un clásico de su disco Senderos De Traición, Entre Dos Tierras que nos amenizó varias noches de verano en el Zoco de La Manga hace unos cuantos añitos ya.

Disfrutad!