Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

I artiklen vil jeg fortælle dig, hvordan vi greb spørgsmålet om PostgreSQL-fejltolerance, hvorfor det blev vigtigt for os, og hvad der skete i sidste ende.

Vi har en meget belastet tjeneste: 2,5 millioner brugere på verdensplan, 50K+ aktive brugere hver dag. Serverne er placeret i Amazone i én region i Irland: 100+ forskellige servere arbejder konstant, hvoraf næsten 50 er med databaser.

Hele backend er en stor monolitisk stateful Java-applikation, der holder en konstant websocket-forbindelse med klienten. Når flere brugere arbejder på samme tavle på samme tid, ser de alle ændringerne i realtid, fordi vi skriver hver ændring til databasen. Vi har omkring 10 forespørgsler i sekundet til vores databaser. Ved spidsbelastning i Redis skriver vi 80-100K anmodninger i sekundet.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Hvorfor vi skiftede fra Redis til PostgreSQL

Oprindeligt arbejdede vores tjeneste med Redis, et nøgleværdilager, der gemmer alle data i serverens RAM.

Fordele ved Redis:

  1. Høj responshastighed, pga alt er gemt i hukommelsen;
  2. Nem backup og replikering.

Ulemper ved Redis for os:

  1. Der er ingen reelle transaktioner. Vi forsøgte at simulere dem på niveau med vores applikation. Desværre fungerede dette ikke altid godt og krævede at skrive meget kompleks kode.
  2. Mængden af ​​data er begrænset af mængden af ​​hukommelse. Efterhånden som mængden af ​​data stiger, vil hukommelsen vokse, og i sidste ende vil vi løbe ind i karakteristikaene for den valgte instans, hvilket i AWS kræver, at vi stopper vores service for at ændre instanstypen.
  3. Det er nødvendigt konstant at opretholde et lavt latensniveau, fordi. vi har et meget stort antal forespørgsler. Det optimale forsinkelsesniveau for os er 17-20 ms. På et niveau på 30-40 ms får vi lange svar på anmodninger fra vores applikation og forringelse af tjenesten. Desværre skete dette for os i september 2018, hvor et af tilfældene med Redis af en eller anden grund fik latency 2 gange mere end normalt. For at løse problemet stoppede vi tjenesten midt på dagen for ikke-planlagt vedligeholdelse og erstattede den problematiske Redis-instans.
  4. Det er nemt at få datainkonsistens selv med mindre fejl i koden og derefter bruge meget tid på at skrive kode for at rette disse data.

Vi tog hensyn til ulemperne og indså, at vi var nødt til at flytte til noget mere bekvemt, med normale transaktioner og mindre afhængighed af latenstid. Foretog research, analyserede mange muligheder og valgte PostgreSQL.

Vi har flyttet til en ny database i 1,5 år allerede og har kun flyttet en lille del af dataene, så nu arbejder vi samtidigt med Redis og PostgreSQL. Mere information om stadierne af flytning og skift af data mellem databaser er skrevet ind min kollegas artikel.

Da vi først begyndte at flytte, arbejdede vores applikation direkte med databasen og fik adgang til master Redis og PostgreSQL. PostgreSQL-klyngen bestod af en master og en replika med asynkron replikering. Sådan så databaseskemaet ud:
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Implementering af PgBouncer

Mens vi flyttede, var produktet også under udvikling: antallet af brugere og antallet af servere, der arbejdede med PostgreSQL, steg, og vi begyndte at mangle forbindelser. PostgreSQL opretter en separat proces for hver forbindelse og bruger ressourcer. Du kan øge antallet af forbindelser op til et vist punkt, ellers er der en chance for at få suboptimal databaseydelse. Den ideelle mulighed i en sådan situation ville være at vælge en forbindelsesadministrator, der vil stå foran basen.

Vi havde to muligheder for forbindelsesadministratoren: Pgpool og PgBouncer. Men den første understøtter ikke transaktionstilstanden til at arbejde med databasen, så vi valgte PgBouncer.

Vi har oprettet følgende arbejdsskema: vores applikation har adgang til én PgBouncer, bag hvilken er PostgreSQL-mastere, og bag hver master er en replika med asynkron replikering.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Samtidig kunne vi ikke gemme hele mængden af ​​data i PostgreSQL og hastigheden af ​​arbejdet med databasen var vigtig for os, så vi begyndte at sønderdele PostgreSQL på applikationsniveau. Skemaet beskrevet ovenfor er relativt praktisk til dette: når du tilføjer et nyt PostgreSQL-shard, er det nok at opdatere PgBouncer-konfigurationen, og applikationen kan straks arbejde med den nye shard.

PgBouncer failover

Denne ordning fungerede indtil det øjeblik, hvor den eneste PgBouncer-instans døde. Vi er i AWS, hvor alle instanser kører på hardware, der dør periodisk. I sådanne tilfælde flytter instansen simpelthen til ny hardware og fungerer igen. Dette skete med PgBouncer, men det blev utilgængeligt. Resultatet af dette efterår var, at vores service ikke var tilgængelig i 25 minutter. AWS anbefaler at bruge redundans på brugersiden til sådanne situationer, som ikke var implementeret i vores land på det tidspunkt.

Derefter tænkte vi seriøst på fejltolerancen for PgBouncer- og PostgreSQL-klynger, fordi en lignende situation kunne ske med enhver instans på vores AWS-konto.

Vi byggede PgBouncer-fejltoleranceskemaet som følger: alle applikationsservere får adgang til Network Load Balancer, bag hvilken der er to PgBouncere. Hver PgBouncer ser på den samme PostgreSQL-mester for hvert shard. Hvis et AWS-forekomstnedbrud opstår igen, omdirigeres al trafik gennem en anden PgBouncer. Network Load Balancer failover leveres af AWS.

Denne ordning gør det nemt at tilføje nye PgBouncer-servere.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Opret en PostgreSQL Failover Cluster

Da vi løste dette problem, overvejede vi forskellige muligheder: selvskrevet failover, repmgr, AWS RDS, Patroni.

Selvskrevne manuskripter

De kan overvåge masterens arbejde og, hvis det mislykkes, promovere replikaen til masteren og opdatere PgBouncer-konfigurationen.

Fordelene ved denne tilgang er maksimal enkelhed, fordi du selv skriver scripts og forstår præcis, hvordan de fungerer.

Ulemper:

  • Masteren er muligvis ikke død, i stedet kan der være opstået en netværksfejl. Failover, uvidende om dette, vil fremme replikaen til mesteren, mens den gamle mester vil fortsætte med at arbejde. Som følge heraf får vi to servere i rollen som master, og vi ved ikke, hvilken af ​​dem der har de seneste ajourførte data. Denne situation kaldes også split-brain;
  • Vi stod tilbage uden svar. I vores konfiguration, masteren og en replika, efter skift, flytter replikaen op til masteren, og vi har ikke længere replikaer, så vi er nødt til manuelt at tilføje en ny replika;
  • Vi har brug for yderligere overvågning af failover-operationen, mens vi har 12 PostgreSQL-shards, hvilket betyder, at vi skal overvåge 12 klynger. Med en stigning i antallet af shards skal du også huske at opdatere failoveren.

Selvskrevet failover ser meget kompliceret ud og kræver ikke-triviel support. Med en enkelt PostgreSQL-klynge ville dette være den nemmeste mulighed, men den skaleres ikke, så den er ikke egnet til os.

Repmgr

Replication Manager for PostgreSQL-klynger, som kan styre driften af ​​en PostgreSQL-klynge. Samtidig har den ikke en automatisk failover ud af boksen, så til arbejde skal du skrive din egen "wrapper" oven på den færdige løsning. Så alt kan blive endnu mere kompliceret end med selvskrevne scripts, så vi prøvede ikke engang Repmgr.

AWS RDS

Understøtter alt, hvad vi har brug for, ved, hvordan man laver sikkerhedskopier og vedligeholder en pulje af forbindelser. Den har automatisk skift: Når masteren dør, bliver replikaen den nye master, og AWS ændrer dns-posten til den nye master, mens replikaerne kan placeres i forskellige AZ'er.

Ulemperne omfatter manglen på finjusteringer. Som et eksempel på finjustering: vores instanser har begrænsninger for tcp-forbindelser, hvilket desværre ikke kan udfø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

Derudover er AWS RDS næsten dobbelt så dyr som den almindelige instanspris, hvilket var hovedårsagen til at opgive denne løsning.

Patroni

Dette er en python-skabelon til styring af PostgreSQL med god dokumentation, automatisk failover og kildekode på github.

Fordele ved Patroni:

  • Hver konfigurationsparameter er beskrevet, det er tydeligt, hvordan det virker;
  • Automatisk failover fungerer ud af boksen;
  • Skrevet i python, og da vi selv skriver meget i python, bliver det nemmere for os at håndtere problemer og måske endda hjælpe med udviklingen af ​​projektet;
  • Fuldstændig administrerer PostgreSQL, giver dig mulighed for at ændre konfigurationen på alle noder i klyngen på én gang, og hvis klyngen skal genstartes for at anvende den nye konfiguration, så kan dette gøres igen ved hjælp af Patroni.

Ulemper:

  • Det fremgår ikke tydeligt af dokumentationen, hvordan man arbejder med PgBouncer korrekt. Selvom det er svært at kalde det et minus, fordi Patronis opgave er at administrere PostgreSQL, og hvordan forbindelserne til Patroni vil gå, er allerede vores problem;
  • Der er få eksempler på implementering af Patroni på store mængder, mens der er mange eksempler på implementering fra bunden.

Som et resultat valgte vi Patroni til at oprette en failover-klynge.

Patroni implementeringsproces

Før Patroni havde vi 12 PostgreSQL-shards i en konfiguration af en master og en replika med asynkron replikering. Applikationsserverne fik adgang til databaserne gennem Network Load Balancer, bag hvilken der var to instanser med PgBouncer, og bag dem var alle PostgreSQL-serverne.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

For at implementere Patroni var vi nødt til at vælge en distribueret lagerklyngekonfiguration. Patroni arbejder med distribuerede konfigurationslagringssystemer såsom etcd, Zookeeper, Consul. Vi har netop en fuldgyldig Consul-klynge på markedet, som fungerer sammen med Vault, og vi bruger den ikke længere. En god grund til at begynde at bruge Consul til det tilsigtede formål.

Hvordan Patroni arbejder med Consul

Vi har en Consul-klynge, som består af tre noder, og en Patroni-klynge, som består af en leder og en replika (i Patroni kaldes mesteren klyngelederen, og slaverne kaldes replikaer). Hver forekomst af Patroni-klyngen sender konstant information om klyngens tilstand til Consul. Derfor kan du fra Consul altid finde ud af den aktuelle konfiguration af Patroni-klyngen, og hvem der er leder i øjeblikket.

Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

For at forbinde Patroni til Consul er det nok at studere den officielle dokumentation, som siger, at du skal angive en vært i http- eller https-formatet, afhængigt af hvordan vi arbejder med Consul, og forbindelsesordningen, 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 simpelt ud, men her begynder faldgruberne. Med Consul arbejder vi over en sikker forbindelse via https, og vores forbindelseskonfiguration ser således ud:

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

Men det går ikke. Ved opstart kan Patroni ikke oprette forbindelse til Consul, fordi den alligevel forsøger at gå gennem http.

Kildekoden til Patroni hjalp med at håndtere problemet. Godt det er skrevet i python. Det viser sig, at værtsparameteren ikke er parset på nogen måde, og protokollen skal angives i skemaet. Sådan ser arbejdskonfigurationsblokken til at arbejde med Consul ud for os:

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

konsul-skabelon

Så vi har valgt lageret til konfigurationen. Nu skal vi forstå, hvordan PgBouncer vil ændre sin konfiguration, når lederen i Patroni-klyngen ændres. Der er ikke noget svar på dette spørgsmål i dokumentationen, pga. der er arbejdet med PgBouncer i princippet ikke beskrevet.

På jagt efter en løsning fandt vi en artikel (jeg kan desværre ikke huske titlen), hvor det blev skrevet, at Сonsul-skabelonen hjalp meget med at parre PgBouncer og Patroni. Dette fik os til at undersøge, hvordan Consul-template fungerer.

Det viste sig, at Consul-template konstant overvåger konfigurationen af ​​PostgreSQL-klyngen i Consul. Når lederen ændres, opdaterer den PgBouncer-konfigurationen og sender en kommando for at genindlæse den.

Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Et stort plus ved skabelon er, at den er gemt som kode, så når du tilføjer et nyt shard, er det nok at lave en ny commit og opdatere skabelonen automatisk, hvilket understøtter Infrastruktur som kode-princippet.

Ny arkitektur med Patroni

Som et resultat fik vi følgende arbejdsplan:
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Alle applikationsservere får adgang til balanceren → der er to instanser af PgBouncer bag den → på hver instans lanceres Consul-skabelonen, som overvåger status for hver Patroni-klynge og overvåger relevansen af ​​PgBouncer-konfigurationen, som sender anmodninger til den nuværende leder af hver klynge.

Manuel test

Vi kørte denne ordning, før vi lancerede den på et lille testmiljø og kontrollerede funktionen af ​​automatisk skift. De åbnede tavlen, flyttede klistermærket, og i det øjeblik "dræbte" de lederen af ​​klyngen. I AWS er ​​dette så simpelt som at lukke instansen ned via konsollen.

Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Klistermærket vendte tilbage inden for 10-20 sekunder og begyndte så igen at bevæge sig normalt. Dette betyder, at Patroni-klyngen fungerede korrekt: den ændrede lederen, sendte informationen til Сonsul, og Сonsul-skabelonen hentede straks denne information, erstattede PgBouncer-konfigurationen og sendte kommandoen for at genindlæse.

Hvordan overlever man under høj belastning og holder nedetiden minimal?

Alt fungerer perfekt! Men der er nye spørgsmål: Hvordan vil det fungere under høj belastning? Hvordan ruller man hurtigt og sikkert alt ud i produktionen?

Testmiljøet, som vi udfører belastningstest på, hjælper os med at besvare det første spørgsmål. Den er arkitekturmæssigt fuldstændig identisk med produktionen og har genereret testdata, der i volumen er omtrent lige så stor som produktionen. Vi beslutter os for bare at "dræbe" en af ​​PostgreSQL-mestrene under testen og se, hvad der sker. Men før det er det vigtigt at tjekke den automatiske rulning, for på dette miljø har vi flere PostgreSQL shards, så vi vil få fremragende test af konfigurationsscripts inden produktion.

Begge opgaver ser ambitiøse ud, men vi har PostgreSQL 9.6. Kan vi straks opgradere til 11.2?

Vi beslutter os for at gøre det i 2 trin: først opgradere til 11.2, og derefter starte Patroni.

PostgreSQL opdatering

Brug muligheden for hurtigt at opdatere PostgreSQL-versionen -k, hvor der oprettes hårde links på disken, og der er ingen grund til at kopiere dine data. På basis af 300-400 GB tager opdateringen 1 sekund.

Vi har mange skår, så opdateringen skal ske automatisk. For at gøre dette skrev vi en Ansible playbook, der håndterer hele opdateringsprocessen for os:

/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 vigtigt at bemærke her, at før du starter opgraderingen, skal du udføre den med parameteren --kontrollerefor at sikre, at du kan opgradere. Vores script foretager også udskiftning af konfigurationer under opgraderingens varighed. Vores script blev færdigt på 30 sekunder, hvilket er et fremragende resultat.

Start Patroni

For at løse det andet problem skal du bare se på Patroni-konfigurationen. Det officielle lager har en eksempelkonfiguration med initdb, som er ansvarlig for at initialisere en ny database, når du starter Patroni første gang. Men da vi allerede har en færdig database, fjernede vi simpelthen denne sektion fra konfigurationen.

Da vi begyndte at installere Patroni på en allerede eksisterende PostgreSQL-klynge og køre den, løb vi ind i et nyt problem: begge servere startede som en leder. Patroni ved intet om klyngens tidlige tilstand og forsøger at starte begge servere som to separate klynger med samme navn. For at løse dette problem skal du slette mappen med data på slaven:

rm -rf /var/lib/postgresql/

Dette skal kun gøres på slaven!

Når en ren replika er tilsluttet, laver Patroni en basebackup-leder og gendanner den til replikaen og indhenter derefter den aktuelle tilstand i henhold til vægloggene.

En anden vanskelighed, vi stødte på, er, at alle PostgreSQL-klynger er navngivet main som standard. Når hver klynge ikke ved noget om den anden, er dette normalt. Men når du vil bruge Patroni, så skal alle klynger have et unikt navn. Løsningen er at ændre klyngenavnet i PostgreSQL-konfigurationen.

belastningstest

Vi har lanceret en test, der simulerer brugeroplevelse på boards. Da belastningen nåede vores gennemsnitlige daglige værdi, gentog vi nøjagtig den samme test, vi deaktiverede en instans med PostgreSQL-lederen. Den automatiske failover fungerede som vi forventede: Patroni ændrede lederen, Consul-skabelonen opdaterede PgBouncer-konfigurationen og sendte en kommando for at genindlæse. Ifølge vores grafer i Grafana var det tydeligt, at der er forsinkelser på 20-30 sekunder og en lille mængde fejl fra serverne forbundet med forbindelsen til databasen. Dette er en normal situation, sådanne værdier er acceptable for vores failover og er bestemt bedre end nedetiden for tjenesten.

At bringe Patroni i produktion

Som et resultat kom vi med følgende plan:

  • Implementer Consul-skabelon til PgBouncer-servere og start;
  • PostgreSQL-opdateringer til version 11.2;
  • Skift navnet på klyngen;
  • Start af Patroni-klyngen.

Samtidig giver vores ordning os mulighed for at gøre det første punkt næsten når som helst, vi kan fjerne hver PgBouncer fra arbejde på skift og implementere og køre en konsul-skabelon på den. Så det gjorde vi.

Til hurtig implementering brugte vi Ansible, da vi allerede har testet alle playbooks i et testmiljø, og udførelsestiden for det fulde script var fra 1,5 til 2 minutter for hvert shard. Vi kunne rulle alt ud på skift til hver shard uden at stoppe vores service, men vi ville være nødt til at slukke for hver PostgreSQL i flere minutter. I dette tilfælde kunne brugere, hvis data er på denne shard, ikke fungere fuldt ud på nuværende tidspunkt, og dette er uacceptabelt for os.

Vejen ud af denne situation var den planlagte vedligeholdelse, som finder sted hver 3. måned. Dette er et vindue til planlagt arbejde, når vi fuldstændig lukker vores service ned og opgraderer vores databaseforekomster. Der var en uge tilbage til næste vindue, og vi besluttede at vente og forberede os yderligere. I ventetiden sikrede vi os yderligere: For hver PostgreSQL-shard rejste vi en reservekopi i tilfælde af manglende opbevaring af de seneste data, og tilføjede en ny instans for hver shard, som skulle blive en ny replika i Patroni-klyngen, for ikke at udføre en kommando for at slette data. Alt dette var med til at minimere risikoen for fejl.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Vi genstartede vores service, alt fungerede som det skulle, brugerne fortsatte med at arbejde, men på graferne bemærkede vi en unormalt høj belastning på Consul-serverne.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Hvorfor så vi det ikke i testmiljøet? Dette problem illustrerer meget godt, at det er nødvendigt at følge Infrastruktur som kodeprincippet og forfine hele infrastrukturen, fra testmiljøer til produktion. Ellers er det meget nemt at få det problem, vi fik. Hvad skete der? Consul dukkede først op på produktion og derefter på testmiljøer, som et resultat, på testmiljøer, var versionen af ​​Consul højere end på produktion. Bare i en af ​​udgivelserne blev en CPU-lækage løst, når man arbejdede med consul-template. Derfor har vi blot opdateret Consul, og dermed løst problemet.

Genstart Patroni-klyngen

Vi fik dog et nyt problem, som vi ikke engang havde mistanke om. Når du opdaterer Consul, fjerner vi blot Consul-noden fra klyngen ved at bruge kommandoen consul leave → Patroni opretter forbindelse til en anden Consul-server → alt fungerer. Men da vi nåede den sidste forekomst af Consul-klyngen og sendte konsul-forlad-kommandoen til den, genstartede alle Patroni-klynger simpelthen, og i logfilerne så vi følgende fejl:

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 var ikke i stand til at hente oplysninger om sin klynge og genstartede.

For at finde en løsning kontaktede vi Patroni-forfatterne via et problem på github. De foreslog forbedringer til vores konfigurationsfiler:

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

Vi var i stand til at replikere problemet på et testmiljø og testede disse muligheder der, men de virkede desværre ikke.

Problemet er stadig uløst. Vi planlægger at prøve følgende løsninger:

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

Vi forstår, hvor fejlen opstod: problemet er sandsynligvis brugen af ​​standard timeout, som ikke tilsidesættes gennem konfigurationsfilen. Når den sidste Consul-server fjernes fra klyngen, hænger hele Consul-klyngen i mere end et sekund, på grund af dette kan Patroni ikke få status for klyngen og genstarter fuldstændigt hele klyngen.

Heldigvis stødte vi ikke på flere fejl.

Resultater af brug af Patroni

Efter den vellykkede lancering af Patroni tilføjede vi en ekstra replika i hver klynge. Nu i hver klynge er der et udtryk for et kvorum: en leder og to replikaer, som sikkerhedsnet i tilfælde af split-brain ved skift.
Failover Cluster PostgreSQL + Patroni. Erfaring med implementering

Patroni har arbejdet på produktionen i mere end tre måneder. I løbet af denne tid har han allerede formået at hjælpe os. For nylig døde lederen af ​​en af ​​klyngerne i AWS, automatisk failover virkede, og brugerne fortsatte med at arbejde. Patroni opfyldte sin hovedopgave.

En lille oversigt over brugen af ​​Patroni:

  • Nem konfigurationsændringer. Det er nok at ændre konfigurationen på én instans, og den vil blive trukket op til hele klyngen. Hvis en genstart er påkrævet for at anvende den nye konfiguration, vil Patroni give dig besked. Patroni kan genstarte hele klyngen med en enkelt kommando, hvilket også er meget praktisk.
  • Automatisk failover virker og har allerede formået at hjælpe os.
  • PostgreSQL-opdatering uden programnedetid. Du skal først opdatere replikaerne til den nye version, derefter ændre lederen i Patroni-klyngen og opdatere den gamle leder. I dette tilfælde sker den nødvendige test af automatisk failover.

Kilde: www.habr.com

Tilføj en kommentar