
Effekten av tabell- och indexbloat Ă€r vĂ€lkĂ€nd och förekommer inte bara i Postgres. Det finns sĂ€tt att hantera det âout of the boxâ, som VACUUM FULL eller CLUSTER, men dessa lĂ„ser tabellerna under anvĂ€ndning och kan dĂ€rför inte alltid anvĂ€ndas.
I artikeln kommer vi att gÄ igenom lite teori om hur bloat uppstÄr, hur man kan hantera det, om deferred constraints och de problem som de medför vid anvÀndning av tillÀgget pg_repack.
Denna artikel Àr baserad pÄ PgConf.Russia 2020.

Varför uppstÄr bloat
Postgres bygger pÄ en flerVersionsmodell (). KÀrnan i detta Àr att varje rad i tabellen kan ha flera versioner, dÀr transaktioner ser högst en av dessa versioner, men nödvÀndigtvis inte samma version. Detta möjliggör att flera transaktioner kan utföras samtidigt utan att pÄverka varandra nÀmnvÀrt.
Det Àr uppenbart att alla dessa versioner mÄste lagras. Postgres arbetar med minnet per sida, och en sida Àr den minimi mÀngd data som kan lÀsas frÄn disken eller skrivas till den. LÄt oss titta pÄ ett litet exempel för att förstÄ hur detta sker.
Anta att vi har en tabell dÀr vi har lagt till flera poster. I den första sidan av filen dÀr tabellen lagras har nya data lagts till. Detta Àr de aktiva versionerna av raderna, som Àr tillgÀngliga för andra transaktioner efter commit (för enkelhets skull antar vi att isoleringsnivÄn Àr Read Committed).

DÀrefter uppdaterade vi en av posterna och markerade dÀrmed den gamla versionen som ogiltig.

Steg för steg, genom att uppdatera och ta bort versioner av rader, har vi fĂ„tt en sida dĂ€r ungefĂ€r hĂ€lften av datan Ă€r âskrĂ€pâ. Dessa data Ă€r osynliga för nĂ„gon transaktion.

I Postgres finns det en mekanism , som rensar ogiltiga versioner och frigör plats för nya data. Men om den inte Ă€r instĂ€lld pĂ„ att vara tillrĂ€ckligt aggressiv eller sysselsatt med arbete i andra tabeller, sĂ„ finns âskrĂ€pdataâ kvar, och vi tvingas anvĂ€nda ytterligare sidor för nya data.
SÄ i vÄrt exempel kommer tabellen vid nÄgon tidpunkt att bestÄ av fyra sidor, men det kommer bara att finnas levande data i hÀlften av dem. Som ett resultat kommer vi att hÀmta mycket mer data frÄn tabellen Àn nödvÀndigt.

Ăven om VACUUM nu tar bort alla irrelevanta radversioner kommer situationen inte att förbĂ€ttras radikalt. Vi fĂ„r ledigt utrymme pĂ„ sidorna eller till och med hela sidor för nya rader, men vi kommer fortfarande att lĂ€sa mer data Ă€n nödvĂ€ndigt.
Förresten, om en helt tom sida (den andra i vÄrt exempel) hamnade i slutet av filen skulle VACUUM kunna trimma den. Men nu Àr den i mitten, sÄ vi kan inte göra nÄgot med den.

NÀr antalet sÄdana tomma eller kraftigt nedpackade sidor blir stort, kallas det bloat, och det börjar pÄverka prestandan.
OvanstÄende Àr mekaniken bakom uppkomsten av bloat i tabeller. I index sker detta ungefÀr pÄ samma sÀtt.
Har jag bloat?
Det finns flera sÀtt att avgöra om du har bloat. Idén med den första Àr att anvÀnda inre statistik frÄn Postgres, som innehÄller ungefÀrlig information om antalet rader i tabellerna, antalet "levande" rader etc. PÄ internet finns det mÄnga varianter av redan fÀrdiga skript. Vi tog som utgÄngspunkt frÄn PostgreSQL Experts, som kan bedöma bloat av tabeller tillsammans med toast och bloat av btree-index. Enligt vÄr erfarenhet ligger dess felmarginal pÄ 10-20%.
Ett annat sÀtt Àr att anvÀnda tillÀgget , vilket gör att du kan titta inuti sidorna och fÄ bÄde en uppskattning och ett exakt vÀrde för bloat. Men i det andra fallet mÄste hela tabellen genomsökas.
Ett litet bloat-vÀrde, upp till 20%, anses vara acceptabelt. Det kan liknas vid fillfactor för och . Vid 50% och mer kan problem med prestanda börja uppstÄ.
SĂ€tt att hantera bloat
I Postgres finns det flera sÀtt att hantera bloat "out-of-the-box", men de passar lÄngt ifrÄn alltid och för alla.
Konfigurera AUTOVACUUM sĂ„ att bloat inte uppstĂ„r. För att vara mer exakt, sĂ„ att det hĂ„lls pĂ„ en nivĂ„ som Ă€r acceptabel för dig. Det kan verka som ett âkapten rĂ„dâ, men i verkligheten Ă€r detta inte alltid lĂ€tt att uppnĂ„. Till exempel, om du har aktiv utveckling med regelbundna Ă€ndringar i datamodellen eller om det pĂ„gĂ„r nĂ„gon form av datamigrering. Som en följd kan din belastningsprofil förĂ€ndras ofta, och vanligtvis Ă€r den olika för olika tabeller. Det betyder att du hela tiden mĂ„ste ligga steget före och justera AUTOVACUUM efter den förĂ€nderliga profilen för varje tabell. Men det Ă€r uppenbart att det inte Ă€r enkelt att göra.
En annan vanlig orsak till att AUTOVACUUM inte hinner bearbeta tabellerna Ă€r nĂ€r det finns lĂ„ngvariga transaktioner som hindrar det frĂ„n att rensa data eftersom de Ă€r tillgĂ€ngliga för dessa transaktioner. Rekommendationen hĂ€r Ă€r ocksĂ„ uppenbar â bli av med âhĂ€ngandeâ transaktioner och minimera tiden för aktiva transaktioner. Men om belastningen pĂ„ din applikation Ă€r en hybrid av OLAP och OLTP kan du ha bĂ„de mĂ„nga frekventa uppdateringar och korta frĂ„gor samtidigt, samt lĂ€ngre operationer â till exempel att skapa nĂ„gon form av rapport. I en sĂ„dan situation bör du övervĂ€ga att fördela belastningen pĂ„ olika databaser, vilket skulle möjliggöra en finare justering av var och en av dem.
Ett annat exempel â Ă€ven om profilen Ă€r homogent, men databasen Ă€r under mycket hög belastning, kan Ă€ven det mest aggressiva AUTOVACUUM misslyckas, och bloat kan uppstĂ„. Skala (vertikalt eller horisontellt) Ă€r den enda lösningen.
Vad ska man göra i en situation dÀr du har stÀllt in AUTOVACUUM, men bloat fortsÀtter att öka.
Team VACUUM FULL Äteruppbygger innehÄllet i tabeller och index och lÀmnar bara aktuell data. För att ÄtgÀrda bloat fungerar den perfekt, men under dess körning lÄser den tabellen med ett exklusivt lÄs (AccessExclusiveLock), vilket gör att frÄgor mot denna tabell inte kan utföras, inte ens select-frÄgor. Om du kan tillÄta att din tjÀnst eller en del av den stoppas under en viss tid (frÄn tiotals minuter till flera timmar beroende pÄ storleken pÄ databasen och din hÄrdvara), Àr det hÀr alternativet det bÀsta. TyvÀrr hinner vi inte köra VACUUM FULL under den planerade underhÄllet, sÄ den metoden passar oss inte.
Team CLUSTER den Ă„teruppbygger tabellinnehĂ„llet, precis som VACUUM FULL, men tillĂ„ter att ange en index som data kommer att ordnas fysiskt pĂ„ disken (men för nya rader garanteras ordningen inte i framtiden). I vissa situationer Ă€r detta en bra optimering för en rad frĂ„gor â med lĂ€sning av flera poster via index. Nackdelen med kommandot Ă€r densamma som med VACUUM FULL â det lĂ„ser tabellen under arbete.
Team REINDEX Àr lik de tvÄ föregÄende, men utför Äteruppbyggnad av ett specifikt index eller alla index i tabellen. LÄsningarna Àr nÄgot svagare: ShareLock pÄ tabellen (hindrar modifieringar men tillÄter select) och AccessExclusiveLock pÄ det Äteruppbyggda indexet (lÄser förfrÄgningar som anvÀnder detta index). Men i version 12 av Postgres introducerades parametrar , som tillÄter att Äteruppbygga indexet utan att blockera parallell inmatning, Àndring eller borttagning av poster.
I tidigare versioner av Postgres kan man nÄ resultat som liknar REINDEX CONCURRENTLY genom . Det tillÄter att skapa ett index utan strikt lÄsning (ShareUpdateExclusiveLock, som inte hindrar parallella förfrÄgningar), sedan byta ut det gamla indexet mot det nya och ta bort det gamla indexet. Detta möjliggör eliminering av indexbloat utan att störa ditt applikationsarbete. Det Àr viktigt att notera att nÀr index Äteruppbyggs kommer det att finnas en extra belastning pÄ diskunderlaget.
SÄledes, om det finns sÀtt för index att eliminera bloat "pÄ sprÄng", finns det inga sÄdana för tabeller. HÀr kommer olika externa tillÀgg in i bilden: (tidigare pg_reorg), , och andra. I denna artikel kommer jag inte att jÀmföra dem utan bara berÀtta om pg_repack, som vi anvÀnder efter viss anpassning.
Hur pg_repack fungerar

Antag att vi har en ganska vanlig tabell - med index, begrÀnsningar och, tyvÀrr, med bloat. Det första steget pg_repack gör Àr att skapa en loggtabell för att lagra data om alla förÀndringar under arbetets gÄng. En trigger kommer att replikera dessa förÀndringar vid varje insert, update och delete. DÀrefter skapas en tabell som liknar den ursprungliga i struktur, men utan index och begrÀnsningar, för att inte sakta ner datainmatningsprocessen.
DÀrefter flyttar pg_repack över data frÄn den gamla tabellen till den nya, och filtrerar automatiskt bort alla irrelevanta rader, och skapar sedan index för den nya tabellen. Under tiden som alla dessa operationer utförs samlas Àndringarna i loggtabellen.
NĂ€sta steg Ă€r att överföra Ă€ndringarna till den nya tabellen. Ăverföringen utförs i flera iterationer, och nĂ€r det finns mindre Ă€n 20 poster kvar i loggtabellen tar pg_repack en strikt lĂ„sning, överför de senaste data och ersĂ€tter den gamla tabellen med den nya i Postgres systemtabeller. Det Ă€r den enda och mycket korta tiden dĂ„ du inte kan arbeta med tabellen. Efter detta tas den gamla tabellen och loggtabellen bort, och utrymme frigörs i filsystemet. Processen Ă€r avslutad.
I teorin ser allt bra ut, men hur Àr det i praktiken? Vi testade pg_repack utan belastning och under belastning, och granskade dess funktion vid en ÄtgÀrd för att stoppa den i förtid (med andra ord via Ctrl+C). Alla tester var positiva.
Vi gick live â och hĂ€r gick allt inte som vi förvĂ€ntade oss.
Första lyckosamma försöket i produktion
PÄ den första klustret fick vi ett fel om ett brott mot unika begrÀnsningar:
$ ./pg_repack -t tablename -o id
INFO: ompackar tabellen "tablename"
ERROR: frÄgan misslyckades:
ERROR: duplicerat nyckelvÀrde bryter mot unik begrÀnsning "index_16508"
DETAIL: Nyckel (id, index)=(100500, 42) existerar redan.
Denna begrĂ€nsning hade ett autogenererat namn index_16508 â det skapades av pg_repack. Genom Attribute som ingĂ„r i det bestĂ€mde vi "vĂ„r" begrĂ€nsning som motsvarade det. Problemet visade sig vara att detta inte Ă€r en helt vanlig begrĂ€nsning, utan en uppskjuten (), dvs. att verifieringen av det utförs senare Ă€n SQL-kommandot, vilket leder till ovĂ€ntade konsekvenser.
Uppskjutna begrÀnsningar: varför de behövs och hur de fungerar
Lite teori om uppskjutna begrÀnsningar.
LĂ„t oss övervĂ€ga ett enkelt exempel: vi har en referenstabell över bilar med tvĂ„ attribut â namn och ordningen pĂ„ bilen i referenslistan.

create table cars
(
name text constraint pk_cars primary key,
ord integer not null constraint uk_cars unique
);
Anta att vi behöver byta plats pÄ den första och andra bilen. En "rak" lösning Àr att uppdatera det första vÀrdet till det andra, och det andra till det första:
begin;
update cars set ord = 2 where name = 'audi';
update cars set ord = 1 where name = 'bmw';
commit;
Men nÀr vi utför denna kod förvÀntar vi oss naturligtvis ett brott mot begrÀnsningen eftersom ordningen pÄ vÀrdena i tabellen Àr unik:
[23305] FEL: duplicerat nyckelvĂ€rde bryter mot unik begrĂ€nsning âuk_carsâ
Detalj: Nyckel (ord)=(2) finns redan.
Hur gör man pĂ„ ett annat sĂ€tt? Första alternativet: lĂ€gg till en extra ersĂ€ttning av vĂ€rdet till en ordning som garanterat inte finns i tabellen, till exempel â-1â. Inom programmering kallas detta âbyta vĂ€rden pĂ„ tvĂ„ variabler med hjĂ€lp av en tredjeâ. Den enda nackdelen med denna metod Ă€r ytterligare en uppdatering.
Andra alternativet: omdesigna tabellen för att anvÀnda flyttal för ordningen istÀllet för heltal. DÄ, vid uppdatering av vÀrden med 1 till exempel till 2.5, kommer den första posten automatiskt att placera sig mellan den andra och tredje. Denna lösning fungerar, men det finns tvÄ begrÀnsningar. För det första passar den dig inte om vÀrdet anvÀnds nÄgonstans i grÀnssnittet. För det andra, beroende pÄ noggrannheten hos datatypen, kommer du att ha ett begrÀnsat antal möjliga insÀttningar innan omberÀkning av alla posters vÀrden.
Tredje alternativet: gör begrÀnsningen uppskjuten, sÄ att den endast kontrolleras vid commit:
create table cars
(
name text constraint pk_cars primary key,
ord integer not null constraint uk_cars unique deferrable initially deferred
);Eftersom logiken i vÄr ursprungliga frÄga garanterar att alla vÀrden Àr unika vid tidpunkten för commit, kommer den att genomföras framgÄngsrikt.
Exemplet ovan Àr förstÄs mycket syntetiskt, men det förklarar idén. I vÄr applikation anvÀnder vi uppskjutna begrÀnsningar för att implementera logik som hanterar konflikter nÀr flera anvÀndare arbetar med gemensamma objekt-widgetar pÄ tavlan. AnvÀndningen av sÄdana begrÀnsningar gör att vi kan göra den applicerade koden nÄgot enklare.
Ăverlag, beroende pĂ„ typen av begrĂ€nsning i Postgres, finns det tre nivĂ„er av granularitet för deras kontroll: radnivĂ„, transaktionsnivĂ„ och uttrycksnivĂ„.

KĂ€lla:
CHECK och NOT NULL kontrolleras alltid pÄ radnivÄ, medan för andra begrÀnsningar, som framgÄr av tabellen, finns olika alternativ. Detaljer kan lÀsas om. .
Sammanfattningsvis ger uppskjutna begrÀnsningar i flera situationer mer lÀsbar kod och fÀrre kommandon. Men man fÄr betala för detta med en mer komplicerad felsökningsprocess, eftersom tidpunkten för att ett fel uppstÄr och tidpunkten för nÀr man fÄr veta om det Àr Ätskild. En annan potentiell problematik Àr att schemalÀggaren inte alltid kan bygga en optimal plan om den inkluderar en uppskjuten begrÀnsning i förfrÄgan.
FörbÀttring av pg_repack
Vi har förstÄtt vad uppskjutna begrÀnsningar Àr, men hur hÀnger de ihop med vÄrt problem? LÄt oss Äterkalla felet vi fick tidigare:
$ ./pg_repack -t tablename -o id
INFO: ompackar tabellen "tablename"
ERROR: frÄgan misslyckades:
ERROR: duplicerat nyckelvÀrde bryter mot unik begrÀnsning "index_16508"
DETAIL: Nyckel (id, index)=(100500, 42) existerar redan.Det uppstÄr i samband med kopieringen av data frÄn loggtabellen till den nya tabellen. Det verkar mÀrkligt, eftersom data i loggtabellen committas tillsammans med data frÄn den ursprungliga tabellen. Om de uppfyller begrÀnsningarna för den ursprungliga tabellen, hur kan de dÄ bryta mot samma begrÀnsningar i den nya?
Som det visar sig ligger roten till problemet i det föregÄende steget av pg_repack, dÀr endast index skapas, men inga begrÀnsningar: den gamla tabellen hade en unik begrÀnsning, och i den nya skapades istÀllet ett unikt index.

Det Àr viktigt att notera att om begrÀnsningen Àr vanlig och inte uppskjuten, sÄ Àr det skapade unika indexet likvÀrdigt med denna begrÀnsning, eftersom unika begrÀnsningar i Postgres implementeras genom att skapa ett unikt index. Men i fallet med en uppskjuten begrÀnsning Àr beteendet inte detsamma, eftersom indexet inte kan vara uppskjutet och alltid kontrolleras vid körningen av SQL-kommandot.
SÄledes ligger problemet i "uppskjutningen" av kontrollen: i den ursprungliga tabellen sker den vid tidpunkten för commit, medan den i den nya sker vid tidpunkten för körningen av SQL-kommandot. Det innebÀr att vi mÄste se till att kontrollerna görs pÄ samma sÀtt i bÄda fallen: antingen alltid uppskjutet eller alltid omedelbart.
SÄ, vilka idéer hade vi.
Skapa ett index som Àr likvÀrdigt med deferred
Den första idén Àr att utföra bÄda kontrollerna i omedelbart lÀge. Detta kan leda till flera falska positiva svar vid begrÀnsningar, men om de Àr fÄ bör det inte pÄverka anvÀndarnas arbete, eftersom sÄdana konflikter Àr en normal situation för dem. De intrÀffar till exempel nÀr tvÄ anvÀndare börjar redigera samma widget samtidigt, och klienten för den andra anvÀndaren hinner inte fÄ information om att widgeten redan Àr lÄst för redigering av den första anvÀndaren. I en sÄdan situation svarar servern den andra anvÀndaren med ett avslag, och deras klient rullar tillbaka Àndringarna och lÄser widgeten. NÄgon tids efterÄt, nÀr den första anvÀndaren avslutar redigeringen, fÄr den andra information om att widgeten inte lÀngre Àr lÄst, och kan upprepa sin ÄtgÀrd.

För att kontrollerna alltid ska vara i brÄdskande lÀge har vi skapat ett nytt index, liknande det ursprungliga fördröjda begrÀnsningen:
CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- kör pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;I testmiljön fick vi bara nÄgra fÄ förvÀntade fel. FramgÄng! Vi körde Äter pg_repack pÄ produkten och fick 5 fel pÄ den första klustern under en timmes drift. Detta Àr ett Acceptabelt resultat. Men redan pÄ den andra klustern ökade antalet fel flera gÄnger och vi var tvungna att stoppa pg_repack.
Varför hÀnde detta? Sannolikheten för att ett fel intrÀffar beror pÄ hur mÄnga anvÀndare som arbetar samtidigt med de samma widgetarna. Det verkar som om det vid den tiden fanns mycket fÀrre konkurrerande förÀndringar i data som lagrades pÄ den första klustern jÀmfört med de andra, det vill sÀga att vi helt enkelt hade "tur".
Idén fungerade inte. Vid den tiden sÄg vi tvÄ andra alternativ för att lösa problemet: omarbeta vÄr applikationskod för att avstÄ frÄn de fördröjda begrÀnsningarna, eller "lÀra" pg_repack att arbeta med dem. Vi valde den andra.
ErsÀtt indexerna i den nya tabellen med de fördröjda begrÀnsningarna frÄn den ursprungliga tabellen
MĂ„let med förbĂ€ttringen var uppenbart â om den ursprungliga tabellen har en fördröjd begrĂ€nsning, sĂ„ mĂ„ste en sĂ„dan begrĂ€nsning skapas för den nya istĂ€llet för ett index.
För att kontrollera vÄra förÀndringar skrev vi ett enkelt test:
- tabell med en fördröjd begrÀnsning och en post;
- vi sÀtter in data i en cykel som konflikterar med den befintliga posten;
- vi gör en uppdatering â data konflikter inte lĂ€ngre;
- vi committar Àndringar.
create table test_table
(
id serial,
val int,
constraint uk_test_table__val unique (val) deferrable initially deferred
);
INSERT INTO test_table (val) VALUES (0);
FOR i IN 1..10000 LOOP
BEGIN
INSERT INTO test_table VALUES (0) RETURNING id INTO v_id;
UPDATE test_table set val = i where id = v_id;
COMMIT;
END;
END LOOP;Den ursprungliga versionen av pg_repack kraschar alltid vid den första insÀttningen, den reviderade versionen fungerade utan fel. UtmÀrkt.
Vi gÄr live och fÄr Äterigen ett fel i samma fas nÀr datakopieringen frÄn loggtabellen till den nya:
$ ./pg_repack -t tablename -o id
INFO: ompackar tabellen "tablename"
ERROR: frÄgan misslyckades:
ERROR: duplicerat nyckelvÀrde bryter mot unik begrÀnsning "index_16508"
DETAIL: Nyckel (id, index)=(100500, 42) existerar redan.Klassisk situation: allt fungerar i testmiljöer, men inte i produktion?!
APPLY_COUNT och grÀnsen mellan tvÄ batchar
Vi började analysera koden rad för rad och upptÀckte en viktig punkt: överföringen av data frÄn loggtabellen till den nya sker i batchar, konstanten APPLY_COUNT angav storleken pÄ batchen:
for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);
if (num > MIN_TUPLES_BEFORE_SWITCH)
continue;
/* det kan fortfarande finnas nÄgra tuples, upprepa. */
...
}Problemet Ă€r att data frĂ„n den ursprungliga transaktionen, dĂ€r flera operationer potentiellt kan bryta mot begrĂ€nsningen, vid överföringen kan hamna i grĂ€nsen mellan tvĂ„ batchar â hĂ€lften av kommandona kommer att committas i den första batchen, medan den andra hĂ€lften kommer att committas i den andra. Och hĂ€r beror det pĂ„ tur: om kommandona i den första batchen inte bryter mot nĂ„gra begrĂ€nsningar, Ă€r allt bra, men om de bryter mot dem â uppstĂ„r ett fel.
APPLY_COUNT Ă€r 1000 poster, vilket förklarar varför vĂ„ra tester passerade framgĂ„ngsrikt â de tĂ€ckte inte fallet med "grĂ€nsen mellan batcharna". Vi anvĂ€nde tvĂ„ kommandon â insert och update, sĂ„ exakt 500 transaktioner med tvĂ„ kommandon fick alltid plats i batchen och vi upplevde inga problem. Efter att ha lagt till en andra update slutade vĂ„r modifiering att fungera:
FOR i IN 1..10000 LOOP
BEGIN
INSERT INTO test_table VALUES (1) RETURNING id INTO v_id;
UPDATE test_table set val = i where id = v_id;
UPDATE test_table set val = i where id = v_id; -- en till update
COMMIT;
END;
END LOOP;SÄ nÀsta uppgift Àr att se till att data frÄn den ursprungliga tabellen, som förÀndrades i en transaktion, ocksÄ hamnar i den nya tabellen inom samma transaktion.
AvstÄende frÄn batchning
Och vi hade Ă„terigen tvĂ„ lösningar. Första: lĂ„t oss helt avstĂ„ frĂ„n att dela upp i batchar och istĂ€llet göra dataöverföringen i en transaktion. För detta beslut talade dess enkelhet â de nödvĂ€ndiga kodĂ€ndringarna Ă€r minimala (för övrigt fungerade pg_reorg i Ă€ldre versioner pĂ„ just detta sĂ€tt). Men det finns ett problem â vi skapar en lĂ„ngvarig transaktion, och detta, som nĂ€mnts tidigare, utgör en risk för att ny bloat uppstĂ„r.
Den andra lösningen Àr mer komplex, men antagligen mer korrekt: skapa en kolumn i loggtabellen med identifieraren för den transaktion som la till data i tabellen. DÄ kan vi, nÀr vi kopierar data, gruppera dem efter denna attribut och garantera att relaterade Àndringar överförs tillsammans. Batchen kommer att bestÄ av flera transaktioner (eller en stor) och dess storlek kommer att variera beroende pÄ hur mycket data som har Àndrats i dessa transaktioner. Det Àr viktigt att notera att eftersom data frÄn olika transaktioner kommer in i loggtabellen i slumpmÀssig ordning, kommer det inte lÀngre att vara möjligt att lÀsa den sekventiellt, som det var tidigare. seqscan med filtrering efter tx_id vid varje förfrÄgan Àr alltför kostsamt, vi behöver en index, men det kommer att sakta ner metoden pÄ grund av kostnaderna för att uppdatera det. Som vanligt mÄste man offra nÄgot.
SÄ vi bestÀmde oss för att börja med det första alternativet, dÄ det var enklare. Först behövde vi förstÄ om en lÄngvarig transaktion verkligen skulle vara ett problem. Eftersom den huvudsakliga dataöverföringen frÄn den gamla tabellen till den nya ocksÄ sker i en lÄngvarig transaktion, förÀndrades frÄgan till "hur mycket kommer vi att öka denna transaktion?" Varaktigheten av den första transaktionen beror frÀmst pÄ storleken pÄ tabellen. Varaktigheten av den nya beror pÄ hur mycket Àndringar som samlas i tabellen under databelÀggningen, dvs. frÄn belastningens intensitet. pg_repack kördes under minimal belastning pÄ tjÀnsten, och mÀngden Àndringar var i jÀmförelse med det ursprungliga tabellinnehÄllet obetydlig. Vi beslutade att vi kunde bortse frÄn tiden för den nya transaktionen (i genomsnitt Àr detta 1 timme och 2-3 minuter).
Experimenten var framgĂ„ngsrika. Produktionsstarten ocksĂ„. För tydlighetens skull â en bild som visar storleken pĂ„ en av databaserna efter körningen:

Eftersom denna lösning fullt ut passar oss, sÄ har vi inte prövat att implementera en andra, men vi övervÀger möjligheten att diskutera det med utvecklarna av tillÀgget. TyvÀrr Àr vÄr nuvarande förbÀttring Ànnu inte klar för publicering, eftersom vi bara har löst problemet med unika förseningar, och för en fullstÀndig patch behöver vi Àven stöd för andra typer. Vi hoppas kunna göra detta i framtiden.
Kanske har du undrat varför vi överhuvudtaget gav oss in i denna historia med att förbÀttra pg_repack, istÀllet för att anvÀnda dess motsvarigheter? Vid nÄgot tillfÀlle tÀnkte vi ocksÄ sÄ, men den positiva erfarenheten vi hade tidigare, med tabeller utan förseningar, motiverade oss att försöka förstÄ problemet och ÄtgÀrda det. Dessutom krÀver anvÀndningen av andra lösningar ocksÄ tid för tester, sÄ vi beslutade att först försöka lösa problemet i det och om vi inser att vi inte kan göra det inom rimlig tid, dÄ börjar vi övervÀga alternativ.
Slutsatser
Vad vi kan rekommendera baserat pÄ egen erfarenhet:
- Ăvervaka din bloat. Baserat pĂ„ övervakningsdata kan du förstĂ„ hur vĂ€l autovacuum Ă€r instĂ€llt.
- Konfigurera AUTOVACUUM för att hÄlla bloat pÄ acceptabel nivÄ.
- Om bloat ÀndÄ ökar och du inte kan övervinna det med hjÀlp av "out-of-the-box"-lösningar, tveka inte att anvÀnda externa tillÀgg. Det viktigaste Àr att noggrant testa allt.
- Tveka inte att förbĂ€ttra externa lösningar för dina behov â ibland kan detta vara mer effektivt och till och med enklare Ă€n att Ă€ndra din egen kod.
KĂ€lla: habr.com
