Postgres: bloat, pg_repack dhe kufizimet e shtyra

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Efekti i fryrjes në tabela dhe indekse është i njohur gjerësisht dhe është i pranishëm jo vetëm në Postgres. Ka mënyra për ta trajtuar atë jashtë kutisë, si VACUUM FULL ose CLUSTER, por ato bllokojnë tabelat gjatë funksionimit dhe për këtë arsye nuk mund të përdoren gjithmonë.

Artikulli do të përmbajë një teori të vogël rreth asaj se si ndodh fryrja, si mund ta luftoni atë, për kufizimet e shtyra dhe problemet që ato sjellin në përdorimin e shtesës pg_repack.

Ky artikull është shkruar në bazë të fjalimi im në PgConf.Russia 2020.

Pse ndodh fryrja?

Postgres bazohet në një model me shumë versione (MVCC). Thelbi i tij është se çdo rresht në tabelë mund të ketë disa versione, ndërsa transaksionet nuk shohin më shumë se një nga këto versione, por jo domosdoshmërisht të njëjtin. Kjo lejon që disa transaksione të funksionojnë njëkohësisht dhe praktikisht të mos kenë asnjë ndikim mbi njëri-tjetrin.

Natyrisht, të gjitha këto versione duhet të ruhen. Postgres punon me memorie faqe për faqe dhe një faqe është sasia minimale e të dhënave që mund të lexohet ose të shkruhet nga disku. Le të shohim një shembull të vogël për të kuptuar se si ndodh kjo.

Le të themi se kemi një tabelë në të cilën kemi shtuar disa regjistrime. Në faqen e parë të skedarit ku ruhet tabela janë shfaqur të dhëna të reja. Këto janë versione të drejtpërdrejta të rreshtave që janë të disponueshme për transaksione të tjera pas një kryerjeje (për thjeshtësi, ne do të supozojmë se niveli i izolimit është Read Committed).

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Më pas përditësuam një nga hyrjet, duke shënuar kështu versionin e vjetër si jo më të përshtatshëm.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Hap pas hapi, duke përditësuar dhe fshirë versionet e rreshtave, përfunduam me një faqe në të cilën afërsisht gjysma e të dhënave janë "plehra". Këto të dhëna nuk janë të dukshme për asnjë transaksion.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Postgres ka një mekanizëm vakum, i cili pastron versionet e vjetruara dhe krijon hapësirë ​​për të dhëna të reja. Por nëse nuk është konfiguruar mjaftueshëm në mënyrë agresive ose është i zënë duke punuar në tabela të tjera, atëherë mbeten "të dhënat e mbeturinave" dhe ne duhet të përdorim faqe shtesë për të dhëna të reja.

Pra, në shembullin tonë, në një moment në kohë tabela do të përbëhet nga katër faqe, por vetëm gjysma e saj do të përmbajë të dhëna të drejtpërdrejta. Si rezultat, kur të qasemi në tabelë, do të lexojmë shumë më tepër të dhëna sesa duhet.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Edhe nëse VACUUM tani fshin të gjitha versionet e rreshtave të parëndësishëm, situata nuk do të përmirësohet në mënyrë dramatike. Do të kemi hapësirë ​​të lirë në faqe apo edhe faqe të tëra për rreshta të rinj, por do të lexojmë akoma më shumë të dhëna sesa duhet.
Nga rruga, nëse një faqe plotësisht e zbrazët (e dyta në shembullin tonë) do të ishte në fund të skedarit, atëherë VACUUM do të ishte në gjendje ta shkurtonte atë. Por tani ajo është në mes, kështu që asgjë nuk mund të bëhet me të.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Kur numri i faqeve të tilla boshe ose shumë të rralla bëhet i madh, i cili quhet fryrje, ai fillon të ndikojë në performancën.

Gjithçka e përshkruar më sipër është mekanika e shfaqjes së fryrjes në tabela. Në indekse kjo ndodh pothuajse në të njëjtën mënyrë.

A kam fryrje?

Ka disa mënyra për të përcaktuar nëse keni fryrje. Ideja e së parës është përdorimi i statistikave të brendshme të Postgres, të cilat përmbajnë informacion të përafërt për numrin e rreshtave në tabela, numrin e rreshtave "live" etj. Në internet mund të gjeni shumë variacione të skripteve të gatshme. Ne e morëm si bazë skenar nga PostgreSQL Experts, të cilët mund të vlerësojnë tabelat e fryrjes së bashku me indekset e tost dhe btree bloat. Në përvojën tonë, gabimi i tij është 10-20%.

Një mënyrë tjetër është përdorimi i shtesës pgstattuple, e cila ju lejon të shikoni brenda faqeve dhe të merrni një vlerë të vlerësuar dhe të saktë të fryrjes. Por në rastin e dytë, do t'ju duhet të skanoni të gjithë tabelën.

Ne e konsiderojmë të pranueshme një vlerë të vogël fryrjeje, deri në 20%. Mund të konsiderohet si një analog i faktorit mbushës për tavolina и tregues. Në 50% e lart, mund të fillojnë problemet e performancës.

Mënyrat për të luftuar fryrjen

Postgres ka disa mënyra për t'u marrë me fryrjen jashtë kutisë, por ato nuk janë gjithmonë të përshtatshme për të gjithë.

Konfiguro AUTOVACUUM në mënyrë që të mos ndodhë fryrje. Ose më saktë, për ta mbajtur atë në një nivel të pranueshëm për ju. Kjo duket si këshilla e "kapitenit", por në realitet kjo nuk është gjithmonë e lehtë për t'u arritur. Për shembull, ju keni një zhvillim aktiv me ndryshime të rregullta në skemën e të dhënave, ose po ndodh një lloj migrimi i të dhënave. Si rezultat, profili juaj i ngarkesës mund të ndryshojë shpesh dhe zakonisht do të ndryshojë nga tabela në tabelë. Kjo do të thotë që ju duhet të punoni vazhdimisht pak përpara dhe të përshtatni AUTOVACUUM në profilin e ndryshimit të çdo tavoline. Por padyshim që kjo nuk është e lehtë për t'u bërë.

Një arsye tjetër e zakonshme pse AUTOVACUUM nuk mund të vazhdojë me tabelat është sepse ka transaksione afatgjata që e pengojnë atë të pastrojë të dhënat që janë në dispozicion për ato transaksione. Rekomandimi këtu është gjithashtu i qartë - hiqni qafe transaksionet "të varura" dhe minimizoni kohën e transaksioneve aktive. Por nëse ngarkesa në aplikacionin tuaj është një hibrid i OLAP dhe OLTP, atëherë mund të keni njëkohësisht shumë përditësime të shpeshta dhe pyetje të shkurtra, si dhe operacione afatgjata - për shembull, ndërtimi i një raporti. Në një situatë të tillë, ia vlen të mendoni për përhapjen e ngarkesës nëpër baza të ndryshme, gjë që do të lejojë më shumë rregullim të hollësishëm të secilës prej tyre.

Një shembull tjetër - edhe nëse profili është homogjen, por baza e të dhënave është nën një ngarkesë shumë të lartë, atëherë edhe AUTOVACUUM më agresiv mund të mos përballojë dhe do të ndodhë fryrje. Shkallëzimi (vertikal ose horizontal) është zgjidhja e vetme.

Çfarë duhet të bëni në një situatë ku keni vendosur AUTOVACUUM, por fryrja vazhdon të rritet.

Ekip FSHESËSIA E PLOTË rindërton përmbajtjen e tabelave dhe indekseve dhe lë në to vetëm të dhëna përkatëse. Për të eliminuar fryrjen, funksionon në mënyrë perfekte, por gjatë ekzekutimit të tij kapet një bllokim ekskluziv në tryezë (AccessExclusiveLock), i cili nuk do të lejojë ekzekutimin e pyetjeve në këtë tabelë, madje edhe përzgjedh. Nëse keni mundësi të ndaloni shërbimin tuaj ose një pjesë të tij për ca kohë (nga dhjetëra minuta në disa orë në varësi të madhësisë së bazës së të dhënave dhe harduerit tuaj), atëherë ky opsion është më i miri. Fatkeqësisht, nuk kemi kohë për të ekzekutuar VACUUM FULL gjatë mirëmbajtjes së planifikuar, kështu që kjo metodë nuk është e përshtatshme për ne.

Ekip KLASTERI Rindërton përmbajtjen e tabelave në të njëjtën mënyrë si VACUUM FULL, por ju lejon të specifikoni një indeks sipas të cilit të dhënat do të renditen fizikisht në disk (por në të ardhmen renditja nuk është e garantuar për rreshtat e rinj). Në situata të caktuara, ky është një optimizim i mirë për një numër pyetjesh - me leximin e të dhënave të shumta sipas indeksit. Disavantazhi i komandës është i njëjtë me atë të VACUUM FULL - bllokon tabelën gjatë funksionimit.

Ekip REINDEX ngjashëm me dy të mëparshmet, por rindërton një indeks specifik ose të gjithë indekset e tabelës. Kyçet janë pak më të dobët: ShareLock në tabelë (parandalon modifikimet, por lejon përzgjedhjen) dhe AccessExclusiveLock në indeksin që po rindërtohet (bllokon pyetjet duke përdorur këtë indeks). Sidoqoftë, në versionin e 12-të të Postgres u shfaq një parametër Njëkohësisht, i cili ju lejon të rindërtoni indeksin pa bllokuar shtimin, modifikimin ose fshirjen e njëkohshme të të dhënave.

Në versionet e mëparshme të Postgres, ju mund të arrini një rezultat të ngjashëm me REINDEX NË NJËSHTJE duke përdorur KRIJONI INDEKSIN NË NJËZOR. Kjo ju lejon të krijoni një indeks pa mbyllje të rreptë (ShareUpdateExclusiveLock, e cila nuk ndërhyn me pyetjet paralele), më pas zëvendësoni indeksin e vjetër me një të ri dhe fshini indeksin e vjetër. Kjo ju lejon të eliminoni fryrjen e indeksit pa ndërhyrë në aplikimin tuaj. Është e rëndësishme të merret parasysh që gjatë rindërtimit të indekseve do të ketë një ngarkesë shtesë në nënsistemin e diskut.

Kështu, nëse për indekset ka mënyra për të eliminuar fryrjen "në fluturim", atëherë nuk ka asnjë për tabelat. Këtu hyjnë në lojë shtesat e ndryshme të jashtme: pg_repack (më parë pg_reorg), pgkompakt, pgkompaktueshme dhe të tjerët. Në këtë artikull, unë nuk do t'i krahasoj ato dhe do të flas vetëm për pg_repack, të cilin, pas disa modifikimeve, ne e përdorim vetë.

Si funksionon pg_repack

Postgres: bloat, pg_repack dhe kufizimet e shtyra
Le të themi se kemi një tabelë krejtësisht të zakonshme - me indekse, kufizime dhe, për fat të keq, me fryrje. Hapi i parë i pg_repack është krijimi i një tabele regjistrash për të ruajtur të dhënat për të gjitha ndryshimet gjatë ekzekutimit. Shkaku do të përsërisë këto ndryshime për çdo futje, përditësim dhe fshirje. Më pas krijohet një tabelë, e ngjashme me atë origjinale në strukturë, por pa indekse dhe kufizime, për të mos ngadalësuar procesin e futjes së të dhënave.

Më pas, pg_repack transferon të dhënat nga tabela e vjetër në tabelën e re, duke filtruar automatikisht të gjitha rreshtat e parëndësishme dhe më pas krijon indekse për tabelën e re. Gjatë ekzekutimit të të gjitha këtyre operacioneve, ndryshimet grumbullohen në tabelën e regjistrave.

Hapi tjetër është transferimi i ndryshimeve në tabelën e re. Migrimi kryhet në disa përsëritje dhe kur ka më pak se 20 hyrje në tabelën e regjistrave, pg_repack fiton një bllokim të fortë, migron të dhënat më të fundit dhe zëvendëson tabelën e vjetër me të renë në tabelat e sistemit Postgres. Kjo është koha e vetme dhe shumë e shkurtër kur nuk do të mund të punoni me tavolinën. Pas kësaj, tabela e vjetër dhe tabela me regjistrat fshihen dhe lirohet hapësira në sistemin e skedarëve. Procesi ka përfunduar.

Gjithçka duket e mrekullueshme në teori, por çfarë ndodh në praktikë? Ne testuam pg_repack pa ngarkesë dhe nën ngarkesë dhe kontrolluam funksionimin e tij në rast të ndalimit të parakohshëm (me fjalë të tjera, duke përdorur Ctrl+C). Të gjitha testet ishin pozitive.

Ne shkuam në dyqan ushqimor - dhe më pas gjithçka nuk shkoi siç prisnim.

Petulla e parë në shitje

Në grupin e parë morëm një gabim në lidhje me një shkelje të një kufizimi unik:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Ky kufizim kishte një emër të gjeneruar automatikisht index_16508 - ai u krijua nga pg_repack. Bazuar në atributet e përfshira në përbërjen e tij, ne përcaktuam kufizimin "tonin" që korrespondon me të. Problemi doli të jetë se ky nuk është një kufizim krejtësisht i zakonshëm, por një i shtyrë (kufizimi i shtyrë), d.m.th. verifikimi i tij kryhet më vonë se komanda sql, gjë që çon në pasoja të papritura.

Kufizimet e shtyra: pse nevojiten dhe si funksionojnë

Një teori e vogël rreth kufizimeve të shtyra.
Le të shqyrtojmë një shembull të thjeshtë: ne kemi një libër referimi në tabelë të makinave me dy atribute - emrin dhe rendin e makinës në drejtori.
Postgres: bloat, pg_repack dhe kufizimet e shtyra

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique
);



Le të themi se duhej të ndërronim makinat e para dhe të dyta. Zgjidhja e drejtpërdrejtë është të përditësoni vlerën e parë në të dytën dhe të dytën në të parën:

begin;
  update cars set ord = 2 where name = 'audi';
  update cars set ord = 1 where name = 'bmw';
commit;

Por kur ekzekutojmë këtë kod, presim një shkelje të kufizimit sepse rendi i vlerave në tabelë është unik:

[23305] ERROR: duplicate key value violates unique constraint “uk_cars”
Detail: Key (ord)=(2) already exists.

Si mund ta bëj ndryshe? Opsioni i parë: shtoni një zëvendësim me vlerë shtesë në një porosi që garantohet se nuk ekziston në tabelë, për shembull "-1". Në programim, kjo quhet "shkëmbimi i vlerave të dy variablave përmes një të tretës". E vetmja pengesë e kësaj metode është përditësimi shtesë.

Opsioni dy: Ridizajnoni tabelën për të përdorur një lloj të dhënash me pikë lundruese për vlerën e rendit në vend të numrave të plotë. Pastaj, kur përditësoni vlerën nga 1, për shembull, në 2.5, hyrja e parë do të "qëndrojë" automatikisht midis të dytit dhe të tretës. Kjo zgjidhje funksionon, por ka dy kufizime. Së pari, nuk do të funksionojë për ju nëse vlera përdoret diku në ndërfaqe. Së dyti, në varësi të saktësisë së llojit të të dhënave, do të keni një numër të kufizuar futjesh të mundshme përpara se të rillogaritni vlerat e të gjitha rekordeve.

Opsioni i tretë: bëjeni kufizimin të shtyrë në mënyrë që të kontrollohet vetëm në kohën e kryerjes:

create table cars
(
  name text constraint pk_cars primary key,
  ord integer not null constraint uk_cars unique deferrable initially deferred
);

Meqenëse logjika e kërkesës sonë fillestare siguron që të gjitha vlerat janë unike në kohën e kryerjes, ajo do të ketë sukses.

Shembulli i diskutuar më sipër është, natyrisht, shumë sintetik, por zbulon idenë. Në aplikacionin tonë, ne përdorim kufizime të shtyra për të zbatuar logjikën që është përgjegjëse për zgjidhjen e konflikteve kur përdoruesit punojnë njëkohësisht me objekte të miniaplikacioneve të përbashkëta në tabelë. Përdorimi i kufizimeve të tilla na lejon ta bëjmë kodin e aplikacionit pak më të thjeshtë.

Në përgjithësi, në varësi të llojit të kufizimit, Postgres ka tre nivele të hollësive për kontrollimin e tyre: nivelet e rreshtit, transaksionit dhe shprehjes.
Postgres: bloat, pg_repack dhe kufizimet e shtyra
Burimi: kërkues

CHECK dhe NOT NULL kontrollohen gjithmonë në nivelin e rreshtit; për kufizime të tjera, siç shihet nga tabela, ka opsione të ndryshme. Mund të lexoni më shumë këtu.

Për ta përmbledhur shkurtimisht, kufizimet e shtyra në një numër situatash ofrojnë kod më të lexueshëm dhe më pak komanda. Megjithatë, për këtë duhet të paguani duke e komplikuar procesin e korrigjimit, pasi në momentin që ndodh gabimi dhe momenti kur e mësoni janë të ndara në kohë. Një problem tjetër i mundshëm është se planifikuesi mund të mos jetë gjithmonë në gjendje të ndërtojë një plan optimal nëse kërkesa përfshin një kufizim të shtyrë.

Përmirësimi i pg_repack

Ne kemi mbuluar se cilat janë kufizimet e shtyra, por si lidhen ato me problemin tonë? Le të kujtojmë gabimin që kemi marrë më parë:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Ndodh kur të dhënat kopjohen nga një tabelë regjistri në një tabelë të re. Kjo duket e çuditshme sepse... të dhënat në tabelën e regjistrit angazhohen së bashku me të dhënat në tabelën burimore. Nëse plotësojnë kufizimet e tabelës origjinale, si mund të shkelin të njëjtat kufizime në tabelën e re?

Siç rezulton, rrënja e problemit qëndron në hapin e mëparshëm të pg_repack, i cili krijon vetëm indekse, por jo kufizime: tabela e vjetër kishte një kufizim unik, dhe e reja krijoi një indeks unik në vend.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Është e rëndësishme të theksohet këtu se nëse kufizimi është normal dhe jo i shtyrë, atëherë indeksi unik i krijuar në vend është ekuivalent me këtë kufizim, sepse Kufizimet unike në Postgres zbatohen duke krijuar një indeks unik. Por në rastin e një kufizimi të shtyrë, sjellja nuk është e njëjtë, sepse indeksi nuk mund të shtyhet dhe kontrollohet gjithmonë në kohën kur ekzekutohet komanda sql.

Kështu, thelbi i problemit qëndron në "vonesën" e kontrollit: në tabelën origjinale ndodh në momentin e kryerjes, dhe në tabelën e re në kohën kur ekzekutohet komanda sql. Kjo do të thotë që duhet të sigurohemi që kontrollet të kryhen njësoj në të dyja rastet: ose gjithmonë me vonesë, ose gjithmonë menjëherë.

Pra, çfarë idesh kishim ne?

Krijo një indeks të ngjashëm me të shtyrë

Ideja e parë është të kryhen të dy kontrollet në modalitetin e menjëhershëm. Kjo mund të gjenerojë disa kufizime false pozitive, por nëse ka pak prej tyre, kjo nuk duhet të ndikojë në punën e përdoruesve, pasi konflikte të tilla janë një situatë normale për ta. Ato ndodhin, për shembull, kur dy përdorues fillojnë të redaktojnë të njëjtin miniaplikacion në të njëjtën kohë, dhe klienti i përdoruesit të dytë nuk ka kohë të marrë informacion se miniaplikacioni tashmë është i bllokuar për modifikim nga përdoruesi i parë. Në një situatë të tillë, serveri refuzon përdoruesin e dytë dhe klienti i tij i kthen ndryshimet dhe bllokon widget-in. Pak më vonë, kur përdoruesi i parë të përfundojë redaktimin, i dyti do të marrë informacion se widget nuk është më i bllokuar dhe do të jetë në gjendje të përsërisë veprimin e tij.

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Për të siguruar që kontrollet janë gjithmonë në modalitetin jo të shtyrë, ne krijuam një indeks të ri të ngjashëm me kufizimin origjinal të shtyrë:

CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- run pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;

Në mjedisin e testimit, ne morëm vetëm disa gabime të pritshme. Sukses! Ne ekzekutuam pg_repack përsëri në prodhim dhe morëm 5 gabime në grupin e parë në një orë pune. Ky është një rezultat i pranueshëm. Sidoqoftë, tashmë në grupin e dytë numri i gabimeve u rrit ndjeshëm dhe ne duhej të ndalonim pg_repack.

Pse ndodhi? Mundësia e shfaqjes së një gabimi varet nga numri i përdoruesve që punojnë me të njëjtat widget në të njëjtën kohë. Me sa duket, në atë moment kishte shumë më pak ndryshime konkurruese me të dhënat e ruajtura në grupin e parë sesa në të tjerët, d.m.th. ne ishim thjesht "me fat".

Ideja nuk funksionoi. Në atë pikë, ne pamë dy zgjidhje të tjera: të rishkruajmë kodin tonë të aplikacionit për të hequr qafe kufizimet e shtyra, ose "të mësojmë" pg_repack të punojë me ta. Ne zgjodhëm të dytën.

Zëvendësoni indekset në tabelën e re me kufizime të shtyra nga tabela origjinale

Qëllimi i rishikimit ishte i qartë - nëse tabela origjinale ka një kufizim të shtyrë, atëherë për të renë duhet të krijoni një kufizim të tillë, dhe jo një indeks.

Për të testuar ndryshimet tona, ne shkruam një test të thjeshtë:

  • tabelë me një kufizim të shtyrë dhe një rekord;
  • futni të dhëna në një lak që bie ndesh me një rekord ekzistues;
  • bëni një përditësim - të dhënat nuk janë më në konflikt;
  • kryeni ndryshimet.

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;

Versioni origjinal i pg_repack u rrëzua gjithmonë në futjen e parë, versioni i modifikuar funksionoi pa gabime. E madhe.

Ne shkojmë në prodhim dhe përsëri marrim një gabim në të njëjtën fazë të kopjimit të të dhënave nga tabela e regjistrit në një të re:

$ ./pg_repack -t tablename -o id
INFO: repacking table "tablename"
ERROR: query failed: 
    ERROR: duplicate key value violates unique constraint "index_16508"
DETAIL:  Key (id, index)=(100500, 42) already exists.

Situata klasike: gjithçka funksionon në mjedise testimi, por jo në prodhim?!

APPLY_COUNT dhe kryqëzimi i dy grupeve

Ne filluam të analizojmë kodin fjalë për fjalë rresht pas rreshti dhe zbuluam një pikë të rëndësishme: të dhënat transferohen nga tabela e regjistrit në një të re në grupe, konstanta APPLY_COUNT tregoi madhësinë e grupit:

for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);

if (num > MIN_TUPLES_BEFORE_SWITCH)
     continue;  /* there might be still some tuples, repeat. */
...
}

Problemi është se të dhënat nga transaksioni origjinal, në të cilin disa operacione potencialisht mund të shkelin kufizimin, kur transferohen, mund të përfundojnë në kryqëzimin e dy grupeve - gjysma e komandave do të kryhen në grupin e parë, dhe gjysma tjetër në të dytën. Dhe këtu, në varësi të fatit tuaj: nëse skuadrat nuk shkelin asgjë në grupin e parë, atëherë gjithçka është në rregull, por nëse e bëjnë, ndodh një gabim.

APPLY_COUNT është e barabartë me 1000 rekorde, gjë që shpjegon pse testet tona ishin të suksesshme - ato nuk mbuluan rastin e "bashkimit të grupeve". Ne kemi përdorur dy komanda - insert dhe update, kështu që saktësisht 500 transaksione të dy komandave janë vendosur gjithmonë në një grup dhe nuk kemi pasur asnjë problem. Pas shtimit të përditësimit të dytë, redaktimi ynë pushoi së funksionuari:

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; -- one more update
    COMMIT;
  END;
END LOOP;

Pra, detyra tjetër është të sigurohemi që të dhënat nga tabela origjinale, e cila është ndryshuar në një transaksion, të përfundojnë në tabelën e re gjithashtu brenda një transaksioni.

Refuzimi nga grumbullimi

Dhe përsëri kishim dy zgjidhje. Së pari: le të braktisim plotësisht ndarjen në grupe dhe të transferojmë të dhënat në një transaksion. Avantazhi i kësaj zgjidhjeje ishte thjeshtësia e saj - ndryshimet e kërkuara të kodit ishin minimale (nga rruga, në versionet më të vjetra pg_reorg funksiononte pikërisht ashtu). Por ka një problem - ne po krijojmë një transaksion afatgjatë, dhe kjo, siç u tha më parë, është një kërcënim për shfaqjen e një fryrje të re.

Zgjidhja e dytë është më komplekse, por ndoshta më e saktë: krijoni një kolonë në tabelën e regjistrave me identifikuesin e transaksionit që shtoi të dhëna në tabelë. Pastaj, kur kopjojmë të dhënat, ne mund t'i grupojmë sipas këtij atributi dhe të sigurohemi që ndryshimet e lidhura të transferohen së bashku. Batch-i do të formohet nga disa transaksione (ose një i madh) dhe madhësia e tij do të ndryshojë në varësi të asaj se sa të dhëna janë ndryshuar në këto transaksione. Është e rëndësishme të theksohet se meqenëse të dhënat nga transaksionet e ndryshme hyjnë në tabelën e regjistrave në mënyrë të rastësishme, nuk do të jetë më e mundur të lexohen ato në mënyrë sekuenciale, siç ishte më parë. seqscan për çdo kërkesë me filtrim sipas tx_id është shumë i shtrenjtë, nevojitet një indeks, por gjithashtu do të ngadalësojë metodën për shkak të kostos së përgjithshme të përditësimit të tij. Në përgjithësi, si gjithmonë, duhet të sakrifikoni diçka.

Pra, vendosëm të fillojmë me opsionin e parë, pasi është më i thjeshtë. Së pari, ishte e nevojshme të kuptonim nëse një transaksion i gjatë do të ishte një problem real. Meqenëse transferimi kryesor i të dhënave nga tabela e vjetër në atë të re ndodh gjithashtu në një transaksion të gjatë, pyetja u shndërrua në "sa do ta rrisim këtë transaksion?" Kohëzgjatja e transaksionit të parë varet kryesisht nga madhësia e tabelës. Kohëzgjatja e një të reje varet nga sa ndryshime grumbullohen në tabelë gjatë transferimit të të dhënave, d.m.th. në intensitetin e ngarkesës. Ekzekutimi pg_repack ndodhi gjatë një kohe me ngarkesë minimale shërbimi dhe vëllimi i ndryshimeve ishte në mënyrë disproporcionale i vogël në krahasim me madhësinë origjinale të tabelës. Ne vendosëm që mund të neglizhonim kohën e një transaksioni të ri (për krahasim, mesatarisht është 1 orë e 2-3 minuta).

Eksperimentet ishin pozitive. Nis edhe në prodhim. Për qartësi, këtu është një fotografi me madhësinë e njërës prej bazave të të dhënave pas ekzekutimit:

Postgres: bloat, pg_repack dhe kufizimet e shtyra

Meqenëse ishim plotësisht të kënaqur me këtë zgjidhje, nuk u përpoqëm të zbatonim të dytën, por po shqyrtojmë mundësinë për ta diskutuar atë me zhvilluesit e ekstensionit. Rishikimi ynë aktual, për fat të keq, nuk është ende gati për botim, pasi ne e zgjidhëm problemin vetëm me kufizime unike të shtyra, dhe për një patch të plotë është e nevojshme të sigurohet mbështetje për llojet e tjera. Shpresojmë të jemi në gjendje ta bëjmë këtë në të ardhmen.

Ndoshta keni një pyetje, pse ne madje u përfshimë në këtë histori me modifikimin e pg_repack, dhe nuk kemi përdorur, për shembull, analogët e tij? Në një moment kemi menduar edhe për këtë, por përvoja pozitive e përdorimit të tij më herët, në tavolina pa kufizime të shtyra, na motivoi të përpiqemi të kuptojmë thelbin e problemit dhe ta rregullojmë atë. Për më tepër, përdorimi i zgjidhjeve të tjera kërkon gjithashtu kohë për të kryer teste, kështu që vendosëm që së pari të përpiqeshim të rregullonim problemin në të, dhe nëse kuptonim se nuk mund ta bënim këtë në një kohë të arsyeshme, atëherë do të fillonim të shikonim analogët .

Gjetjet

Çfarë mund të rekomandojmë bazuar në përvojën tonë:

  1. Monitoroni fryrjen tuaj. Bazuar në të dhënat e monitorimit, mund të kuptoni se sa mirë është konfiguruar autovakum.
  2. Rregulloni AUTOVACUUM-in për të mbajtur fryrjen në një nivel të pranueshëm.
  3. Nëse fryrja është ende në rritje dhe nuk mund ta kapërceni atë duke përdorur mjete të disponueshme, mos kini frikë të përdorni shtesa të jashtme. Gjëja kryesore është të testoni gjithçka mirë.
  4. Mos kini frikë të modifikoni zgjidhjet e jashtme për t'iu përshtatur nevojave tuaja - ndonjëherë kjo mund të jetë më efektive dhe madje më e lehtë sesa ndryshimi i kodit tuaj.

Burimi: www.habr.com

Shto një koment