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

No artigo contarei como abordamos o tema da tolerancia a fallos de PostgreSQL, por que se fixo importante para nós e que pasou ao final.

Temos un servizo moi cargado: 2,5 millóns de usuarios en todo o mundo, máis de 50 usuarios activos todos os días. Os servidores están situados en Amazone nunha rexión de Irlanda: máis de 100 servidores diferentes funcionan constantemente, dos cales case 50 están con bases de datos.

Todo o backend é unha gran aplicación Java con estado monolítico que mantén unha conexión constante de websocket co cliente. Cando varios usuarios traballan no mesmo taboleiro ao mesmo tempo, todos ven os cambios en tempo real, porque escribimos cada cambio na base de datos. Temos preto de 10 solicitudes por segundo ás nosas bases de datos. Na carga máxima en Redis, escribimos 80-100 XNUMX solicitudes por segundo.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Por que cambiamos de Redis a PostgreSQL

Inicialmente, o noso servizo funcionaba con Redis, unha tenda de valores clave que almacena todos os datos na memoria RAM do servidor.

Pros de Redis:

  1. Alta velocidade de resposta, porque todo está almacenado na memoria;
  2. Facilidade de copia de seguridade e replicación.

Contras de Redis para nós:

  1. Non hai transaccións reais. Tentamos simulalos a nivel da nosa aplicación. Desafortunadamente, isto non sempre funcionou ben e requiriu escribir código moi complexo.
  2. A cantidade de datos está limitada pola cantidade de memoria. A medida que aumenta a cantidade de datos, a memoria crecerá e, ao final, atoparémonos coas características da instancia seleccionada, o que en AWS obriga a deter o noso servizo para cambiar o tipo de instancia.
  3. É necesario manter constantemente un baixo nivel de latencia, porque. temos un número moi grande de solicitudes. O nivel de retardo óptimo para nós é de 17-20 ms. A un nivel de 30-40 ms, obtemos respostas longas ás solicitudes da nosa aplicación e á degradación do servizo. Desafortunadamente, isto pasounos en setembro de 2018, cando unha das instancias con Redis por algún motivo recibiu unha latencia 2 veces máis do habitual. Para resolver o problema, paramos o servizo ao mediodía por mantemento non programado e substituímos a problemática instancia de Redis.
  4. É doado obter incoherencias de datos mesmo con pequenos erros no código e despois pasar moito tempo escribindo código para corrixir estes datos.

Tivemos en conta os contras e decatámonos de que necesitabamos pasar a algo máis cómodo, con transaccións normais e menos dependencia da latencia. Realizou investigacións, analizou moitas opcións e elixiu PostgreSQL.

Xa levamos 1,5 anos trasladando a unha nova base de datos e só movemos unha pequena parte dos datos, polo que agora estamos a traballar simultaneamente con Redis e PostgreSQL. Escribe máis información sobre as fases de desprazamento e cambio de datos entre bases de datos artigo do meu compañeiro.

Cando comezamos a movernos, a nosa aplicación traballaba directamente coa base de datos e accedeu ao mestre Redis e PostgreSQL. O clúster de PostgreSQL constaba dun mestre e dunha réplica con replicación asíncrona. Así quedou o esquema da base de datos:
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Implementación de PgBouncer

Mentres nos movíamos, o produto tamén se desenvolveu: aumentou o número de usuarios e o número de servidores que traballaban con PostgreSQL, e comezamos a carecer de conexións. PostgreSQL crea un proceso separado para cada conexión e consume recursos. Pode aumentar o número de conexións ata certo punto, se non, hai unha posibilidade de obter un rendemento subóptimo da base de datos. A opción ideal en tal situación sería escoller un xestor de conexións que quedará diante da base.

Tivemos dúas opcións para o xestor de conexións: Pgpool e PgBouncer. Pero o primeiro non admite o modo transaccional de traballar coa base de datos, polo que escollemos PgBouncer.

Establecemos o seguinte esquema de traballo: a nosa aplicación accede a un PgBouncer, detrás do cal hai mestres PostgreSQL, e detrás de cada mestre hai unha réplica con replicación asíncrona.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Ao mesmo tempo, non podíamos almacenar toda a cantidade de datos en PostgreSQL e a velocidade de traballo coa base de datos era importante para nós, polo que comezamos a dividir PostgreSQL a nivel de aplicación. O esquema descrito anteriormente é relativamente conveniente para iso: ao engadir un novo fragmento PostgreSQL, abonda con actualizar a configuración de PgBouncer e a aplicación pode traballar inmediatamente co novo fragmento.

Conmutación por error de PgBouncer

Este esquema funcionou ata o momento en que morreu a única instancia de PgBouncer. Estamos en AWS, onde todas as instancias están a executarse en hardware que morre periódicamente. Nestes casos, a instancia simplemente pasa a un novo hardware e funciona de novo. Isto ocorreu con PgBouncer, pero quedou non dispoñible. O resultado desta caída foi a indisponibilidade do noso servizo durante 25 minutos. AWS recomenda utilizar a redundancia do lado do usuario para tales situacións, que non estaba implementada no noso país naquel momento.

Despois diso, pensamos seriamente na tolerancia a fallos dos clústeres PgBouncer e PostgreSQL, porque unha situación similar podería ocorrer con calquera instancia da nosa conta de AWS.

Creamos o esquema de tolerancia a fallos de PgBouncer do seguinte xeito: todos os servidores de aplicacións acceden ao Network Load Balancer, detrás do cal hai dous PgBouncers. Cada PgBouncer mira o mesmo mestre PostgreSQL de cada fragmento. Se se produce unha falla de instancia de AWS de novo, todo o tráfico redirixe a través doutro PgBouncer. A conmutación por falla de Network Load Balancer é proporcionada por AWS.

Este esquema fai que sexa fácil engadir novos servidores PgBouncer.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

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

Ao resolver este problema, consideramos diferentes opcións: failover auto-escrita, repmgr, AWS RDS, Patroni.

Guións autoescritos

Poden supervisar o traballo do mestre e, en caso de falla, promover a réplica ao mestre e actualizar a configuración de PgBouncer.

As vantaxes deste enfoque son a máxima sinxeleza, porque escribes guións ti mesmo e comprendes exactamente como funcionan.

Contras:

  • É posible que o mestre non falecera, senón que se produciu un fallo na rede. Failover, sen saber isto, promoverá a réplica ao mestre, mentres que o mestre antigo seguirá traballando. Como resultado, conseguiremos dous servidores no papel de mestre e non saberemos cal deles ten os últimos datos actualizados. Esta situación tamén se denomina split-brain;
  • Quedamos sen resposta. Na nosa configuración, o mestre e unha réplica, despois de cambiar, a réplica móvese ata o mestre e xa non temos réplicas, polo que temos que engadir manualmente unha nova réplica;
  • Necesitamos un seguimento adicional da operación de failover, mentres que temos 12 fragmentos PostgreSQL, o que significa que temos que supervisar 12 clústeres. Cun aumento no número de fragmentos, tamén debes lembrar de actualizar a conmutación por fallo.

O failover autoescrito parece moi complicado e require soporte non trivial. Cun único clúster de PostgreSQL, esta sería a opción máis sinxela, pero non se escala, polo que non é axeitado para nós.

Repmgr

Xestor de replicación para clústeres PostgreSQL, que pode xestionar o funcionamento dun clúster PostgreSQL. Ao mesmo tempo, non ten unha conmutación automática por falla fóra da caixa, polo que para traballar terás que escribir o teu propio "envoltorio" encima da solución acabada. Así que todo pode resultar aínda máis complicado que cos guións escritos por si mesmo, así que nin sequera probamos Repmgr.

AWS RDS

Soporta todo o que necesitamos, sabe como facer copias de seguridade e mantén un conxunto de conexións. Ten conmutación automática: cando o mestre morre, a réplica convértese no novo mestre e AWS cambia o rexistro dns ao novo mestre, mentres que as réplicas pódense localizar en diferentes AZ.

As desvantaxes inclúen a falta de axustes finos. Como exemplo de axuste fino: as nosas instancias teñen restricións para as conexións tcp, que, por desgraza, non se poden facer 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

Ademais, AWS RDS é case o dobre que o prezo da instancia normal, que foi o principal motivo para abandonar esta solución.

Patroi

Este é un modelo de Python para xestionar PostgreSQL cunha boa documentación, falla automática e código fonte en github.

Pros de Patroni:

  • Descríbese cada parámetro de configuración, está claro como funciona;
  • O failover automático funciona fóra da caixa;
  • Escrito en python, e como nós mesmos escribimos moito en python, será máis doado facernos fronte aos problemas e, se cadra, mesmo axudar ao desenvolvemento do proxecto;
  • Xestiona totalmente PostgreSQL, permítelle cambiar a configuración en todos os nodos do clúster á vez e, se o clúster necesita reiniciarse para aplicar a nova configuración, pódese facer de novo usando Patroni.

Contras:

  • Non está claro na documentación como traballar con PgBouncer correctamente. Aínda que é difícil chamalo menos, porque a tarefa de Patroni é xestionar PostgreSQL, e como irán as conexións a Patroni xa é o noso problema;
  • Hai poucos exemplos de implementación de Patroni en grandes volumes, mentres que hai moitos exemplos de implementación desde cero.

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

Proceso de implantación de Patroni

Antes de Patroni, tiñamos 12 fragmentos PostgreSQL nunha configuración dun mestre e unha réplica con replicación asíncrona. Os servidores de aplicacións accedían ás bases de datos a través do Network Load Balancer, detrás do cal había dúas instancias con PgBouncer, e detrás delas estaban todos os servidores PostgreSQL.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Para implementar Patroni, necesitabamos seleccionar unha configuración de clúster de almacenamento distribuído. Patroni traballa con sistemas de almacenamento de configuración distribuída como etcd, Zookeeper, Consul. Só temos un clúster de Cónsul completo no mercado, que funciona en conxunto con Vault e xa non o usamos. Unha gran razón para comezar a usar Consul para o propósito previsto.

Como traballa Patroni con Consul

Temos un clúster Consul, que consta de tres nodos, e un clúster Patroni, que consta dun líder e unha réplica (en Patroni, o mestre chámase líder do clúster e os escravos chámanse réplicas). Cada instancia do clúster Patroni envía constantemente información sobre o estado do clúster a Consul. Por iso, desde Consul sempre podes coñecer a configuración actual do clúster Patroni e quen é o líder neste momento.

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

Para conectar Patroni a Consul basta con estudar a documentación oficial, que di que cómpre especificar un host no formato http ou https, dependendo de como traballemos con Consul, e o 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 sinxelo, pero aquí comezan as trampas. Con Consul, traballamos nunha conexión segura a través de https e a nosa configuración de conexión terá o seguinte aspecto:

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

Pero iso non funciona. No inicio, Patroni non pode conectarse a Consul, porque tenta pasar por http de todos os xeitos.

O código fonte de Patroni axudou a resolver o problema. Menos mal que está escrito en python. Resulta que o parámetro host non se analiza de ningún xeito e o protocolo debe especificarse no esquema. Así é o bloque de configuración de traballo para traballar con Consul para nós:

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

cónsul-modelo

Entón, escollimos o almacenamento para a configuración. Agora necesitamos entender como PgBouncer cambiará a súa configuración ao cambiar o líder no clúster Patroni. Non hai resposta a esta pregunta na documentación, porque. alí, en principio, non se describe o traballo con PgBouncer.

Na procura dunha solución, atopamos un artigo (desafortunadamente non recordo o título) onde estaba escrito que Сonsul-template axudou moito a combinar PgBouncer e Patroni. Isto levounos a investigar como funciona Consul-template.

Resultou que Consul-template supervisa constantemente a configuración do clúster PostgreSQL en Consul. Cando o líder cambia, actualiza a configuración de PgBouncer e envía un comando para recargalo.

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

Unha gran vantaxe do modelo é que se almacena como código, polo que ao engadir un novo fragmento abonda con facer unha nova confirmación e actualizar o modelo automaticamente, apoiando o principio de Infraestrutura como código.

Nova arquitectura con Patroni

Como resultado, obtivemos o seguinte esquema de traballo:
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Todos os servidores de aplicacións acceden ao equilibrador → hai dúas instancias de PgBouncer detrás del → en cada instancia, lánzase o modelo Consul, que supervisa o estado de cada clúster de Patroni e supervisa a relevancia da configuración de PgBouncer, que envía solicitudes ao líder actual. de cada cluster.

Proba manual

Executamos este esquema antes de lanzala nun pequeno ambiente de proba e comprobamos o funcionamento da conmutación automática. Abriron o taboleiro, moveron o adhesivo e nese momento "mataron" ao líder do clúster. En AWS, isto é tan sinxelo como apagar a instancia a través da consola.

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

O adhesivo volveu de novo en 10-20 segundos e despois comezou a moverse de novo con normalidade. Isto significa que o clúster Patroni funcionou correctamente: cambiou o líder, enviou a información a Сonsul e Сonsul-template colleu inmediatamente esta información, substituíu a configuración de PgBouncer e enviou o comando para recargar.

Como sobrevivir cunha carga elevada e manter o tempo de inactividade mínimo?

Todo funciona perfectamente! Pero hai novas preguntas: como funcionará baixo carga elevada? Como lanzar todo en produción de forma rápida e segura?

O ambiente de proba no que realizamos probas de carga axúdanos a responder á primeira pregunta. É completamente idéntico á produción en termos de arquitectura e xerou datos de proba que son aproximadamente igual en volume á produción. Decidimos simplemente "matar" a un dos mestres de PostgreSQL durante a proba e ver que pasa. Pero antes diso, é importante comprobar o rolling automático, porque neste ambiente temos varios fragmentos de PostgreSQL, polo que teremos excelentes probas dos scripts de configuración antes da produción.

Ambas tarefas parecen ambiciosas, pero temos PostgreSQL 9.6. Podemos actualizar inmediatamente a 11.2?

Decidimos facelo en 2 pasos: primeiro actualizar a 11.2 e despois lanzar Patroni.

Actualización de PostgreSQL

Para actualizar rapidamente a versión de PostgreSQL, use a opción -k, no que se crean ligazóns físicas no disco e non é necesario copiar os seus datos. En bases de 300-400 GB, a actualización leva 1 segundo.

Temos moitos fragmentos, polo que a actualización debe facerse automaticamente. Para iso, escribimos un manual de Ansible que se encarga de todo o proceso de actualización:

/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='

É importante ter en conta aquí que antes de comezar a actualización, debes realizala co parámetro --comprobarpara asegurarse de que pode actualizar. O noso script tamén fai a substitución de configuracións durante a duración da actualización. O noso guión completouse en 30 segundos, o que é un excelente resultado.

Inicia Patroni

Para resolver o segundo problema, basta con mirar a configuración de Patroni. O repositorio oficial ten un exemplo de configuración con initdb, que se encarga de inicializar unha nova base de datos cando inicia Patroni por primeira vez. Pero como xa temos unha base de datos preparada, simplemente eliminamos esta sección da configuración.

Cando comezamos a instalar Patroni nun clúster PostgreSQL xa existente e a executalo, atopamos un novo problema: ambos os servidores comezaron como líderes. Patroni non sabe nada sobre o estado inicial do clúster e tenta iniciar ambos servidores como dous clústeres separados co mesmo nome. Para resolver este problema, cómpre eliminar o directorio cos datos do escravo:

rm -rf /var/lib/postgresql/

Isto só hai que facer no escravo!

Cando se conecta unha réplica limpa, Patroni fai un líder de copia de seguridade base e restaúrao na réplica, e despois recupera o estado actual segundo os rexistros de wal.

Outra dificultade que atopamos é que todos os clústeres de PostgreSQL reciben o nome de principal por defecto. Cando cada cluster non sabe nada do outro, isto é normal. Pero cando queres usar Patroni, todos os clústeres deben ter un nome único. A solución é cambiar o nome do clúster na configuración de PostgreSQL.

proba de carga

Lanzamos unha proba que simula a experiencia do usuario nos taboleiros. Cando a carga alcanzou o noso valor medio diario, repetimos exactamente a mesma proba, desactivamos unha instancia co líder PostgreSQL. A failover automática funcionou como esperabamos: Patroni cambiou o líder, Consul-template actualizou a configuración de PgBouncer e enviou un comando para recargar. Segundo os nosos gráficos en Grafana, estaba claro que hai atrasos de 20-30 segundos e unha pequena cantidade de erros dos servidores asociados á conexión á base de datos. Esta é unha situación normal, tales valores son aceptables para a nosa conmutación por fallo e son definitivamente mellores que o tempo de inactividade do servizo.

Levando Patroni á produción

Como resultado, elaboramos o seguinte plan:

  • Implementa o modelo Consul nos servidores de PgBouncer e lánzao;
  • Actualizacións de PostgreSQL á versión 11.2;
  • Cambia o nome do clúster;
  • Comezando o Clúster Patroni.

Ao mesmo tempo, o noso esquema permítenos facer o primeiro punto case en calquera momento, podemos eliminar cada PgBouncer do traballo á súa vez e implementar e executar consul-template nel. Así o fixemos.

Para unha implantación rápida, usamos Ansible, xa que xa probamos todos os libros de xogo nun ambiente de proba e o tempo de execución do script completo era de 1,5 a 2 minutos para cada fragmento. Poderíamos lanzar todo á súa vez en cada fragmento sen deter o noso servizo, pero teriamos que desactivar cada PostgreSQL durante varios minutos. Neste caso, os usuarios cuxos datos están neste fragmento non poderían funcionar completamente neste momento, e isto é inaceptable para nós.

A saída desta situación foi o mantemento previsto, que se realiza cada 3 meses. Esta é unha xanela para o traballo programado, cando pechamos completamente o noso servizo e actualizamos as nosas instancias de base de datos. Quedaba unha semana para a seguinte fiestra, e decidimos esperar e prepararnos máis. Durante o tempo de espera, asegurámonos adicionalmente: para cada fragmento de PostgreSQL, suscitamos unha réplica de reserva en caso de non conservar os datos máis recentes e engadimos unha nova instancia para cada fragmento, que debería converterse nunha nova réplica no clúster Patroni. para non executar un comando para eliminar datos . Todo isto axudou a minimizar o risco de erro.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Reiniciamos o noso servizo, todo funcionou como debía, os usuarios seguiron traballando, pero nas gráficas observamos unha carga anormalmente alta nos servidores de Consul.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Por que non vimos isto no ambiente de proba? Este problema ilustra moi ben que é necesario seguir o principio de Infraestrutura como código e perfeccionar toda a infraestrutura, desde os ambientes de proba ata a produción. Se non, é moi doado conseguir o problema que temos. Que pasou? Consul apareceu primeiro en produción, e despois en ambientes de proba, como resultado, en ambientes de proba, a versión de Consul era superior á de produción. Só nunha das versións, resolveuse unha fuga de CPU ao traballar con consul-template. Polo tanto, simplemente actualizamos Consul, resolvendo así o problema.

Reinicie o cluster Patroni

Con todo, temos un novo problema, que nin sequera sospeitabamos. Ao actualizar Consul, simplemente eliminamos o nodo Consul do clúster mediante o comando consul leave → Patroni conéctase a outro servidor Consul → todo funciona. Pero cando chegamos á última instancia do clúster Cónsul e enviamos a el o comando consul leave, todos os clústeres de Patroni simplemente reiniciáronse e nos rexistros vimos o seguinte erro:

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>

O clúster de Patroni non puido recuperar información sobre o seu clúster e reiniciouse.

Para atopar unha solución, contactamos cos autores de Patroni a través dun problema en github. Suxeriron melloras nos nosos ficheiros de configuración:

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

Puidemos replicar o problema nun ambiente de proba e probar alí estas opcións, pero lamentablemente non funcionaron.

O problema aínda segue sen resolverse. Pensamos probar as seguintes solucións:

  • Use Consul-agent en cada instancia de clúster de Patroni;
  • Resolve o problema no código.

Entendemos onde ocorreu o erro: o problema probablemente sexa o uso do tempo de espera predeterminado, que non se anula a través do ficheiro de configuración. Cando o último servidor Consul é eliminado do clúster, todo o clúster Consul pende durante máis dun segundo, polo que Patroni non pode obter o estado do clúster e reinicia completamente todo o clúster.

Afortunadamente, non atopamos máis erros.

Resultados do uso de Patroni

Despois do lanzamento exitoso de Patroni, engadimos unha réplica adicional en cada clúster. Agora en cada clúster hai unha apariencia de quórum: un líder e dúas réplicas, para a rede de seguridade en caso de división do cerebro ao cambiar.
Clúster de conmutación por error PostgreSQL + Patroni. Experiencia de implantación

Patroni leva máis de tres meses traballando na produción. Durante este tempo, xa conseguiu axudarnos. Recentemente, o líder dun dos clústeres morreu en AWS, o failover automático funcionou e os usuarios continuaron traballando. Patroni cumpriu o seu cometido principal.

Un pequeno resumo do uso de Patroni:

  • Facilidade de cambios de configuración. É suficiente con cambiar a configuración nunha instancia e subirase a todo o clúster. Se é necesario reiniciar para aplicar a nova configuración, Patroni informarao. Patroni pode reiniciar todo o clúster cun só comando, o que tamén é moi cómodo.
  • A failover automática funciona e xa conseguiu axudarnos.
  • Actualización de PostgreSQL sen tempo de inactividade da aplicación. Primeiro debes actualizar as réplicas á nova versión, despois cambiar o líder no clúster Patroni e actualizar o líder antigo. Neste caso, prodúcese a proba necesaria de fallo automático.

Fonte: www.habr.com

Engadir un comentario