Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

En el artículo, les contaré cómo abordamos el tema de la tolerancia a fallas de PostgreSQL, por qué se volvió importante para nosotros y qué sucedió al final.

Tenemos un servicio altamente cargado: 2,5 millones de usuarios en todo el mundo, más de 50 100 usuarios activos todos los días. Los servidores están ubicados en Amazone en una región de Irlanda: más de 50 servidores diferentes funcionan constantemente, de los cuales casi XNUMX tienen bases de datos.

Todo el backend es una gran aplicación monolítica de Java con estado que mantiene una conexión websocket constante con el cliente. Cuando varios usuarios trabajan en el mismo tablero al mismo tiempo, todos ven los cambios en tiempo real, porque escribimos cada cambio en la base de datos. Tenemos alrededor de 10K solicitudes por segundo a nuestras bases de datos. En el pico de carga en Redis, escribimos entre 80 100 y XNUMX XNUMX solicitudes por segundo.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Por qué cambiamos de Redis a PostgreSQL

Inicialmente, nuestro servicio funcionaba con Redis, un almacén de clave-valor que almacena todos los datos en la memoria RAM del servidor.

Ventajas de Redis:

  1. Alta velocidad de respuesta, porque todo se almacena en la memoria;
  2. Facilidad de copia de seguridad y replicación.

Contras de Redis para nosotros:

  1. No hay transacciones reales. Intentamos simularlos al nivel de nuestra aplicación. Desafortunadamente, esto no siempre funcionó bien y requirió escribir un código muy complejo.
  2. La cantidad de datos está limitada por la cantidad de memoria. A medida que aumenta la cantidad de datos, la memoria crecerá y, al final, nos encontraremos con las características de la instancia seleccionada, lo que en AWS requiere detener nuestro servicio para cambiar el tipo de instancia.
  3. Es necesario mantener constantemente un nivel de latencia bajo, porque. Tenemos una gran cantidad de solicitudes. El nivel de retardo óptimo para nosotros es de 17 a 20 ms. A un nivel de 30-40 ms, obtenemos respuestas largas a las solicitudes de nuestra aplicación y degradación del servicio. Lamentablemente, esto nos sucedió en septiembre de 2018, cuando una de las instancias con Redis por algún motivo recibió una latencia 2 veces mayor a la habitual. Para resolver el problema, detuvimos el servicio al mediodía por mantenimiento no programado y reemplazamos la instancia problemática de Redis.
  4. Es fácil obtener inconsistencias en los datos incluso con errores menores en el código y luego dedicar mucho tiempo a escribir código para corregir estos datos.

Tomamos en cuenta las desventajas y nos dimos cuenta de que necesitábamos cambiar a algo más conveniente, con transacciones normales y menos dependencia de la latencia. Realicé una investigación, analicé muchas opciones y elegí PostgreSQL.

Nos hemos estado mudando a una nueva base de datos durante 1,5 años y solo hemos movido una pequeña parte de los datos, por lo que ahora estamos trabajando simultáneamente con Redis y PostgreSQL. Más información sobre las etapas de mover y cambiar datos entre bases de datos está escrito en el articulo de mi colega.

Cuando comenzamos a mudarnos, nuestra aplicación trabajaba directamente con la base de datos y accedía al maestro Redis y PostgreSQL. El clúster de PostgreSQL constaba de un maestro y una réplica con replicación asíncrona. Así es como se veía el esquema de la base de datos:
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Implementando PgBouncer

Mientras nos mudábamos, el producto también se desarrollaba: aumentaba la cantidad de usuarios y la cantidad de servidores que trabajaban con PostgreSQL, y comenzamos a carecer de conexiones. PostgreSQL crea un proceso separado para cada conexión y consume recursos. Puede aumentar la cantidad de conexiones hasta cierto punto; de lo contrario, existe la posibilidad de obtener un rendimiento de base de datos subóptimo. La opción ideal en tal situación sería elegir un administrador de conexión que se ubique frente a la base.

Teníamos dos opciones para el administrador de conexiones: Pgpool y PgBouncer. Pero el primero no admite el modo transaccional de trabajar con la base de datos, por lo que elegimos PgBouncer.

Hemos configurado el siguiente esquema de trabajo: nuestra aplicación accede a un PgBouncer, detrás del cual hay maestros de PostgreSQL, y detrás de cada maestro hay una réplica con replicación asíncrona.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Al mismo tiempo, no podíamos almacenar toda la cantidad de datos en PostgreSQL y la velocidad de trabajo con la base de datos era importante para nosotros, por lo que comenzamos a fragmentar PostgreSQL a nivel de aplicación. El esquema descrito anteriormente es relativamente conveniente para esto: al agregar un nuevo fragmento de PostgreSQL, es suficiente actualizar la configuración de PgBouncer y la aplicación puede funcionar inmediatamente con el nuevo fragmento.

Conmutación por error de PgBouncer

Este esquema funcionó hasta el momento en que murió la única instancia de PgBouncer. Estamos en AWS, donde todas las instancias se ejecutan en hardware que muere periódicamente. En tales casos, la instancia simplemente se traslada a un nuevo hardware y vuelve a funcionar. Esto sucedió con PgBouncer, pero dejó de estar disponible. El resultado de esta caída fue la indisponibilidad de nuestro servicio durante 25 minutos. AWS recomienda utilizar la redundancia del lado del usuario para tales situaciones, que no estaba implementada en nuestro país en ese momento.

Después de eso, pensamos seriamente en la tolerancia a fallas de los clústeres de PgBouncer y PostgreSQL, porque una situación similar podría ocurrir con cualquier instancia en nuestra cuenta de AWS.

Construimos el esquema de tolerancia a fallas de PgBouncer de la siguiente manera: todos los servidores de aplicaciones acceden al Network Load Balancer, detrás del cual hay dos PgBouncers. Cada PgBouncer mira al mismo maestro de PostgreSQL de cada fragmento. Si se vuelve a producir un bloqueo de instancia de AWS, todo el tráfico se redirige a través de otro PgBouncer. AWS proporciona la conmutación por error de Network Load Balancer.

Este esquema facilita la adición de nuevos servidores PgBouncer.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Crear un clúster de conmutación por error de PostgreSQL

Al resolver este problema, consideramos diferentes opciones: conmutación por error autoescrita, repmgr, AWS RDS, Patroni.

Guiones escritos por uno mismo

Pueden monitorear el trabajo del maestro y, si falla, promocionar la réplica al maestro y actualizar la configuración de PgBouncer.

Las ventajas de este enfoque son la máxima simplicidad, ya que usted mismo escribe los scripts y comprende exactamente cómo funcionan.

Contras:

  • Es posible que el maestro no haya muerto, sino que se haya producido una falla en la red. La conmutación por error, sin darse cuenta de esto, promocionará la réplica al maestro, mientras que el antiguo maestro seguirá funcionando. Como resultado, obtendremos dos servidores en el papel de maestro y no sabremos cuál de ellos tiene los últimos datos actualizados. Esta situación también se denomina cerebro dividido;
  • Nos quedamos sin respuesta. En nuestra configuración, el maestro y una réplica, después de cambiar, la réplica sube al maestro y ya no tenemos réplicas, por lo que debemos agregar manualmente una nueva réplica;
  • Necesitamos monitoreo adicional de la operación de conmutación por error, mientras que tenemos 12 fragmentos de PostgreSQL, lo que significa que tenemos que monitorear 12 clústeres. Con un aumento en la cantidad de fragmentos, también debe recordar actualizar la conmutación por error.

La conmutación por error autoescrita parece muy complicada y requiere soporte no trivial. Con un solo clúster de PostgreSQL, esta sería la opción más fácil, pero no escala, por lo que no es adecuada para nosotros.

Repmgr

Administrador de replicación para clústeres de PostgreSQL, que puede administrar el funcionamiento de un clúster de PostgreSQL. Al mismo tiempo, no tiene una conmutación por error automática lista para usar, por lo que para el trabajo deberá escribir su propio "envoltorio" en la parte superior de la solución terminada. Entonces, todo puede resultar aún más complicado que con los scripts escritos por uno mismo, por lo que ni siquiera probamos Repmgr.

RDS de AWS

Soporta todo lo que necesitamos, sabe cómo hacer copias de seguridad y mantiene un grupo de conexiones. Tiene cambio automático: cuando el maestro muere, la réplica se convierte en el nuevo maestro y AWS cambia el registro dns al nuevo maestro, mientras que las réplicas se pueden ubicar en diferentes AZ.

Las desventajas incluyen la falta de ajustes finos. Como ejemplo de ajuste fino: nuestras instancias tienen restricciones para las conexiones tcp, lo que, lamentablemente, no se puede hacer en RDS:

net.ipv4.tcp_keepalive_time=10
net.ipv4.tcp_keepalive_intvl=1
net.ipv4.tcp_keepalive_probes=5
net.ipv4.tcp_retries2=3

Además, AWS RDS cuesta casi el doble que el precio de una instancia regular, que fue la principal razón para abandonar esta solución.

patrón

Esta es una plantilla de python para administrar PostgreSQL con buena documentación, conmutación por error automática y código fuente en github.

Ventajas de Patroni:

  • Se describe cada parámetro de configuración, queda claro cómo funciona;
  • La conmutación por error automática funciona de inmediato;
  • Escrito en python, y dado que nosotros mismos escribimos mucho en python, nos será más fácil tratar los problemas y, quizás, incluso ayudar al desarrollo del proyecto;
  • Administra completamente PostgreSQL, le permite cambiar la configuración en todos los nodos del clúster a la vez y, si es necesario reiniciar el clúster para aplicar la nueva configuración, puede volver a hacerlo con Patroni.

Contras:

  • No está claro en la documentación cómo trabajar correctamente con PgBouncer. Aunque es difícil llamarlo un menos, porque la tarea de Patroni es administrar PostgreSQL, y cómo funcionarán las conexiones a Patroni ya es nuestro problema;
  • Hay pocos ejemplos de implementación de Patroni en grandes volúmenes, mientras que hay muchos ejemplos de implementación desde cero.

Como resultado, elegimos Patroni para crear un clúster de conmutación por error.

Proceso de implementación de Patroni

Antes de Patroni, teníamos 12 fragmentos de PostgreSQL en una configuración de un maestro y una réplica con replicación asíncrona. Los servidores de aplicaciones accedían a las bases de datos a través del Network Load Balancer, detrás de las cuales se encontraban dos instancias con PgBouncer, y detrás de ellas se encontraban todos los servidores PostgreSQL.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Para implementar Patroni, necesitábamos seleccionar una configuración de clúster de almacenamiento distribuido. Patroni trabaja con sistemas de almacenamiento de configuración distribuida como etcd, Zookeeper, Consul. Solo tenemos un clúster Consul completo en el mercado, que funciona junto con Vault y ya no lo usamos. Una gran razón para comenzar a usar Consul para el propósito previsto.

Cómo Patroni trabaja con Consul

Tenemos un clúster Consul, que consta de tres nodos, y un clúster Patroni, que consta de un líder y una réplica (en Patroni, el maestro se llama líder del clúster y los esclavos se llaman réplicas). Cada instancia del clúster Patroni envía constantemente información sobre el estado del clúster a Consul. Por tanto, desde Consul siempre podrás conocer la configuración actual del clúster Patroni y quién es el líder en este momento.

Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Para conectar Patroni a Consul, basta con estudiar la documentación oficial, que dice que debe especificar un host en formato http o https, según cómo trabajemos con Consul, y el esquema de conexión, opcionalmente:

host: the host:port for the Consul endpoint, in format: http(s)://host:port
scheme: (optional) http or https, defaults to http

Parece simple, pero aquí comienzan las trampas. Con Consul, trabajamos a través de una conexión segura a través de https y nuestra configuración de conexión se verá así:

consul:
  host: https://server.production.consul:8080 
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

Pero eso no funciona. Al inicio, Patroni no puede conectarse a Consul, porque de todos modos intenta pasar por http.

El código fuente de Patroni ayudó a solucionar el problema. Menos mal que está escrito en python. Resulta que el parámetro de host no se analiza de ninguna manera y el protocolo debe especificarse en el esquema. Así es como nos parece el bloque de configuración de trabajo para trabajar con Consul:

consul:
  host: server.production.consul:8080
  scheme: https
  verify: true
  cacert: {{ consul_cacert }}
  cert: {{ consul_cert }}
  key: {{ consul_key }}

Cónsul-plantilla

Entonces, hemos elegido el almacenamiento para la configuración. Ahora necesitamos entender cómo PgBouncer cambiará su configuración al cambiar el líder en el grupo Patroni. No hay respuesta a esta pregunta en la documentación, porque. allí, en principio, no se describe el trabajo con PgBouncer.

En busca de una solución, encontramos un artículo (desafortunadamente no recuerdo el título) donde estaba escrito que Сonsul-template ayudó mucho a emparejar PgBouncer y Patroni. Esto nos llevó a investigar cómo funciona Consul-template.

Resultó que Consul-template monitorea constantemente la configuración del clúster de PostgreSQL en Consul. Cuando el líder cambia, actualiza la configuración de PgBouncer y envía un comando para recargarlo.

Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Una gran ventaja de la plantilla es que se almacena como código, por lo que al agregar un nuevo fragmento, basta con realizar una nueva confirmación y actualizar la plantilla automáticamente, lo que respalda el principio de Infraestructura como código.

Nueva arquitectura con Patroni

Como resultado, obtuvimos el siguiente esquema de trabajo:
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Todos los servidores de aplicaciones acceden al balanceador → hay dos instancias de PgBouncer detrás → en cada instancia, se inicia Consul-template, que monitorea el estado de cada clúster Patroni y monitorea la relevancia de la configuración de PgBouncer, que envía solicitudes al líder actual de cada racimo.

Prueba manual

Ejecutamos este esquema antes de lanzarlo en un pequeño entorno de prueba y verificamos el funcionamiento del cambio automático. Abrieron el tablero, movieron la calcomanía y en ese momento “mataron” al líder del grupo. En AWS, esto es tan simple como cerrar la instancia a través de la consola.

Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

La pegatina regresó en 10 a 20 segundos y luego nuevamente comenzó a moverse normalmente. Esto significa que el clúster de Patroni funcionó correctamente: cambió el líder, envió la información a Сonsul y Сonsul-template inmediatamente recogió esta información, reemplazó la configuración de PgBouncer y envió el comando para recargar.

¿Cómo sobrevivir bajo una carga alta y mantener el tiempo de inactividad mínimo?

¡Todo funciona perfectamente! Pero hay nuevas preguntas: ¿Cómo funcionará bajo una carga alta? ¿Cómo implementar todo en producción de forma rápida y segura?

El entorno de prueba en el que realizamos las pruebas de carga nos ayuda a responder la primera pregunta. Es completamente idéntico a la producción en términos de arquitectura y ha generado datos de prueba que son aproximadamente iguales en volumen a la producción. Decidimos simplemente "matar" a uno de los maestros de PostgreSQL durante la prueba y ver qué sucede. Pero antes de eso, es importante verificar el balanceo automático, porque en este entorno tenemos varios fragmentos de PostgreSQL, por lo que obtendremos excelentes pruebas de los scripts de configuración antes de la producción.

Ambas tareas parecen ambiciosas, pero tenemos PostgreSQL 9.6. ¿Podemos actualizar inmediatamente a 11.2?

Decidimos hacerlo en 2 pasos: primero actualice a 11.2, luego inicie Patroni.

Actualización de PostgreSQL

Para actualizar rápidamente la versión de PostgreSQL, use la opción -k, en el que se crean enlaces duros en el disco y no hay necesidad de copiar sus datos. En bases de 300-400 GB, la actualización tarda 1 segundo.

Tenemos muchos fragmentos, por lo que la actualización debe realizarse automáticamente. Para hacer esto, escribimos un libro de jugadas de Ansible que maneja todo el proceso de actualización por nosotros:

/usr/lib/postgresql/11/bin/pg_upgrade 
<b>--link </b>
--old-datadir='' --new-datadir='' 
 --old-bindir=''  --new-bindir='' 
 --old-options=' -c config_file=' 
 --new-options=' -c config_file='

Es importante señalar aquí que antes de iniciar la actualización, debe realizarla con el parámetro --controlarpara asegurarse de que puede actualizar. Nuestro script también realiza la sustitución de configuraciones durante la actualización. Nuestro guión se completó en 30 segundos, lo cual es un resultado excelente.

Lanzamiento Patroni

Para resolver el segundo problema, solo mire la configuración de Patroni. El repositorio oficial tiene una configuración de ejemplo con initdb, que es responsable de inicializar una nueva base de datos cuando inicia Patroni por primera vez. Pero como ya tenemos una base de datos preparada, simplemente eliminamos esta sección de la configuración.

Cuando comenzamos a instalar Patroni en un clúster de PostgreSQL ya existente y lo ejecutamos, nos encontramos con un nuevo problema: ambos servidores comenzaron como líderes. Patroni no sabe nada sobre el estado inicial del clúster e intenta iniciar ambos servidores como dos clústeres separados con el mismo nombre. Para resolver este problema, debe eliminar el directorio con datos en el esclavo:

rm -rf /var/lib/postgresql/

¡Esto debe hacerse solo en el esclavo!

Cuando se conecta una réplica limpia, Patroni crea un líder de copia de seguridad base y lo restaura en la réplica, y luego se pone al día con el estado actual de acuerdo con los registros de wal.

Otra dificultad que encontramos es que todos los clústeres de PostgreSQL se nombran como principales de forma predeterminada. Cuando cada grupo no sabe nada sobre el otro, esto es normal. Pero cuando desea utilizar Patroni, todos los clústeres deben tener un nombre único. La solución es cambiar el nombre del clúster en la configuración de PostgreSQL.

prueba de carga

Hemos lanzado una prueba que simula la experiencia del usuario en los tableros. Cuando la carga alcanzó nuestro valor promedio diario, repetimos exactamente la misma prueba, apagamos una instancia con el líder de PostgreSQL. La conmutación por error automática funcionó como esperábamos: Patroni cambió el líder, Consul-template actualizó la configuración de PgBouncer y envió un comando para recargar. Según nuestros gráficos en Grafana, está claro que hay demoras de 20 a 30 segundos y una pequeña cantidad de errores de los servidores asociados con la conexión a la base de datos. Esta es una situación normal, tales valores son aceptables para nuestra conmutación por error y definitivamente son mejores que el tiempo de inactividad del servicio.

Llevando Patroni a la producción

Como resultado, se nos ocurrió el siguiente plan:

  • Implemente Consul-template en los servidores de PgBouncer y ejecútelo;
  • Actualizaciones de PostgreSQL a la versión 11.2;
  • Cambie el nombre del clúster;
  • Iniciando el Clúster Patroni.

Al mismo tiempo, nuestro esquema nos permite realizar el primer punto casi en cualquier momento, podemos eliminar cada PgBouncer del trabajo por turnos e implementar y ejecutar consul-template en él. Así lo hicimos.

Para una implementación rápida, usamos Ansible, ya que probamos todos los playbooks en un entorno de prueba, y el tiempo de ejecución del script completo fue de 1,5 a 2 minutos para cada fragmento. Podríamos implementar todo a la vez en cada fragmento sin detener nuestro servicio, pero tendríamos que apagar cada PostgreSQL durante varios minutos. En este caso, los usuarios cuyos datos están en este fragmento no podrían trabajar completamente en este momento, y esto es inaceptable para nosotros.

La salida a esta situación fue el mantenimiento planificado, que se realiza cada 3 meses. Esta es una ventana para el trabajo programado, cuando cerramos completamente nuestro servicio y actualizamos nuestras instancias de base de datos. Quedaba una semana hasta la próxima ventana, y decidimos esperar y prepararnos más. Durante el tiempo de espera, nos aseguramos adicionalmente: para cada fragmento de PostgreSQL, creamos una réplica de repuesto en caso de que no se mantuvieran los datos más recientes y agregamos una nueva instancia para cada fragmento, que debería convertirse en una nueva réplica en el clúster Patroni. para no ejecutar un comando para borrar datos. Todo esto ayudó a minimizar el riesgo de error.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Reiniciamos nuestro servicio, todo funcionó como debería, los usuarios continuaron trabajando, pero en los gráficos notamos una carga anormalmente alta en los servidores de Consul.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

¿Por qué no vimos esto en el entorno de prueba? Este problema ilustra muy bien que es necesario seguir el principio de Infraestructura como código y refinar toda la infraestructura, desde los entornos de prueba hasta la producción. De lo contrario, es muy fácil obtener el problema que tenemos. ¿Qué pasó? Consul apareció primero en producción y luego en entornos de prueba, como resultado, en entornos de prueba, la versión de Consul era superior a la de producción. Solo en uno de los releases se resolvió una fuga de CPU al trabajar con consul-template. Por lo tanto, simplemente actualizamos Consul, resolviendo así el problema.

Reiniciar el clúster de Patroni

Sin embargo, tenemos un nuevo problema, que ni siquiera sospechamos. Al actualizar Consul, simplemente eliminamos el nodo Consul del clúster usando el comando Consul Leave → Patroni se conecta a otro servidor Consul → todo funciona. Pero cuando llegamos a la última instancia del clúster de Consul y le enviamos el comando de licencia de cónsul, todos los clústeres de Patroni simplemente se reiniciaron y en los registros vimos el siguiente error:

ERROR: get_cluster
Traceback (most recent call last):
...
RetryFailedError: 'Exceeded retry deadline'
ERROR: Error communicating with DCS
<b>LOG: database system is shut down</b>

El clúster de Patroni no pudo recuperar información sobre su clúster y se reinició.

Para encontrar una solución, contactamos a los autores de Patroni a través de un problema en github. Ellos sugirieron mejoras a nuestros archivos de configuración:

consul:
 consul.checks: []
bootstrap:
 dcs:
   retry_timeout: 8

Pudimos replicar el problema en un entorno de prueba y probamos estas opciones allí, pero desafortunadamente no funcionaron.

El problema sigue sin resolverse. Planeamos probar las siguientes soluciones:

  • Use Consul-agent en cada instancia de clúster de Patroni;
  • Solucione el problema en el código.

Entendemos dónde ocurrió el error: el problema probablemente sea el uso del tiempo de espera predeterminado, que no se anula a través del archivo de configuración. Cuando se elimina el último servidor Consul del clúster, todo el clúster Consul se bloquea durante más de un segundo, por lo que Patroni no puede obtener el estado del clúster y lo reinicia por completo.

Afortunadamente, no encontramos más errores.

Resultados del uso de Patroni

Después del exitoso lanzamiento de Patroni, agregamos una réplica adicional en cada clúster. Ahora en cada grupo hay una apariencia de quórum: un líder y dos réplicas, como red de seguridad en caso de división del cerebro al cambiar.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia en implementación

Patroni ha estado trabajando en la producción durante más de tres meses. Durante este tiempo, ya ha logrado ayudarnos. Recientemente, el líder de uno de los clústeres murió en AWS, la conmutación por error automática funcionó y los usuarios continuaron trabajando. Patroni cumplió su cometido principal.

Un pequeño resumen del uso de Patroni:

  • Facilidad de cambios de configuración. Basta con cambiar la configuración en una instancia y se cargará en todo el clúster. Si es necesario reiniciar para aplicar la nueva configuración, Patroni se lo hará saber. Patroni puede reiniciar todo el clúster con un solo comando, lo que también es muy conveniente.
  • La conmutación por error automática funciona y ya ha logrado ayudarnos.
  • Actualización de PostgreSQL sin tiempo de inactividad de la aplicación. Primero debe actualizar las réplicas a la nueva versión, luego cambiar el líder en el clúster Patroni y actualizar el líder anterior. En este caso, se produce la prueba necesaria de conmutación por error automática.

Fuente: habr.com

Añadir un comentario