Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

I artikeln kommer jag att berätta hur vi tog oss an frågan om PostgreSQL-feltolerans, varför det blev viktigt för oss och vad som hände till slut.

Vi har en mycket laddad tjänst: 2,5 miljoner användare över hela världen, 50K+ aktiva användare varje dag. Servrarna finns i Amazone i en region i Irland: 100+ olika servrar är ständigt i drift, varav nästan 50 med databaser.

Hela backend är en stor monolitisk stateful Java-applikation som håller en konstant websocket-anslutning med klienten. När flera användare arbetar på samma tavla samtidigt ser de alla ändringarna i realtid, eftersom vi skriver varje ändring till databasen. Vi har cirka 10K förfrågningar per sekund till våra databaser. Vid toppbelastning i Redis skriver vi 80-100K förfrågningar per sekund.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Varför vi bytte från Redis till PostgreSQL

Inledningsvis fungerade vår tjänst med Redis, en nyckel-värde butik som lagrar all data i serverns RAM.

Fördelar med Redis:

  1. Hög svarshastighet, eftersom allt lagras i minnet;
  2. Enkel säkerhetskopiering och replikering.

Nackdelar med Redis för oss:

  1. Det finns inga riktiga transaktioner. Vi försökte simulera dem på nivån för vår applikation. Tyvärr fungerade detta inte alltid bra och krävde att man skrev mycket komplex kod.
  2. Mängden data begränsas av mängden minne. När mängden data ökar kommer minnet att växa, och i slutändan kommer vi att stöta på egenskaperna hos den valda instansen, vilket i AWS kräver att vi stoppar vår tjänst för att ändra typ av instans.
  3. Det är nödvändigt att ständigt upprätthålla en låg latensnivå, eftersom. vi har ett mycket stort antal förfrågningar. Den optimala fördröjningsnivån för oss är 17-20 ms. På en nivå av 30-40 ms får vi långa svar på förfrågningar från vår applikation och försämring av tjänsten. Tyvärr hände detta oss i september 2018, när ett av fallen med Redis av någon anledning fick fördröjning 2 gånger mer än vanligt. För att lösa problemet stoppade vi tjänsten mitt på dagen för oplanerat underhåll och ersatte den problematiska Redis-instansen.
  4. Det är lätt att få datainkonsekvens även med mindre fel i koden och sedan lägga ner mycket tid på att skriva kod för att korrigera denna data.

Vi tog hänsyn till nackdelarna och insåg att vi behövde flytta till något bekvämare, med normala transaktioner och mindre beroende av latens. Utförde research, analyserade många alternativ och valde PostgreSQL.

Vi har flyttat till en ny databas redan i 1,5 år och har bara flyttat en liten del av datan, så nu arbetar vi samtidigt med Redis och PostgreSQL. Mer information om stadierna för att flytta och byta data mellan databaser skrivs in min kollegas artikel.

När vi först började flytta arbetade vår applikation direkt med databasen och fick åtkomst till master Redis och PostgreSQL. PostgreSQL-klustret bestod av en master och en replik med asynkron replikering. Så här såg databasschemat ut:
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Implementering av PgBouncer

Medan vi flyttade utvecklades också produkten: antalet användare och antalet servrar som arbetade med PostgreSQL ökade, och vi började sakna anslutningar. PostgreSQL skapar en separat process för varje anslutning och förbrukar resurser. Du kan öka antalet anslutningar upp till en viss punkt, annars finns det en chans att få suboptimal databasprestanda. Det ideala alternativet i en sådan situation skulle vara att välja en anslutningshanterare som kommer att stå framför basen.

Vi hade två alternativ för anslutningshanteraren: Pgpool och PgBouncer. Men den första stöder inte transaktionsläget för att arbeta med databasen, så vi valde PgBouncer.

Vi har satt upp följande arbetsschema: vår applikation får åtkomst till en PgBouncer, bakom vilken finns PostgreSQL-master, och bakom varje master finns en replik med asynkron replikering.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Samtidigt kunde vi inte lagra hela datamängden i PostgreSQL och hastigheten på att arbeta med databasen var viktig för oss, så vi började sharda PostgreSQL på applikationsnivå. Schemat som beskrivs ovan är relativt bekvämt för detta: när du lägger till en ny PostgreSQL-shard räcker det att uppdatera PgBouncer-konfigurationen och applikationen kan omedelbart arbeta med den nya shard.

PgBouncer failover

Detta schema fungerade tills det ögonblick då den enda PgBouncer-instansen dog. Vi är i AWS, där alla instanser körs på hårdvara som dör periodvis. I sådana fall flyttar instansen helt enkelt till ny hårdvara och fungerar igen. Detta hände med PgBouncer, men det blev otillgängligt. Resultatet av hösten var att vår tjänst inte var tillgänglig i 25 minuter. AWS rekommenderar att man använder redundans på användarsidan för sådana situationer, vilket inte var implementerat i vårt land vid den tiden.

Efter det tänkte vi allvarligt på feltoleransen för PgBouncer- och PostgreSQL-kluster, eftersom en liknande situation kan hända med vilken instans som helst i vårt AWS-konto.

Vi byggde PgBouncers feltoleransschema enligt följande: alla applikationsservrar har åtkomst till Network Load Balancer, bakom vilken det finns två PgBouncers. Varje PgBouncer tittar på samma PostgreSQL-mästare för varje skärva. Om en AWS-instanskrasch inträffar igen, omdirigeras all trafik genom en annan PgBouncer. Network Load Balancer failover tillhandahålls av AWS.

Detta schema gör det enkelt att lägga till nya PgBouncer-servrar.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Skapa ett PostgreSQL-failover-kluster

När vi löste det här problemet övervägde vi olika alternativ: självskriven failover, repmgr, AWS RDS, Patroni.

Självskrivna manus

De kan övervaka masterns arbete och, om det misslyckas, marknadsföra repliken till mastern och uppdatera PgBouncer-konfigurationen.

Fördelarna med detta tillvägagångssätt är maximal enkelhet, eftersom du själv skriver manus och förstår exakt hur de fungerar.

Nackdelar:

  • Mastern kanske inte har dött, istället kan ett nätverksfel ha inträffat. Failover, omedveten om detta, kommer att främja repliken till mästaren, medan den gamla mästaren kommer att fortsätta att arbeta. Som ett resultat kommer vi att få två servrar i rollen som master och vi kommer inte att veta vilken av dem som har den senaste uppdaterade informationen. Denna situation kallas också split-brain;
  • Vi blev lämnade utan svar. I vår konfiguration, mastern och en replik, efter bytet, flyttar repliken upp till mastern och vi har inte längre repliker, så vi måste manuellt lägga till en ny replik;
  • Vi behöver ytterligare övervakning av failover-operationen, medan vi har 12 PostgreSQL-skärvor, vilket innebär att vi måste övervaka 12 kluster. Med en ökning av antalet shards måste du också komma ihåg att uppdatera failover.

Självskriven failover ser väldigt komplicerad ut och kräver icke-trivialt stöd. Med ett enda PostgreSQL-kluster skulle detta vara det enklaste alternativet, men det skalas inte, så det är inte lämpligt för oss.

Repmgr

Replikeringshanterare för PostgreSQL-kluster, som kan hantera driften av ett PostgreSQL-kluster. Samtidigt har den ingen automatisk failover ur lådan, så för arbete måste du skriva din egen "wrapper" ovanpå den färdiga lösningen. Så allt kan bli ännu mer komplicerat än med självskrivna manus, så vi provade inte ens Repmgr.

AWS RDS

Stöder allt vi behöver, vet hur man gör säkerhetskopior och upprätthåller en pool av anslutningar. Den har automatisk växling: när mastern dör blir repliken den nya mastern, och AWS ändrar dns-posten till den nya mastern, medan replikerna kan placeras i olika AZ.

Nackdelarna är avsaknaden av finjusteringar. Som ett exempel på finjustering: våra instanser har restriktioner för tcp-anslutningar, vilket tyvärr inte kan göras 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

Dessutom är AWS RDS nästan dubbelt så dyrt som det vanliga instanspriset, vilket var huvudskälet till att överge denna lösning.

Patroni

Detta är en pythonmall för att hantera PostgreSQL med bra dokumentation, automatisk failover och källkod på github.

Fördelar med Patroni:

  • Varje konfigurationsparameter beskrivs, det är tydligt hur det fungerar;
  • Automatisk failover fungerar direkt;
  • Skrivet i python, och eftersom vi själva skriver mycket i python, blir det lättare för oss att hantera problem och kanske till och med hjälpa till med utvecklingen av projektet;
  • Hanterar helt PostgreSQL, låter dig ändra konfigurationen på alla noder i klustret samtidigt, och om klustret behöver startas om för att tillämpa den nya konfigurationen, kan detta göras igen med Patroni.

Nackdelar:

  • Det framgår inte av dokumentationen hur man arbetar med PgBouncer korrekt. Även om det är svårt att kalla det ett minus, eftersom Patronis uppgift är att hantera PostgreSQL, och hur kopplingar till Patroni kommer att gå är redan vårt problem;
  • Det finns få exempel på implementering av Patroni på stora volymer, medan det finns många exempel på implementering från grunden.

Som ett resultat valde vi Patroni för att skapa ett failover-kluster.

Implementeringsprocess för Patroni

Innan Patroni hade vi 12 PostgreSQL-skärvor i en konfiguration av en master och en replik med asynkron replikering. Applikationsservrarna fick åtkomst till databaserna genom Network Load Balancer, bakom vilken fanns två instanser med PgBouncer, och bakom dem fanns alla PostgreSQL-servrar.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

För att implementera Patroni behövde vi välja en distribuerad lagringsklusterkonfiguration. Patroni arbetar med distribuerade konfigurationslagringssystem som etcd, Zookeeper, Consul. Vi har precis ett fullfjädrat Consul-kluster på marknaden, som fungerar tillsammans med Vault och vi använder det inte längre. En stor anledning att börja använda Consul för sitt avsedda syfte.

Hur Patroni arbetar med Consul

Vi har ett Consul-kluster, som består av tre noder, och ett Patroni-kluster, som består av en ledare och en replika (i Patroni kallas befälhavaren för klusterledaren, och slavarna kallas repliker). Varje instans av Patroni-klustret skickar ständigt information om klustrets tillstånd till Consul. Därför kan du från Consul alltid ta reda på den aktuella konfigurationen av Patroni-klustret och vem som är ledaren för tillfället.

Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

För att ansluta Patroni till Consul räcker det att studera den officiella dokumentationen, som säger att du måste ange en värd i http- eller https-formatet, beroende på hur vi arbetar med Consul, och anslutningsschemat, valfritt:

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 här börjar fallgroparna. Med Consul arbetar vi över en säker anslutning via https och vår anslutningskonfiguration kommer att se ut så här:

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

Men det går inte. Vid uppstart kan Patroni inte ansluta till Consul, eftersom den försöker gå igenom http ändå.

Patronis källkod hjälpte till att hantera problemet. Bra att det är skrivet i python. Det visar sig att värdparametern inte analyseras på något sätt, och protokollet måste anges i schemat. Så här ser arbetskonfigurationsblocket för att arbeta med Consul ut för oss:

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

konsul-mall

Så vi har valt lagringen för konfigurationen. Nu måste vi förstå hur PgBouncer kommer att ändra sin konfiguration när du byter ledare i Patroni-klustret. Det finns inget svar på denna fråga i dokumentationen, eftersom. där beskrivs i princip inte arbetet med PgBouncer.

På jakt efter en lösning hittade vi en artikel (jag kommer tyvärr inte ihåg titeln) där det skrevs att Сonsul-mall hjälpte mycket med att para ihop PgBouncer och Patroni. Detta fick oss att undersöka hur Consul-template fungerar.

Det visade sig att Consul-template ständigt övervakar konfigurationen av PostgreSQL-klustret i Consul. När ledaren ändras uppdaterar den PgBouncer-konfigurationen och skickar ett kommando för att ladda om den.

Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Ett stort plus med mallen är att den lagras som kod, så när du lägger till en ny shard räcker det att göra en ny commit och uppdatera mallen automatiskt, vilket stöder principen Infrastructure as code.

Ny arkitektur med Patroni

Som ett resultat fick vi följande arbetsschema:
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Alla applikationsservrar får åtkomst till balanseringen → det finns två instanser av PgBouncer bakom den → på varje instans lanseras Consul-mall, som övervakar statusen för varje Patroni-kluster och övervakar relevansen av PgBouncer-konfigurationen, som skickar förfrågningar till den nuvarande ledaren av varje kluster.

Manuell testning

Vi körde detta schema innan vi lanserade det i en liten testmiljö och kontrollerade funktionen för automatisk växling. De öppnade tavlan, flyttade klistermärket och i det ögonblicket "dödade" de ledaren för klustret. I AWS är detta så enkelt som att stänga av instansen via konsolen.

Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Dekalen kom tillbaka inom 10-20 sekunder och började sedan återigen röra sig normalt. Detta betyder att Patroni-klustret fungerade korrekt: det bytte ledare, skickade informationen till Сonsul, och Сonsul-mall hämtade omedelbart denna information, ersatte PgBouncer-konfigurationen och skickade kommandot för att ladda om.

Hur överlever man under hög belastning och håller stilleståndstiden minimal?

Allt fungerar perfekt! Men det finns nya frågor: Hur kommer det att fungera under hög belastning? Hur rullar man snabbt och säkert ut allt i produktionen?

Testmiljön där vi utför belastningstester hjälper oss att svara på den första frågan. Den är helt identisk med produktionen rent arkitekturmässigt och har genererat testdata som är ungefär lika volymmässigt som produktionen. Vi bestämmer oss för att bara "döda" en av PostgreSQL-mästarna under testet och se vad som händer. Men innan dess är det viktigt att kontrollera den automatiska rullningen, för i den här miljön har vi flera PostgreSQL-skärvor, så vi kommer att få utmärkta tester av konfigurationsskript innan produktion.

Båda uppgifterna ser ambitiösa ut, men vi har PostgreSQL 9.6. Kan vi omedelbart uppgradera till 11.2?

Vi bestämmer oss för att göra det i två steg: först uppgradera till 2 och sedan starta Patroni.

PostgreSQL-uppdatering

För att snabbt uppdatera PostgreSQL-versionen, använd alternativet -k, där hårda länkar skapas på disken och du behöver inte kopiera dina data. På baser på 300-400 GB tar uppdateringen 1 sekund.

Vi har många skärvor, så uppdateringen måste göras automatiskt. För att göra detta skrev vi en Ansible-spelbok som hanterar hela uppdateringsprocessen åt 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 är viktigt att notera här att innan du startar uppgraderingen måste du utföra den med parametern --kolla uppför att se till att du kan uppgradera. Vårt skript byter även ut konfigurationer under uppgraderingens varaktighet. Vårt manus slutfördes på 30 sekunder, vilket är ett utmärkt resultat.

Starta Patroni

För att lösa det andra problemet, titta bara på Patroni-konfigurationen. Det officiella arkivet har en exempelkonfiguration med initdb, som ansvarar för att initiera en ny databas när du startar Patroni. Men eftersom vi redan har en färdig databas tog vi helt enkelt bort det här avsnittet från konfigurationen.

När vi började installera Patroni på ett redan befintligt PostgreSQL-kluster och körde det, stötte vi på ett nytt problem: båda servrarna började som en ledare. Patroni vet ingenting om klustrets tidiga tillstånd och försöker starta båda servrarna som två separata kluster med samma namn. För att lösa detta problem måste du ta bort katalogen med data på slaven:

rm -rf /var/lib/postgresql/

Detta behöver endast göras på slaven!

När en ren replik är ansluten, gör Patroni en basebackup-ledare och återställer den till repliken, och kommer sedan ikapp det aktuella tillståndet enligt loggarna.

En annan svårighet som vi stött på är att alla PostgreSQL-kluster kallas main som standard. När varje kluster inte vet något om det andra är detta normalt. Men när du vill använda Patroni måste alla kluster ha ett unikt namn. Lösningen är att ändra klusternamnet i PostgreSQL-konfigurationen.

ladda test

Vi har lanserat ett test som simulerar användarupplevelsen på brädor. När belastningen nådde vårt genomsnittliga dagliga värde, upprepade vi exakt samma test, vi stängde av en instans med PostgreSQL-ledaren. Den automatiska failoveren fungerade som vi förväntade oss: Patroni bytte ledare, Consul-mall uppdaterade PgBouncer-konfigurationen och skickade ett kommando för att ladda om. Enligt våra grafer i Grafana var det tydligt att det finns förseningar på 20-30 sekunder och en liten mängd fel från servrarna som är kopplade till anslutningen till databasen. Detta är en normal situation, sådana värden är acceptabla för vår failover och är definitivt bättre än serviceavbrottstiden.

Tar Patroni till produktion

Som ett resultat kom vi fram till följande plan:

  • Distribuera Consul-mall till PgBouncer-servrar och starta;
  • PostgreSQL-uppdateringar till version 11.2;
  • Ändra namnet på klustret;
  • Startar Patroni-klustret.

Samtidigt tillåter vårt schema oss att göra den första punkten nästan när som helst, vi kan ta bort varje PgBouncer från jobbet i tur och ordning och distribuera och köra konsul-mall på den. Så det gjorde vi.

För snabb distribution använde vi Ansible, eftersom vi redan har testat alla spelböcker i en testmiljö, och exekveringstiden för hela skriptet var från 1,5 till 2 minuter för varje skärva. Vi kunde rulla ut allt i tur och ordning till varje skärva utan att stoppa vår tjänst, men vi skulle behöva stänga av varje PostgreSQL i flera minuter. I det här fallet kunde användare vars data finns på denna shard inte fungera fullt ut just nu, och detta är oacceptabelt för oss.

Vägen ur denna situation var det planerade underhållet, som sker var tredje månad. Detta är ett fönster för schemalagt arbete, när vi helt stänger av vår tjänst och uppgraderar våra databasinstanser. Det var en vecka kvar till nästa fönster, och vi bestämde oss för att bara vänta och förbereda oss ytterligare. Under väntetiden säkrade vi oss dessutom: för varje PostgreSQL-skärv tog vi upp en reservreplik i händelse av misslyckande med att behålla de senaste data, och la till en ny instans för varje shard, som skulle bli en ny replik i Patroni-klustret, för att inte köra ett kommando för att radera data. Allt detta bidrog till att minimera risken för fel.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Vi startade om vår tjänst, allt fungerade som det skulle, användarna fortsatte att arbeta, men på graferna märkte vi en onormalt hög belastning på Consul-servrarna.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Varför såg vi inte detta i testmiljön? Detta problem illustrerar mycket väl att det är nödvändigt att följa principen Infrastruktur som kod och förfina hela infrastrukturen, från testmiljöer till produktion. Annars är det väldigt lätt att få problemet vi fick. Vad hände? Consul dök först upp i produktionen och sedan i testmiljöer, som ett resultat, på testmiljöer var versionen av Consul högre än på produktionen. Bara i en av utgåvorna löstes en CPU-läcka när man arbetade med consul-mall. Därför uppdaterade vi helt enkelt Consul, vilket löste problemet.

Starta om Patroni-klustret

Vi fick dock ett nytt problem, som vi inte ens misstänkte. När vi uppdaterar Consul tar vi helt enkelt bort Consul-noden från klustret med kommandot consul leave → Patroni ansluter till en annan Consul-server → allt fungerar. Men när vi nådde den sista instansen av Consul-klustret och skickade kommandot konsul lämna till det, startade alla Patroni-kluster helt enkelt om, och i loggarna såg vi följande fel:

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-klustret kunde inte hämta information om sitt kluster och startade om.

För att hitta en lösning kontaktade vi Patroni-författarna via ett problem på github. De föreslog förbättringar av våra konfigurationsfiler:

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

Vi kunde replikera problemet i en testmiljö och testade dessa alternativ där, men tyvärr fungerade de inte.

Problemet är fortfarande olöst. Vi planerar att prova följande lösningar:

  • Använd Consul-agent på varje Patroni-klusterinstans;
  • Åtgärda problemet i koden.

Vi förstår var felet uppstod: problemet är förmodligen användningen av standard timeout, som inte åsidosätts genom konfigurationsfilen. När den sista Consul-servern tas bort från klustret hänger hela Consul-klustret i mer än en sekund, på grund av detta kan Patroni inte få status för klustret och startar om hela klustret helt.

Lyckligtvis stötte vi inte på några fler fel.

Resultat av att använda Patroni

Efter den framgångsrika lanseringen av Patroni lade vi till ytterligare en replik i varje kluster. Nu i varje kluster finns det ett sken av ett kvorum: en ledare och två repliker, för skyddsnät i händelse av split-brain vid byte.
Failover-kluster PostgreSQL + Patroni. Erfarenhet av implementering

Patroni har arbetat med produktion i mer än tre månader. Under den här tiden har han redan lyckats hjälpa oss. Nyligen dog ledaren för ett av klustren i AWS, automatisk failover fungerade och användare fortsatte att arbeta. Patroni fullgjorde sin huvuduppgift.

En liten sammanfattning av användningen av Patroni:

  • Enkel konfigurationsändring. Det räcker att ändra konfigurationen på en instans och den kommer att dras upp till hela klustret. Om en omstart krävs för att tillämpa den nya konfigurationen kommer Patroni att meddela dig. Patroni kan starta om hela klustret med ett enda kommando, vilket också är väldigt bekvämt.
  • Automatisk failover fungerar och har redan lyckats hjälpa oss.
  • PostgreSQL-uppdatering utan programstopp. Du måste först uppdatera replikerna till den nya versionen, sedan ändra ledaren i Patroni-klustret och uppdatera den gamla ledaren. I detta fall sker den nödvändiga testningen av automatisk failover.

Källa: will.com

Lägg en kommentar