
Efekti i fryrjes sĂ« tabelave dhe indekseve (bloat) Ă«shtĂ« i njohur gjerĂ«sisht dhe nuk ndodh vetĂ«m nĂ« Postgres. EkzistojnĂ« mĂ«nyra pĂ«r ta luftuar atĂ« ânga fabrikaâ siç janĂ« VACUUM FULL ose CLUSTER, por ato bllokojnĂ« tabelat gjatĂ« punĂ«s dhe prandaj nuk mund tĂ« pĂ«rdoren gjithmonĂ«.
Në këtë artikull do të ketë pak teori mbi atë se si ndodh bloat, si mund të luftohet, mbi kufizimet e vonuara dhe mbi problemet që ato sjellin në përdorimin e zgjerimit pg_repack.
Ky artikull është shkruar mbi bazën e në PgConf.Russia 2020.

Pse ndodh bloat
Postgres bazohet në një model me shumë versione (). Thelbi i tij është se çdo rresht në tabelë mund të ketë disa versionet, ndërsa transaksionet shohin jo më shumë se një nga këto versione, por nuk është e nevojshme që të jetë e njëjta. Kjo lejon disa transaksione të punojnë njëkohësisht dhe të mos ndikojnë pothuajse fare në njëra-tjetrën.
ĂshtĂ« e qartĂ« se tĂ« gjitha kĂ«to versione duhet tĂ« ruhen. Postgres punon me kujtesĂ«n nĂ« mĂ«nyrĂ« faqesh dhe faqeja Ă«shtĂ« minimumi i tĂ« dhĂ«nave qĂ« mund tĂ« lexohet nga disku ose tĂ« shkruhet. Le tĂ« shqyrtojmĂ« njĂ« shembull tĂ« vogĂ«l pĂ«r tĂ« kuptuar se si ndodh kjo.
Supozoni se kemi një tabelë, në të cilën kemi shtuar disa të dhëna. Në faqen e parë të skedarit, ku ruhen tabelat, janë shfaqur të dhëna të reja. Këto janë versione të drejtpërdrejta të rreshtave, të cilat janë në dispozicion për transaksione të tjera pas komitimit (për thjeshtësi, le të supozojmë se niveli i izolimit është Read Committed).

Më pas, ne përditësuam një nga regjistrimet dhe kështu e shënuam versionin e vjetër si të pavlefshëm.

Hap pas hapi, duke pĂ«rditĂ«suar dhe fshirĂ« versionet e rreshtave, ne fituam njĂ« faqe nĂ« tĂ« cilĂ«n rreth gjysma e tĂ« dhĂ«nave janĂ« âmbetjeâ. KĂ«to tĂ« dhĂ«na nuk shihen nga asnjĂ« transaksion.

NĂ« Postgres ekziston njĂ« mekanizĂ«m , i cili pastron versionet e pavlefshme dhe liçon hapĂ«sirĂ« pĂ«r tĂ« dhĂ«na tĂ« reja. Por nĂ«se ai nuk Ă«shtĂ« konfiguruar mjaft agresivisht ose Ă«shtĂ« i zĂ«nĂ« duke punuar me tabela tĂ« tjera, atĂ«herĂ« âtĂ« dhĂ«nat mbetjeâ mbeten, dhe na duhet tĂ« pĂ«rdorim faqe tĂ« tjera pĂ«r tĂ« dhĂ«na tĂ« reja.
Në shembullin tonë, në një moment të caktuar, tabela do të përbëhet nga katër fqinj, por të dhënat aktive në të do të kenë vetëm gjysmën. Si rezultat, kur kërkojmë në tabelë, ne do të lexojmë shumë më tepër të dhëna se sa është e nevojshme.

Edhe nëse VACUUM heq të gjitha versionet e panevojshme të rreshtave tani, situata nuk do të përmirësohet ndjeshëm. Do të kemi hapësirë të lirë në faqe ose madje faqe të tëra për rreshta të rinj, por ne ende do të lexojmë më shumë të dhëna sesa duhet.
Për më tepër, nëse një faqe krejtësisht e zbrazët (e dyta në shembullin tonë) do të ishte në fund të skedarit, atëherë VACUUM do ta kishte prerë atë. Por tani ajo ndodhet në mes, kështu që nuk mund të bëjmë asgjë me të.

Kur numri i këtyre faqeve të zbrazëta ose shumë të shëmtuar bëhet i madh, çka quhet bloat, kjo fillon të ndikojë në performancën.
E gjithë ajo që u përshkrua më lart është mekanika e shfaqjes së bloat në tabela. Në inde ndodh më ose më pak në të njëjtën mënyrë.
A kam unë bloat?
Ka disa mënyra për të përcaktuar nëse keni bloat. Ideja e parë është përdorimi i statistikave të brendshme të Postgres, të cilat përmbajnë informacion të përafërt mbi numrin e rreshtave në tabela, numrin e rreshtave "të gjallë" etj. Në internet mund të gjeni shumë variacione të skenarëve të gatshëm. Ne morëm si bazë nga PostgreSQL Ekspertët, të cilët mund të vlerësojnë bloat-in e tabelave së bashku me toast dhe bloat-in e indekseve btree. Sipas përvojës sonë, pasiguria e tij është rreth 10-20%.
Një tjetër mënyrë është të përdorësh zgjatjen , e cila lejon të shikohet brenda faqeve dhe të marrë një vlerësim si dhe një vlerë të saktë të bloat-it. Por në rastin e fundit, do të duhet të skanohet e gjithë tabela.
Një vlerë të vogël bloat, deri në 20%, e konsiderojmë të pranueshme. Ajo mund të merret si një analog i fillfactor për dhe . Me 50% e lartë mund të fillojnë problemet me performancën.
Mënyrat për të luftuar bloat-in
Në Postgres ka disa mënyra për të luftuar bloat-in "nga kutia", megjithatë ato nuk janë gjithmonë të përshtatshme për të gjithë.
Konfiguroni AUTOVACUUM-in qĂ« bloat-i tĂ« mos ndodhĂ«. Dhe mĂ« saktĂ«sisht, qĂ« tĂ« mbetet nĂ« njĂ« nivel tĂ« pranueshĂ«m pĂ«r ju. Mund tĂ« duket si njĂ« kĂ«shillĂ« âkapiteniâ, por nĂ« realitet nuk Ă«shtĂ« gjithmonĂ« e lehtĂ« tĂ« arrihet. PĂ«r shembull, nĂ«se keni njĂ« zhvillim aktiv me ndryshime tĂ« rregullta nĂ« skemĂ«n e tĂ« dhĂ«nave ose ndodhin disa migrime tĂ« tĂ« dhĂ«nave. Si pasojĂ«, profili juaj i ngarkesĂ«s mund tĂ« ndryshojĂ« shpesh dhe, si rregull, ai Ă«shtĂ« i ndryshĂ«m pĂ«r tabela tĂ« ndryshme. KĂ«shtu, ju nevojitet tĂ« punoni vazhdimisht pak pĂ«rpara dhe tĂ« pĂ«rshtatni AUTOVACUUM pĂ«r profilin qĂ« ndryshon tĂ« secilĂ«s tabelĂ«. Por Ă«shtĂ« e qartĂ« se nuk Ă«shtĂ« e lehtĂ« ta bĂ«ni kĂ«tĂ«.
NjĂ« tjetĂ«r shkak i zakonshĂ«m pĂ«r pĂ«rkatĂ«sinĂ« e AUTOVACUUM pĂ«r tĂ« pĂ«rpunuar tabelat Ă«shtĂ« pranija e transaksioneve tĂ« gjata, tĂ« cilat nuk i lejojnĂ« atij tĂ« pastruar tĂ« dhĂ«nat sepse ato janĂ« tĂ« aksesueshme pĂ«r kĂ«to transaksione. KĂ«tu rekomandimi Ă«shtĂ« gjithashtu i qartĂ« â tĂ« eliminohen transaksionet 'nĂ« pritje' dhe tĂ« minimizohet koha e transaksioneve aktive. Por nĂ«se ngarkesa e aplikacionit tuaj Ă«shtĂ« njĂ« hibrid OLAP dhe OLTP, mund tĂ« keni njĂ«kohĂ«sisht shumĂ« pĂ«rditĂ«sime tĂ« shpeshta dhe kĂ«rkesa tĂ« shkurtra, si dhe operacione tĂ« gjata â pĂ«r shembull, ndĂ«rtimi i ndonjĂ« raporti. NĂ« njĂ« situatĂ« tĂ« tillĂ«, ka kuptim tĂ« mendoni pĂ«r shpĂ«rndarjen e ngarkesĂ«s nĂ« baza tĂ« ndryshme, gjĂ« qĂ« do tĂ« lejojĂ« optimizimin mĂ« tĂ« hollĂ« tĂ« secilĂ«s prej tyre.
NjĂ« shembull tjetĂ«r â edhe nĂ«se profili Ă«shtĂ« homogjen, por DB Ă«shtĂ« nĂ«n njĂ« ngarkesĂ« shumĂ« tĂ« lartĂ«, atĂ«herĂ« edhe AUTOVACUUM mĂ« agresiv mund tĂ« mos pĂ«rballojĂ«, dhe bloat do tĂ« vazhdojĂ« tĂ« shfaqet. ShkĂ«mbimi (vertikal ose horizontal) Ă«shtĂ« zgjidhja e vetme.
Si duhet të veproni në situatën kur AUTOVACUUM e keni konfiguruar, por bloat vazhdon të rritet.
Ekipa VACUUM FULL riorganizojnë përmbajtjen e tabelave dhe indekseve dhe lënë vetëm të dhënat e aktualizuara. Për eliminimin e bloat, ajo funksionon perfekt, por gjatë ekzekutimit të saj merret një bllokim ekskluziv në tabelë (AccessExclusiveLock), i cili nuk lejon të ekzekutohen kërkesat për këtë tabelë, përfshirë edhe select. Nëse mund të përballoni ndalimin e shërbimit tuaj ose një pjese të tij për një kohë (nga dhjetëra minuta deri në disa orë në varësi të madhësisë së DB-së dhe harduerit tuaj), atëherë kjo është alternativa më e mirë. Ne, për fat të keq, nuk arrijmë ta ndjekim VACUUM FULL brenda kohës së planifikuar për mirëmbajtje, prandaj ky opsion nuk na përshtatet.
Ekipa CLUSTER gjithashtu riorganizojnĂ« pĂ«rmbajtjen e tabelave, siç bĂ«n VACUUM FULL, ndĂ«rsa lejon tĂ« specifikoni njĂ« indeks, sipas tĂ« cilit tĂ« dhĂ«nat do tĂ« renditen fizikisht nĂ« disk (por nĂ« tĂ« ardhmen pĂ«r rreshta tĂ« rinj rendi nuk Ă«shtĂ« i garantuar). NĂ« disa raste kjo Ă«shtĂ« njĂ« optimizim i mirĂ« pĂ«r njĂ« sĂ«rĂ« kĂ«rkesash â me leximin e disa regjistrave sipas indeksit. Disavantazhi i komandĂ«s Ă«shtĂ« i njĂ«jtĂ« me atĂ« tĂ« VACUUM FULL â ajo bllokon tabelĂ«n gjatĂ« punĂ«s.
Ekipa REINDEX përngjan me dy të mëparshmet, por kryen rimodelimin e një indeksi të caktuar ose të gjitha indekseve të tabelës. Bllokimet janë pak më të dobëta: ShareLock mbi tabelë (pengon modifikimet, por lejon ekzekutimin e select) dhe AccessExclusiveLock mbi indeksin e rimodeluar (bllokon kërkesat që përdorin këtë indeks). Megjithatë, në versionin e 12-të të Postgres është shtuar parametri , i cili lejon rimodelimin e indeksit pa bllokuar shtimin, ndryshimin ose fshirjen e regjistrimeve në mënyrëparalele.
NĂ« versionet e mĂ«parshme tĂ« Postgres, mund tĂ« arrihet njĂ« rezultat qĂ« Ă«shtĂ« i ngjashĂ«m me REINDEX CONCURRENTLY, pĂ«rmes . Ai lejon krijimin e njĂ« indeksi pa bllokim tĂ« rreptĂ« (ShareUpdateExclusiveLock, i cili nuk pengon kĂ«rkesat paralele), pastaj tĂ« zĂ«vendĂ«sohet indeksi i vjetĂ«r me tĂ« riun dhe tĂ« fshihet indeksi i vjetĂ«r. Kjo lejon elimimin e bloat-it tĂ« indekseve, pa penguar funksionimin e aplikacionit tuaj. ĂshtĂ« e rĂ«ndĂ«sishme tĂ« mbani parasysh se gjatĂ« rimodelimit tĂ« indekseve do tĂ« ketĂ« njĂ« ngarkesĂ« shtesĂ« nĂ« sistemin e diskut.
KĂ«shtu, nĂ«se pĂ«r indekset egzistojnĂ« mĂ«nyra pĂ«r tĂ« eliminuar bloat ânĂ« ngrohjeâ, pĂ«r tabelat kĂ«to nuk ekzistojnĂ«. KĂ«tu hyjnĂ« nĂ« lojĂ« zgjerime tĂ« ndryshme tĂ« jashtme: (mĂ« parĂ« pg_reorg), , dhe tĂ« tjera. NĂ« kĂ«tĂ« artikull nuk do t'i krahasoj dhe do flas vetĂ«m pĂ«r pg_repack, tĂ« cilin, pas disa pĂ«rshtatjeve, e pĂ«rdorim tek ne.
Si funksionon pg_repack

Supozoni se kemi njĂ« tabelĂ« tĂ« zakonshme â me indekse, kufizime dhe, fatkeqĂ«sisht, me bloat. Hapi i parĂ« pg_repack krijon njĂ« tabelĂ« regjistrimi pĂ«r tĂ« ruajtur tĂ« dhĂ«nat mbi tĂ« gjitha ndryshimet gjatĂ« punĂ«s. NjĂ« trigger do tĂ« pĂ«rgjojĂ« kĂ«to ndryshime pĂ«r çdo insert, update dhe delete. Pastaj krijohet njĂ« tabelĂ«, e ngjashme me origjinalen nĂ« strukturĂ«, por pa indekse dhe kufizime, pĂ«r tĂ« mos ngadalĂ«suar procesin e futur tĂ« dhĂ«nash.
Më pas pg_repack transferon të dhënat në tabelën e re nga e vjetra, duke filtruar automatikisht të gjitha rreshtat e pavlefshëm, dhe pastaj krijon indekse për tabelën e re. Gjatë gjithë këtyre operacioneve, në tabelën e regjistrimit akumulohet ndryshime.
Hapi i ardhshëm është të transferoni ndryshimet në tabelën e re. Transferimi kryhet në disa iteracione, dhe kur në tabelën e logut mbeten më pak se 20 regjistrime, pg_repack merr një bllokim të rreptë, transferon të dhënat e fundit dhe zëvendëson tabelën e vjetër me të reja në tabelat sistemike të Postgres. Ky është një moment i vetëm dhe shumë i shkurtër, kur nuk do të jeni në gjendje të punoni me tabelën. Pas kësaj, tabela e vjetër dhe tabela e logut fshihen, duke liruar hapësirë në sistemin e skedarëve. Procesi ka përfunduar.
Teoria duket mjaft mirë, çfarë ndodh në praktikë? E kemi testuar pg_repack pa ngarkesë dhe nën ngarkesë, kemi kontrolluar funksionimin e saj në rast të ndalimit të parakohshëm (thjesht me Ctrl+C). Të gjitha testet rezultuan pozitive.
Shkuam nĂ« prodhim â dhe kĂ«tu gjithçka shkoi ndryshe nga sa priteshim.
Blin i parë në prodhim
Në klasterin e parë morëm një gabim lidhur me shkeljen e kufizimit unik:
$ ./pg_repack -t tablename -o id
INFO: po ripaketon tabelën "tablename"
ERROR: kërkesa dështoi:
ERROR: vlera e çelësit të dyfishtë shkel kufizimin unik "index_16508"
DETAIL: ĂelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.
Ky kyçim kishte emrin auto-gjeneruar index_16508 â e krijoi pg_repack. Nga atributet qĂ« e pĂ«rbĂ«jnĂ«, ne identifikuam "kufizimin" tonĂ«, qĂ« i pĂ«rputhet. Problemi Ă«shtĂ« se kjo nuk Ă«shtĂ« njĂ« kufizim krejtĂ«sisht i zakonshĂ«m, por i vonuar (), dmth verifikimi i tij ndodhi mĂ« vonĂ« se komanda sql, gjĂ« qĂ« çon nĂ« pasoja tĂ« papritura.
Kufizimet e vonuara: përse janë të nevojshme dhe si funksionojnë
Pak teori për kufizimet e vonuara.
Le tĂ« shqyrtojmĂ« njĂ« shembull tĂ« thjeshtĂ«: ne kemi njĂ« tabelĂ« reference automjetesh me dy atribute â emrin dhe rendin e automjetit nĂ« referencĂ«.

create table cars
(
name text constraint pk_cars primary key,
ord integer not null constraint uk_cars unique
);
Supozoni se na duhen tĂ« ndĂ«rronim automjetet e parĂ« dhe tĂ« dytĂ« vendet. Zgjidhja "nĂ« mĂ«nyrĂ« tĂ« drejtpĂ«rdrejtĂ«" â 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 gjatë ekzekutimit të këtij kodi, ne do të marrim një shkelje të pritshme të kufizimit, sepse rendi i vlerave në tabelë është unik:
[23305] ERROR: vlera e çelĂ«sit tĂ« dyfishtĂ« shkel kufizimin unik âuk_carsâ
Detaj: ĂelĂ«si (ord)=(2) ekziston tashmĂ«.
Si tĂ« bĂ«jmĂ« ndryshe? Opsioni i parĂ«: tĂ« shtojmĂ« njĂ« zĂ«vendĂ«sim shtesĂ« pĂ«r rendin, qĂ« me siguri nuk ekziston nĂ« tabelĂ«, pĂ«r shembull â-1â. NĂ« programim, kjo njihet si âkĂ«mbimi i vlerave tĂ« dy variablave pĂ«rmes njĂ« tĂ« tretĂ«â. E vetmja mangĂ«si e kĂ«tij metodi Ă«shtĂ« pĂ«rditĂ«simi shtesĂ«.
Opsioni i dytĂ«: riprojektimi i tabelĂ«s pĂ«r tĂ« pĂ«rdorur njĂ« lloj tĂ« dhĂ«nash me pikĂ« lĂ«vizĂ«se pĂ«r rendin, nĂ« vend tĂ« numrave tĂ« plotĂ«. KĂ«shtu, kur pĂ«rditĂ«sohet njĂ« vlerĂ« nga 1 nĂ« 2.5, regjistrimi i parĂ« automatikisht âdo tĂ« pozicionohetâ mes tĂ« dytit dhe tĂ« tretit. Ky zgjidhje funksionon, por ka dy kufizime. SĂ« pari, nuk do t'ju pĂ«rshtatet nĂ«se vlera pĂ«rdoret diku nĂ« ndĂ«rfaqe. SĂ« dyti, nĂ« varĂ«si tĂ« saktĂ«sisĂ« sĂ« llojit tĂ« dhĂ«nash, do tĂ« keni njĂ« numĂ«r tĂ« kufizuar mundĂ«sish pĂ«r vendosje, deri nĂ« rifreskimin e vlerave tĂ« tĂ« gjithĂ« regjistrave.
Opsioni i tretë: të bëjmë kufizimin të vonuar, në mënyrë që të kontrollohet vetëm në momentin e angazhimit:
create table cars
(
name text constraint pk_cars primary key,
ord integer not null constraint uk_cars unique deferrable initially deferred
);Pasi logjika e kërkesës sonë fillestare garanton se deri në momentin e angazhimit të gjitha vlerat janë unike, ajo do të kryhet me sukses.
Shembulli i diskutuar më lart, sigurisht, është shumë sintetik, por shpreh idenë. Në aplikacionin tonë ne përdorim kufizime të vonuara për të zbatuar logjikën që menaxhon zgjidhjen e konflikteve kur përdoruesit punojnë njëkohësisht me objektet-widget në tablonë e përbashkët. Përdorimi i kufizimeve të tilla na lejon ta thjeshtojmë pak kodin e aplikacionit.
Në përgjithësi, në varësi të tipi të kufizimit në Postgres, ekzistojnë tre nivele granulariteti për verifikimin e tyre: niveli i rreshtit, transaksionit dhe shprehjes.

Burimi:
CHECK dhe NOT NULL gjithmonë kontrollohen në nivelin e rreshtit, për kufizimet e tjera, siç duket nga tabela, ka mundësi të ndryshme. Mund të lexoni më shumë për këtë .
Nëse përmbledhim shkurt, kufizimet e vonuara në disa situata ofrojnë një kod më të lexueshëm dhe më pak komandë. Megjithatë, për këtë paguajmë me një proces më të komplikuar debugimi, pasi momenti i shfaqjes së gabimit dhe momenti kur merrni informacion për të janë të ndara në kohë. Një problem tjetër i mundshëm lidhet me faktin se planifikuesi nuk është gjithmonë në gjendje të ndërrmarë një plan optimal nëse në kërkesë përfshihet një kufizim i vonuar.
Përmirësimi i pg_repack
Kemi kuptuar se çfarë janë kufizimet e vonuara, por si lidhen ato me problemin tonë? Le të rikujtojmë gabimin që ne morëm më parë:
$ ./pg_repack -t tablename -o id
INFO: po ripaketon tabelën "tablename"
ERROR: kërkesa dështoi:
ERROR: vlera e çelësit të dyfishtë shkel kufizimin unik "index_16508"
DETAIL: ĂelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.Ai shfaqet nĂ« momentin e kopjimit tĂ« tĂ« dhĂ«nave nga tabela log nĂ« tabelĂ«n e re. Ky duket i çuditshĂ«m, pasi tĂ« dhĂ«nat nĂ« tabelĂ«n log komitohen sĂ« bashku me tĂ« dhĂ«nat e tabelĂ«s origjinale. NĂ«se ato i pĂ«rmbushin kufizimet e tabelĂ«s origjinale, si mund tĂ« shkelin tĂ« njĂ«jtat kufizime nĂ« tĂ« re?
Siç duket, rrënja e problemit është në hapin paraprak të punës së pg_repack, në të cilin krijohen vetëm indekset, por jo kufizimet: në tabelën e vjetër kishte një kufizim unik, ndërsa në të re është krijuar një indeks unik në vend të tij.

à rëndësishme të theksohet se, nëse kufizimi është i zakonshëm dhe jo i vonuar, atëherë indeksi unik i krijuar në vend të tij është i barabartë me këtë kufizim, pasi kufizimet unike në Postgres zbatohen përmes krijimit të një indeksi unik. Por në rastin e një kufizimi të vonuar, sjellja nuk është e njëjtë, sepse indeksi nuk mund të jetë i vonuar dhe gjithmonë kontrollohet në momentin e ekzekutimit të komandës SQL.
Pra, thelbi i problemit qëndron në 'vonimin' e verifikimit: në tabelën fillestare ndodh në momentin e komitimit, ndërsa në të re njoftohet në momentin e ekzekutimit të komandës SQL. Kështu që ne duhet ta bëjmë që verifikimet të kryhen në mënyrë të njëjtë në të dy rastet: ose gjithmonë të vonuara, ose gjithmonë menjëherë.
Kështu, cilat ide kemi pasur.
Krijo një indeks, të ngjashëm me të vonuar.
Ideja e parë është të kryhen të dyja verifikimet në modin e menjëhershëm. Kjo mund të krijojë disa false positive përjashtime të kufizimit, por nëse ato janë të pakta, nuk duhet ta ndikojnë punën e përdoruesve, pasi për ta, këto konflikte janë një situatë normale. Ato ndodhin, për shembull, kur dy përdorues fillojnë së bashku të redaktojnë të njëjtin widget, dhe klienti i përdoruesit të dytë nuk arrin të marrë informacionin se widgeti është bllokuar për redaktim nga përdoruesi i parë. Në këtë situatë, serveri i përgjigjet përdoruesit të dytë me një refuzim, dhe klienti i tij anulon ndryshimet dhe bllokon widgetin. Pak më vonë, kur përdoruesi i parë përfundon redaktimin, përdoruesi i dytë do të marrë informacionin se widgeti nuk është më bllokuar dhe do të mund të përsërisë veprimin e tij.

Për të siguruar që verifikimet të jenë gjithmonë në mod të menjëhershëm, ne kemi krijuar një indeks të ri, të ngjashëm me kufizimin origjinal të vonuar:
CREATE UNIQUE INDEX CONCURRENTLY uk_tablename__immediate ON tablename (id, index);
-- run pg_repack
DROP INDEX CONCURRENTLY uk_tablename__immediate;Në mjedisin e testit morëm vetëm disa gabime të pritura. Sukses! Kemi nisur përsëri pg_repack në prodhim dhe kemi marrë 5 gabime në klusterin e parë për një orë punë. Ky është një rezultat i pranueshëm. Megjithatë, në klusterin e dytë, numri i gabimeve u rrit ndjeshëm dhe na duhej të ndalonim pg_repack.
Pse ndodhi kĂ«shtu? Shkaku i gabimeve varet nga numri i pĂ«rdoruesve qĂ« punojnĂ« njĂ«kohĂ«sisht me tĂ« njĂ«jtin widget. Duke parĂ«, nĂ« atĂ« moment, tĂ« dhĂ«nat qĂ« ruanin nĂ« klusterin e parĂ« kishin shumĂ« mĂ« pak ndryshime konkurruese se nĂ« tĂ« tjerĂ«t, dmth. thjesht na ândodhi fatâ.
Ideja nuk funksionoi. NĂ« atĂ« moment ne panĂ« dy opsione tĂ« tjera: tĂ« rishkruanim kodin tonĂ« aplikativ pĂ«r tĂ« hequr dorĂ« nga kufizimet e vonuara, ose tĂ« âmĂ«sonimâ pg_repack tĂ« punonte me to. ZgjedhĂ«m opsionin e dytĂ«.
Të zëvendësojmë indeksat në tavolinën e re me kufizimet e vonuara nga tavolina origjinale.
QĂ«llimi i pĂ«rmirĂ«simit ishte i qartĂ« â nĂ«se tavolina origjinale ka njĂ« kufizim tĂ« vonuar, atĂ«herĂ« pĂ«r tĂ« rejĂ«n duhet tĂ« krijohet njĂ« kufizim i tillĂ«, e jo njĂ« indeks.
Për të verifikuar ndryshimet tona, ne shkruam një test të thjeshtë:
- tabela me kufizim të vonuar dhe një shënim;
- ndërhyjmë në cikël për të futur të dhëna që shkaktojnë konflikt me shënimin ekzistues;
- bĂ«jmĂ« update â tĂ« dhĂ«nat tashmĂ« nuk shkaktojnĂ« konflikt;
- komitojmë 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 gjithmonë binte në insertin e parë, versioni i përmirësuar funksionoi pa probleme. Shkëlqyer.
Shkëlqejnë për prodhim dhe përsëri marrim një gabim në të njëjtën fazë të kopjimit të të dhënave nga tabela e logut në të re:
$ ./pg_repack -t tablename -o id
INFO: po ripaketon tabelën "tablename"
ERROR: kërkesa dështoi:
ERROR: vlera e çelësit të dyfishtë shkel kufizimin unik "index_16508"
DETAIL: ĂelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.Situata klasike: nĂ« ambientet e testimit gjithçka funksionon, ndĂ«rsa nĂ« prodhim â jo?!
APPLY_COUNT dhe kufiri i dy batch-eve
Kemi filluar të analizojmë kodin saktësisht rresht pas rreshti dhe zbuluam një moment të rëndësishëm: transferimi i të dhënave nga tabela e logut në të re ndodh me batçe, konstanta APPLY_COUNT tregon madhësinë e batch-it:
for (;;)
{
num = apply_log(connection, table, APPLY_COUNT);
if (num > MIN_TUPLES_BEFORE_SWITCH)
continue; /* mund të ketë ende disa tuple, përsëritni. */
...
}Problemi Ă«shtĂ« se tĂ« dhĂ«nat e transaksionit origjinal, ku disa operacione mund tĂ« shkelin potencialisht kufirin, gjatĂ« transferimit mund tĂ« bien nĂ« kufirin e dy grupeve â gjysma e komandave do tĂ« komitohen nĂ« grupin e parĂ«, ndĂ«rsa gjysma tjetĂ«r nĂ« tĂ« dytin. Dhe kĂ«tu si tĂ« vijĂ«: nĂ«se komandat nĂ« grupin e parĂ« nuk shkelin asgjĂ«, atĂ«herĂ« Ă«shtĂ« nĂ« rregull, por nĂ«se shkelin â ndodh njĂ« gabim.
APPLY_COUNT Ă«shtĂ« 1000 tĂ« dhĂ«na, qĂ« shpjegon pse testet tona kaluan me sukses â ata nuk mbulonin rastin âe kufirit tĂ« grupeveâ. Ne pĂ«rdorĂ«m dy komanda â insert dhe update, kĂ«shtu qĂ« gjithsej 500 transaksione me dy komanda gjithmonĂ« ishin vendosur nĂ« grup dhe ne nuk kemi hasur probleme. Pas shtimit tĂ« update tĂ« dytĂ«, pĂ«rmirĂ«simi ynĂ« nuk funksionoi mĂ«:
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; -- një përmirësim më tepër
COMMIT;
END;
END LOOP;Pra, detyra e ardhshme është të sigurohet që të dhënat nga tabela origjinale, të cilat u ndryshuan në një transaksion, të kalojnë në tabelën e re gjithashtu brenda një transaksioni.
Heqja dorë nga batching
SĂ«rish kishim dy mundĂ«si zgjidhjeje. E para: le tĂ« heqim dorĂ« plotĂ«sisht nga ndarja nĂ« blloqe dhe tĂ« bĂ«jmĂ« transferimin e tĂ« dhĂ«nave me njĂ« transaksion. NĂ« favor tĂ« kĂ«saj zgjidhjeje fliste thjeshtĂ«sia â ndryshimet e kĂ«rkuara nĂ« kod janĂ« minimale (pĂ«rveç kĂ«saj, nĂ« versionet mĂ« tĂ« vjetra, pg_reorg funksiononte pikĂ«risht kĂ«shtu). Por ka njĂ« problem â krijojmĂ« njĂ« transaksion tĂ« gjatĂ«, dhe kjo, siç u tha mĂ« parĂ«, Ă«shtĂ« njĂ« kĂ«rcĂ«nim pĂ«r shfaqjen e bloat-it tĂ« ri.
Zgjidhja e dytĂ« Ă«shtĂ« mĂ« e komplikuar, por ndoshta mĂ« e saktĂ«: tĂ« krijojmĂ« njĂ« kolonĂ« nĂ« tabelĂ«n e log-ut me identifikatorin e transaksionit qĂ« shtoi tĂ« dhĂ«nat nĂ« tabelĂ«. KĂ«shtu, gjatĂ« kopjimit tĂ« tĂ« dhĂ«nave, ne mund t'i grupojmĂ« ato sipas kĂ«tij atributi dhe tĂ« garantojmĂ« qĂ« ndryshimet e lidhura do tĂ« transferohen sĂ« bashku. Batch-i do tĂ« formohet nga disa transaksione (ose njĂ« tĂ« madhe) dhe madhĂ«sia e tij do tĂ« varihojĂ« nĂ« varĂ«si tĂ« sasisĂ« sĂ« tĂ« dhĂ«nave qĂ« janĂ« modifikuar nĂ« kĂ«to transaksione. ĂshtĂ« e rĂ«ndĂ«sishme tĂ« theksohet se, pĂ«r shkak se tĂ« dhĂ«nat e transaksioneve tĂ« ndryshme hynĂ« nĂ« tabelĂ«n e log-ut nĂ« rend tĂ« rastit, nuk do tĂ« mund tĂ« lexohet mĂ« sekuencialisht, siç ishte mĂ« parĂ«. seqscan pĂ«r çdo kĂ«rkesĂ« me filtrimin sipas tx_id Ă«shtĂ« shumĂ« e shtrenjtĂ«, nevojitet njĂ« indeks, por ai do ta ngadalĂ«sojĂ« gjithashtu metodĂ«n pĂ«r shkak tĂ« kostove shtesĂ« pĂ«r pĂ«rditĂ«simin e tij. NĂ« pĂ«rgjithĂ«si, si gjithmonĂ«, Ă«shtĂ« e nevojshme tĂ« sakrifikohet diçka.
Pra ndaj, ne vendosĂ«m tĂ« fillojmĂ« me opsionin e parĂ«, si mĂ« tĂ« thjeshtĂ«. SĂ« pari, duhej tĂ« kuptonim nĂ«se njĂ« transaksion i gjatĂ« do tĂ« ishte njĂ« problem real. Duke qenĂ« se transferimi kryesor i tĂ« dhĂ«nave nga tabela e vjetĂ«r nĂ« atĂ« tĂ« re ndodh gjithashtu nĂ« njĂ« transaksion tĂ« gjatĂ«, çështja u transformua nĂ« âsa shumĂ« do ta zgjasim kĂ«tĂ« transaksion?â KohĂ«zgjatja e transaksionit tĂ« parĂ« varet kryesisht nga madhĂ«sia e tabelĂ«s. KohĂ«zgjatja e re varet nga sa shumĂ« ndryshime do tĂ« grumbullohen nĂ« tabelĂ« gjatĂ« kohĂ«s sĂ« transferimit tĂ« tĂ« dhĂ«nave, pra nga intensiteti i ngarkesĂ«s. Prova e pg_repack u realizua gjatĂ« ngarkesĂ«s minimale nĂ« shĂ«rbim, dhe volumi i ndryshimeve ishte shumĂ« i vogĂ«l nĂ« krahasim me volumin fillestar tĂ« tabelĂ«s. Ne vendosĂ«m se mund ta injorojmĂ« kohĂ«n e transaksionit tĂ« ri (pĂ«r krahasim mesatarisht Ă«shtĂ« 1 orĂ« dhe 2-3 minuta).
Eksperimentet ishin pozitive. Aktivizimi në prodhim gjithashtu. Për ilustër, një imazh me madhësinë e një nga bazat pas kryerjes:

Nëse ky zgjidhje na përshtatet plotësisht, nuk vendosëm ta provojmë një të dytë, por po shqyrtojmë mundësinë e diskutimit me zhvilluesit e zgjatimeve. Ndryshimi ynë aktual, fatkeqësisht, ende nuk është gati për botim, pasi zgjidhëm vetëm problemin me kufizimet e veçanta të vonuara, dhe për një patch të plotë nevojitet të bëhet mbështetje për lloje të tjera. Shpresojmë se do të mund ta realizojmë këtë në të ardhmen.
Ndoshta keni pyetur se përse u angazhuam në këtë histori me modifikimin e pg_repack në vend që të përdornim, për shembull, alternativa të tjera? Në një moment, ne gjithashtu mendojmë për këtë, por përvoja pozitive e përdorimit të tij më parë, në tabela pa kufizime të vonuara, na motivoi të provonim të kuptonim thelbin e problemit dhe ta zgjidhim atë. Për më tepër, përdorimi i zgjidhjeve të tjera gjithashtu kërkon kohë për të kryer teste, prandaj vendosëm që fillimisht të përpiqemi të zgjidhim problemin në të, dhe nëse e kuptojmë se nuk do të mund ta bëjmë këtë brenda një kohe të arsyeshme, atëherë do të fillojmë të shqyrtojmë alternativat.
Përfundimet
ĂfarĂ« mund tĂ« rekomandojmĂ« mbi pĂ«rvojĂ«n tonĂ« personale:
- Monitoroni bloat-in tuaj. Në bazë të të dhënave të monitorimit, do të kuptoni sa mirë është konfigurimi i autovacuum.
- Konfiguroni AUTOVACUUM për të mbajtur bloat-in në nivele të pranueshme.
- Nëse bloat-i vazhdon të rritet dhe nuk mund ta luftoni me mjetet "nga kutia", mos u friksoni nga përdorimi i zgjerimeve të jashtme. E rëndësishme është të testoni gjithçka mirë.
- Mos u friksoni tĂ« pĂ«rshtatni zgjidhjet e jashtme sipas nevojave tuaja â ndonjĂ«herĂ« kjo mund tĂ« jetĂ« mĂ« efektive dhe madje mĂ« e thjeshtĂ« se sa tĂ« ndryshoni kodin tuaj.
Burimi: habr.com
