Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

I artikkelen vil jeg fortelle deg hvordan vi nærmet oss spørsmålet om PostgreSQL-feiltoleranse, hvorfor det ble viktig for oss og hva som skjedde til slutt.

Vi har en svært belastet tjeneste: 2,5 millioner brukere over hele verden, 50K+ aktive brukere hver dag. Serverne er lokalisert i Amazone i en region i Irland: 100+ forskjellige servere er konstant i drift, hvorav nesten 50 med databaser.

Hele backend er en stor monolitisk stateful Java-applikasjon som holder en konstant websocket-forbindelse med klienten. Når flere brukere jobber på samme tavle samtidig, ser de alle endringene i sanntid, fordi vi skriver hver endring til databasen. Vi har omtrent 10K forespørsler per sekund til våre databaser. Ved toppbelastning i Redis skriver vi 80-100K forespørsler per sekund.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Hvorfor vi byttet fra Redis til PostgreSQL

Til å begynne med fungerte tjenesten vår med Redis, et nøkkelverdilager som lagrer all data i serverens RAM.

Fordeler med Redis:

  1. Høy responshastighet, pga alt er lagret i minnet;
  2. Enkel backup og replikering.

Ulemper med Redis for oss:

  1. Det er ingen reelle transaksjoner. Vi prøvde å simulere dem på applikasjonsnivået vårt. Dessverre fungerte ikke dette alltid bra og krevde å skrive veldig kompleks kode.
  2. Mengden data er begrenset av mengden minne. Etter hvert som datamengden øker, vil minnet vokse, og til slutt vil vi støte på egenskapene til den valgte forekomsten, som i AWS krever å stoppe tjenesten vår for å endre type forekomst.
  3. Det er nødvendig å hele tiden opprettholde et lavt latensnivå, fordi. vi har et veldig stort antall forespørsler. Det optimale forsinkelsesnivået for oss er 17-20 ms. På et nivå på 30-40 ms får vi lange svar på forespørsler fra vår applikasjon og degradering av tjenesten. Dessverre skjedde dette med oss ​​i september 2018, da et av tilfellene med Redis av en eller annen grunn fikk ventetid 2 ganger mer enn vanlig. For å løse problemet stoppet vi tjenesten midt på dagen for ikke-planlagt vedlikehold og erstattet den problematiske Redis-forekomsten.
  4. Det er lett å få datainkonsistens selv med mindre feil i koden og deretter bruke mye tid på å skrive kode for å rette opp disse dataene.

Vi tok hensyn til ulempene og innså at vi måtte flytte til noe mer praktisk, med normale transaksjoner og mindre avhengighet av ventetid. Gjennomførte undersøkelser, analyserte mange alternativer og valgte PostgreSQL.

Vi har flyttet til en ny database i 1,5 år allerede og har bare flyttet en liten del av dataene, så nå jobber vi samtidig med Redis og PostgreSQL. Mer informasjon om stadiene for å flytte og bytte data mellom databaser er skrevet inn min kollegas artikkel.

Da vi først begynte å flytte, jobbet applikasjonen vår direkte med databasen og fikk tilgang til master Redis og PostgreSQL. PostgreSQL-klyngen besto av en master og en replika med asynkron replikering. Slik så databaseskjemaet ut:
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Implementering av PgBouncer

Mens vi flyttet, utviklet produktet seg også: antall brukere og antall servere som fungerte med PostgreSQL økte, og vi begynte å mangle tilkoblinger. PostgreSQL oppretter en egen prosess for hver tilkobling og bruker ressurser. Du kan øke antall tilkoblinger opp til et visst punkt, ellers er det en sjanse for å få suboptimal databaseytelse. Det ideelle alternativet i en slik situasjon vil være å velge en tilkoblingsadministrator som vil stå foran basen.

Vi hadde to alternativer for tilkoblingsadministratoren: Pgpool og PgBouncer. Men den første støtter ikke transaksjonsmodusen for å jobbe med databasen, så vi valgte PgBouncer.

Vi har satt opp følgende arbeidsskjema: applikasjonen vår har tilgang til én PgBouncer, bak som er PostgreSQL-mastere, og bak hver master er en replika med asynkron replikering.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Samtidig kunne vi ikke lagre hele datamengden i PostgreSQL og hastigheten på arbeidet med databasen var viktig for oss, så vi begynte å skjære PostgreSQL på applikasjonsnivå. Opplegget beskrevet ovenfor er relativt praktisk for dette: når du legger til et nytt PostgreSQL-shard, er det nok å oppdatere PgBouncer-konfigurasjonen, og applikasjonen kan umiddelbart fungere med den nye sharden.

PgBouncer failover

Denne ordningen fungerte til øyeblikket da den eneste PgBouncer-forekomsten døde. Vi er i AWS, der alle forekomster kjører på maskinvare som dør med jevne mellomrom. I slike tilfeller flytter instansen ganske enkelt til ny maskinvare og fungerer igjen. Dette skjedde med PgBouncer, men det ble utilgjengelig. Resultatet av høsten var at tjenesten vår ikke var tilgjengelig i 25 minutter. AWS anbefaler å bruke redundans på brukersiden for slike situasjoner, som ikke ble implementert i vårt land på den tiden.

Etter det tenkte vi seriøst på feiltoleransen til PgBouncer- og PostgreSQL-klynger, fordi en lignende situasjon kan skje med en hvilken som helst instans i AWS-kontoen vår.

Vi bygde PgBouncer-feiltoleranseskjemaet som følger: alle applikasjonsservere får tilgang til Network Load Balancer, bak som det er to PgBouncers. Hver PgBouncer ser på den samme PostgreSQL-mesteren for hvert shard. Hvis en AWS-forekomst krasjer igjen, blir all trafikk omdirigert gjennom en annen PgBouncer. Network Load Balancer failover leveres av AWS.

Denne ordningen gjør det enkelt å legge til nye PgBouncer-servere.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Opprett en PostgreSQL Failover-klynge

Når vi løste dette problemet, vurderte vi forskjellige alternativer: selvskrevet failover, repmgr, AWS RDS, Patroni.

Selvskrevne manus

De kan overvåke arbeidet til masteren og, i tilfelle dens feil, promotere replikaen til masteren og oppdatere PgBouncer-konfigurasjonen.

Fordelene med denne tilnærmingen er maksimal enkelhet, fordi du skriver skript selv og forstår nøyaktig hvordan de fungerer.

Cons:

  • Masteren har kanskje ikke dødd, i stedet kan det ha oppstått en nettverksfeil. Failover, uvitende om dette, vil fremme replikaen til mesteren, mens den gamle mesteren vil fortsette å jobbe. Som et resultat vil vi få to servere i rollen som master og vi vil ikke vite hvem av dem som har de siste oppdaterte dataene. Denne situasjonen kalles også split-brain;
  • Vi ble stående uten svar. I vår konfigurasjon, master og en replika, etter bytte, flytter replikaen opp til masteren og vi har ikke lenger replikaer, så vi må manuelt legge til en ny replika;
  • Vi trenger ekstra overvåking av failover-operasjonen, mens vi har 12 PostgreSQL-shards, som betyr at vi må overvåke 12 klynger. Med en økning i antall shards må du også huske å oppdatere failoveren.

Selvskrevet failover ser veldig komplisert ut og krever ikke-triviell støtte. Med en enkelt PostgreSQL-klynge ville dette være det enkleste alternativet, men det skaleres ikke, så det passer ikke for oss.

Repmgr

Replikeringsbehandling for PostgreSQL-klynger, som kan administrere driften av en PostgreSQL-klynge. Samtidig har den ikke en automatisk failover ut av esken, så for arbeid må du skrive din egen "wrapper" på toppen av den ferdige løsningen. Så alt kan bli enda mer komplisert enn med selvskrevne skript, så vi prøvde ikke engang Repmgr.

AWS RDS

Støtter alt vi trenger, vet hvordan vi lager sikkerhetskopier og vedlikeholder en pool av tilkoblinger. Den har automatisk veksling: når masteren dør, blir replikaen den nye masteren, og AWS endrer dns-posten til den nye masteren, mens replikaene kan lokaliseres i forskjellige AZ-er.

Ulempene inkluderer mangelen på finjusteringer. Som et eksempel på finjustering: våre instanser har begrensninger for tcp-tilkoblinger, som dessverre ikke kan gjøres i RDS:

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

I tillegg er AWS RDS nesten dobbelt så dyrt som den vanlige forekomstprisen, som var hovedgrunnen til å forlate denne løsningen.

Patroni

Dette er en python-mal for å administrere PostgreSQL med god dokumentasjon, automatisk failover og kildekode på github.

Fordeler med Patroni:

  • Hver konfigurasjonsparameter er beskrevet, det er tydelig hvordan det fungerer;
  • Automatisk failover fungerer ut av esken;
  • Skrevet i python, og siden vi selv skriver mye i python, vil det være lettere for oss å håndtere problemer og kanskje til og med hjelpe utviklingen av prosjektet;
  • Fullt administrerer PostgreSQL, lar deg endre konfigurasjonen på alle noder i klyngen samtidig, og hvis klyngen må startes på nytt for å bruke den nye konfigurasjonen, kan dette gjøres igjen ved å bruke Patroni.

Cons:

  • Det fremgår ikke klart av dokumentasjonen hvordan man jobber med PgBouncer riktig. Selv om det er vanskelig å kalle det et minus, fordi Patronis oppgave er å administrere PostgreSQL, og hvordan tilkoblinger til Patroni vil gå er allerede vårt problem;
  • Det er få eksempler på implementering av Patroni på store volumer, mens det er mange eksempler på implementering fra bunnen av.

Som et resultat valgte vi Patroni for å opprette en failover-klynge.

Implementeringsprosess for Patroni

Før Patroni hadde vi 12 PostgreSQL-shards i en konfigurasjon av en master og en replika med asynkron replikering. Applikasjonsserverne fikk tilgang til databasene gjennom Network Load Balancer, bak som var to instanser med PgBouncer, og bak dem var alle PostgreSQL-serverne.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

For å implementere Patroni, måtte vi velge en distribuert lagringsklyngekonfigurasjon. Patroni jobber med distribuerte konfigurasjonslagringssystemer som etcd, Zookeeper, Consul. Vi har nettopp en fullverdig Consul-klynge på markedet, som fungerer sammen med Vault, og vi bruker den ikke lenger. En god grunn til å begynne å bruke Consul til det tiltenkte formålet.

Hvordan Patroni jobber med Consul

Vi har en Consul-klynge, som består av tre noder, og en Patroni-klynge, som består av en leder og en replika (i Patroni kalles mesteren klyngelederen, og slavene kalles replikaer). Hver forekomst av Patroni-klyngen sender konstant informasjon om klyngens tilstand til Consul. Derfor, fra Consul kan du alltid finne ut den nåværende konfigurasjonen av Patroni-klyngen og hvem som er leder for øyeblikket.

Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

For å koble Patroni til Consul, er det nok å studere den offisielle dokumentasjonen, som sier at du må spesifisere en vert i http- eller https-formatet, avhengig av hvordan vi jobber med Consul, og tilkoblingsskjemaet, eventuelt:

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

Det ser enkelt ut, men her begynner fallgruvene. Med Consul jobber vi over en sikker tilkobling via https og tilkoblingskonfigurasjonen vår vil se slik ut:

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

Men det går ikke. Ved oppstart kan ikke Patroni koble til Consul, fordi den prøver å gå gjennom http uansett.

Kildekoden til Patroni hjalp til med å håndtere problemet. Bra det er skrevet i python. Det viser seg at vertsparameteren ikke blir analysert på noen måte, og protokollen må spesifiseres i skjemaet. Slik ser arbeidskonfigurasjonsblokken for å jobbe med Consul ut for oss:

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

konsul-mal

Så vi har valgt lagringen for konfigurasjonen. Nå må vi forstå hvordan PgBouncer vil bytte konfigurasjon når du bytter leder i Patroni-klyngen. Det er ikke noe svar på dette spørsmålet i dokumentasjonen, fordi. der er i prinsippet ikke arbeidet med PgBouncer beskrevet.

På leting etter en løsning fant vi en artikkel (jeg husker dessverre ikke tittelen) der det ble skrevet at Сonsul-malen hjalp mye med å pare PgBouncer og Patroni. Dette fikk oss til å undersøke hvordan Consul-mal fungerer.

Det viste seg at Consul-template hele tiden overvåker konfigurasjonen av PostgreSQL-klyngen i Consul. Når lederen endres, oppdaterer den PgBouncer-konfigurasjonen og sender en kommando for å laste den på nytt.

Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Et stort pluss med malen er at den lagres som kode, så når du legger til et nytt shard, er det nok å foreta en ny commit og oppdatere malen automatisk, og støtte prinsippet for Infrastructure as code.

Ny arkitektur med Patroni

Som et resultat fikk vi følgende arbeidsplan:
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Alle applikasjonsservere får tilgang til balanseren → det er to forekomster av PgBouncer bak den → på hver forekomst lanseres Consul-mal, som overvåker statusen til hver Patroni-klynge og overvåker relevansen til PgBouncer-konfigurasjonen, som sender forespørsler til den nåværende lederen av hver klynge.

Manuell testing

Vi kjørte dette opplegget før vi lanserte det på et lite testmiljø og sjekket funksjonen til automatisk veksling. De åpnet brettet, flyttet klistremerket, og i det øyeblikket "drepte" de lederen av klyngen. I AWS er ​​dette så enkelt som å slå av instansen via konsollen.

Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Klistremerket kom tilbake innen 10-20 sekunder, og begynte deretter å bevege seg normalt igjen. Dette betyr at Patroni-klyngen fungerte riktig: den endret lederen, sendte informasjonen til Сonsul, og Сonsul-malen plukket umiddelbart opp denne informasjonen, erstattet PgBouncer-konfigurasjonen og sendte kommandoen for å laste på nytt.

Hvordan overleve under høy belastning og holde nedetiden minimal?

Alt fungerer perfekt! Men det er nye spørsmål: Hvordan vil det fungere under høy belastning? Hvordan rulle ut alt i produksjon raskt og sikkert?

Testmiljøet som vi utfører lasttesting på hjelper oss å svare på det første spørsmålet. Den er helt identisk med produksjon når det gjelder arkitektur og har generert testdata som er omtrent like i volum som produksjon. Vi bestemmer oss for å bare "drepe" en av PostgreSQL-mesterne under testen og se hva som skjer. Men før det er det viktig å sjekke den automatiske rulleringen, for på dette miljøet har vi flere PostgreSQL-shards, så vi vil få utmerket testing av konfigurasjonsskript før produksjon.

Begge oppgavene ser ambisiøse ut, men vi har PostgreSQL 9.6. Kan vi umiddelbart oppgradere til 11.2?

Vi bestemmer oss for å gjøre det i 2 trinn: først oppgradere til 11.2, og deretter starte Patroni.

PostgreSQL-oppdatering

For raskt å oppdatere PostgreSQL-versjonen, bruk alternativet -k, der harde koblinger opprettes på disk og det er ikke nødvendig å kopiere dataene dine. På baser på 300-400 GB tar oppdateringen 1 sekund.

Vi har mange skår, så oppdateringen må gjøres automatisk. For å gjøre dette skrev vi en Ansible-spillebok som håndterer hele oppdateringsprosessen for oss:

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

Det er viktig å merke seg her at før du starter oppgraderingen, må du utføre den med parameteren --kryss avfor å være sikker på at du kan oppgradere. Skriptet vårt foretar også erstatning av konfigurasjoner for varigheten av oppgraderingen. Manuset vårt ble fullført på 30 sekunder, noe som er et utmerket resultat.

Start Patroni

For å løse det andre problemet, se bare på Patroni-konfigurasjonen. Det offisielle depotet har en eksempelkonfigurasjon med initdb, som er ansvarlig for å initialisere en ny database når du først starter Patroni. Men siden vi allerede har en ferdig database, fjernet vi ganske enkelt denne delen fra konfigurasjonen.

Da vi begynte å installere Patroni på en allerede eksisterende PostgreSQL-klynge og kjøre den, fikk vi et nytt problem: begge serverne startet som en leder. Patroni vet ingenting om klyngens tidlige tilstand og prøver å starte begge serverne som to separate klynger med samme navn. For å løse dette problemet må du slette katalogen med data på slaven:

rm -rf /var/lib/postgresql/

Dette må bare gjøres på slaven!

Når en ren replika er koblet til, lager Patroni en basebackup-leder og gjenoppretter den til kopien, og fanger deretter opp med gjeldende tilstand i henhold til veggloggene.

En annen vanskelighet vi møtte er at alle PostgreSQL-klynger er navngitt hoved som standard. Når hver klynge ikke vet noe om den andre, er dette normalt. Men når du vil bruke Patroni, må alle klynger ha et unikt navn. Løsningen er å endre klyngenavnet i PostgreSQL-konfigurasjonen.

belastningstest

Vi har lansert en test som simulerer brukeropplevelse på brett. Når belastningen nådde vår gjennomsnittlige daglige verdi, gjentok vi nøyaktig den samme testen, vi slo av én instans med PostgreSQL-lederen. Den automatiske failoveren fungerte som vi forventet: Patroni byttet leder, Consul-mal oppdaterte PgBouncer-konfigurasjonen og sendte en kommando for å laste på nytt. I følge våre grafer i Grafana var det tydelig at det er forsinkelser på 20-30 sekunder og en liten mengde feil fra serverne knyttet til koblingen til databasen. Dette er en normal situasjon, slike verdier er akseptable for vår failover og er definitivt bedre enn nedetiden for tjenesten.

Tar Patroni til produksjon

Som et resultat kom vi opp med følgende plan:

  • Distribuer Consul-mal til PgBouncer-servere og start;
  • PostgreSQL-oppdateringer til versjon 11.2;
  • Endre navnet på klyngen;
  • Starter Patroni-klyngen.

Samtidig lar ordningen vår oss gjøre det første punktet nesten når som helst, vi kan fjerne hver PgBouncer fra jobb etter tur og distribuere og kjøre konsul-mal på den. Så vi gjorde det.

For rask distribusjon brukte vi Ansible, siden vi allerede har testet alle spillebøkene i et testmiljø, og utførelsestiden for hele skriptet var fra 1,5 til 2 minutter for hvert shard. Vi kunne rulle ut alt etter tur til hvert shard uten å stoppe tjenesten vår, men vi måtte slå av hver PostgreSQL i flere minutter. I dette tilfellet kunne ikke brukere hvis data er på dette fragmentet fungere fullt ut på dette tidspunktet, og dette er uakseptabelt for oss.

Veien ut av denne situasjonen var det planlagte vedlikeholdet, som foregår hver 3. måned. Dette er et vindu for planlagt arbeid, når vi stenger tjenesten vår fullstendig og oppgraderer databaseforekomstene våre. Det var en uke igjen til neste vindu, og vi bestemte oss for å bare vente og forberede oss videre. I løpet av ventetiden sikret vi oss i tillegg: for hver PostgreSQL-shard, hentet vi en reservekopi i tilfelle unnlatelse av å beholde de siste dataene, og la til en ny forekomst for hvert shard, som skulle bli en ny replika i Patroni-klyngen, for ikke å utføre en kommando for å slette data. Alt dette bidro til å minimere risikoen for feil.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Vi startet tjenesten vår på nytt, alt fungerte som det skulle, brukerne fortsatte å jobbe, men på grafene merket vi en unormalt høy belastning på Consul-serverne.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Hvorfor så vi ikke dette i testmiljøet? Denne problemstillingen illustrerer meget godt at det er nødvendig å følge Infrastruktur som kodeprinsipp og foredle hele infrastrukturen, fra testmiljøer til produksjon. Ellers er det veldig lett å få problemet vi fikk. Hva skjedde? Consul dukket først opp på produksjon, og deretter på testmiljøer, som et resultat, på testmiljøer var versjonen av Consul høyere enn på produksjon. Bare i en av utgivelsene ble en CPU-lekkasje løst ved arbeid med konsul-mal. Derfor oppdaterte vi bare Consul, og løste dermed problemet.

Start Patroni-klyngen på nytt

Imidlertid fikk vi et nytt problem, som vi ikke engang hadde mistanke om. Når du oppdaterer Consul, fjerner vi ganske enkelt Consul-noden fra klyngen ved å bruke konsul leave-kommandoen → Patroni kobler til en annen Consul-server → alt fungerer. Men da vi nådde den siste forekomsten av Consul-klyngen og sendte konsul-forlat-kommandoen til den, startet alle Patroni-klynger ganske enkelt på nytt, og i loggene så vi følgende feil:

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>

Patroni-klyngen klarte ikke å hente informasjon om klyngen og startet på nytt.

For å finne en løsning kontaktet vi Patroni-forfatterne via et problem på github. De foreslo forbedringer av konfigurasjonsfilene våre:

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

Vi var i stand til å replikere problemet på et testmiljø og testet disse alternativene der, men de fungerte dessverre ikke.

Problemet er fortsatt uløst. Vi planlegger å prøve følgende løsninger:

  • Bruk Consul-agent på hver Patroni-klyngeforekomst;
  • Løs problemet i koden.

Vi forstår hvor feilen oppsto: problemet er sannsynligvis bruken av standard tidsavbrudd, som ikke overstyres gjennom konfigurasjonsfilen. Når den siste Consul-serveren fjernes fra klyngen, henger hele Consul-klyngen i mer enn et sekund, på grunn av dette kan ikke Patroni få statusen til klyngen og starter hele klyngen på nytt.

Heldigvis har vi ikke støtt på flere feil.

Resultater av bruk av Patroni

Etter den vellykkede lanseringen av Patroni la vi til en ekstra kopi i hver klynge. Nå i hver klynge er det et utseende av et quorum: en leder og to replikaer, for sikkerhetsnett i tilfelle splittet hjerne ved bytte.
Failover Cluster PostgreSQL + Patroni. Implementeringserfaring

Patroni har jobbet med produksjon i mer enn tre måneder. I løpet av denne tiden har han allerede klart å hjelpe oss. Nylig døde lederen av en av klyngene i AWS, automatisk failover fungerte og brukerne fortsatte å jobbe. Patroni oppfylte sin hovedoppgave.

En liten oppsummering av bruken av Patroni:

  • Enkel konfigurasjonsendringer. Det er nok å endre konfigurasjonen på en forekomst, og den vil bli trukket opp til hele klyngen. Hvis en omstart er nødvendig for å bruke den nye konfigurasjonen, vil Patroni gi deg beskjed. Patroni kan starte hele klyngen på nytt med en enkelt kommando, noe som også er veldig praktisk.
  • Automatisk failover fungerer og har allerede klart å hjelpe oss.
  • PostgreSQL-oppdatering uten nedetid for programmet. Du må først oppdatere replikaene til den nye versjonen, deretter endre lederen i Patroni-klyngen og oppdatere den gamle lederen. I dette tilfellet skjer den nødvendige testingen av automatisk failover.

Kilde: www.habr.com

Legg til en kommentar