Postgres: fryrje, pg_repack dhe kufizime të shtyra

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Efekti i fryrjes sĂ« tabelave dhe indekseve (bloat) Ă«shtĂ« i njohur gjerĂ«sisht dhe Ă«shtĂ« i pranishĂ«m jo vetĂ«m nĂ« Postgres. EkzistojnĂ« mĂ«nyra pĂ«r ta luftuar atĂ« “nga kutia” siç janĂ« VACUUM FULL ose CLUSTER, por ato bllokojnĂ« tabelat gjatĂ« funksionimit dhe prandaj nuk mund tĂ« pĂ«rdoren gjithmonĂ«.

Ky artikull do të ofrojë pak teori se si ndodh bloat, si mund ta luftojmë atë, për pengesat e shtyrë dhe për problemet që ato sjellin në përdorimin e zgjerimit pg_repack.

Ky artikull është shkruar mbi baza prezantimit tim në PgConf.Russia 2020.

Luaj videon

Pse ndodh bloat

Në thelb, Postgres mbështetet në një model shumëversional (MVCC). Thelbi i saj është se secila rresht në tabelë mund të ketë disa versione, përkatësisht transaksionet shohin jo më shumë se një nga këto versionet, por jo domosdoshmërisht të njëjtën. Kjo lejon që disa transaksione të punojnë në të njëjtën kohë dhe të mos ndikojnë shumë në njëra-tjetrën.

E qartë është se të gjitha këto versione duhet të ruhen. Postgres punon me kujtesën sipas faqeve dhe faqja është minimumi i të dhënave që mund të lexohen nga disku apo të shkruhen. Le të shqyrtojmë një shembull të vogël për të kuptuar si ndodh kjo.

Supozoni se kemi një tabelë në të cilën shtuam disa regjistra. Në faqen e parë të skedarit ku ruhet tabela, u shfaqën të dhëna të reja. Këto janë versione të gjalla të rreshtave, të cilat janë të disponueshme për transaksionet e tjera pas komitimimit (për thjeshtësi le të supozojmë se niveli i izolimit është Read Committed).

Postgres: fryrje, pg_repack dhe kufizime të shtyra

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

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Hap pas hapi, duke pĂ«rditĂ«suar dhe fshirĂ« versionet e rreshtave, morĂ«m njĂ« faqe ku afĂ«rsisht gjysma e tĂ« dhĂ«nave janĂ« “mbetje”. KĂ«to tĂ« dhĂ«na nuk i shohin asnjĂ« transaksion.

Postgres: fryrje, pg_repack dhe kufizime të shtyra

NĂ« Postgres ekziston njĂ« mekanizĂ«m VACUUM, i cili pastron versionet e pavlefshme dhe liçon hapĂ«sirĂ« pĂ«r tĂ« dhĂ«na tĂ« reja. Por nĂ«se ai nuk Ă«shtĂ« konfigururar mjaft agjresivisht ose Ă«shtĂ« i angazhuar me punĂ« nĂ« tabela tĂ« tjera, atĂ«herĂ« “tĂ« dhĂ«nat mbeturina” mbeten dhe na detyrojnĂ« tĂ« pĂ«rdorim faqe tĂ« tjera pĂ«r tĂ« dhĂ«na tĂ« reja.

Kështu, në shembullin tonë, në një moment të caktuar, tabela do të përbëhet nga katër faqe, por të dhëna të gjalla do të kenë vetëm gjysmën e saj. Si rezultat, kur i qasemi tabelës, ne do të lexojmë shumë më tepër të dhëna sesa nevojitet.

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Edhe nëse tani VACUUM heq të gjitha versionet joaktuale të rreshtave, situata nuk do të përmirësohet drasti. Ne do të kemi hapësirë të lirë në faqe ose madje faqe të tëra për rreshta të rinj, por ne do të vazhdojmë të lexojmë më shumë të dhëna se sa është e nevojshme.
Për më tepër, 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 ta priste atë. Por tani ajo ndodhet në mes, kështu që nuk mund të bëhet asgjë me të.

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Kur numri i faqeve të tilla të zbrazëta ose shumë të zhvishtuara bëhet i madh, çka quhet bloat, kjo fillon të ndikojë në performancën.

E gjitha e përshkruar më sipër është mekanika e shfaqjes së bloat në tabela. Në indekra, ndodh në një mënyrë të ngjashme.

A kam 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ë informacione të përafërta për numrin e rreshtave në tabela, numrin e rreshtave 'të gjallë' dhe të tjera. Në internet mund të gjeni një mori variantësh të skripteve të gatshme. Ne e morëm si bazë skript nga PostgreSQL Experts, e cila mund të vlerësojë bloat-in e tabelave së bashku me toast dhe bloat-in e indekseve btree. Nga përvoja jonë, gabimi i saj është 10-20%.

Një mënyrë tjetër është përdorimi i zgjerimit pgstattuple, i cili lejon të shohim brenda faqeve dhe të marrim vlerësimet si të përafruara ashtu edhe të sakta për bloat. Por në rastin e dytë, do të duhet të skanojmë të gjithë tabelën.

Një vlerë e vogël e bloat, deri në 20%, ne e konsiderojmë të pranueshme. Mund të merren si analogjia e fillfactor për tabelat dhe indekset. Në 50% ose më shumë 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ë gjitha raste.

Konfiguroni AUTOVACUUM, që bloat-i të mos shfaqetDhe nëse jemi më të saktë, për ta mbajtur atë në një nivel të pranueshëm për ju. Duket si një këshillë "kapiteni", por në realitet nuk është gjithmonë e lehtë ta arrish. Për shembull, nëse jeni duke zhvilluar aktivisht me ndryshime të rregullta të schemës së të dhënave ose ndodhin disa migrime të të dhënave. Si pasojë, profili juaj i ngarkesës mund të ndryshojë shpesh dhe, zakonisht, ai është i ndryshëm për tabela të ndryshme. Kjo do të thotë se duhet të punoni vazhdimisht përpara dhe të përshtatni AUTOVACUUM për profilin e ndryshueshëm të secilës tabelë. Por është e qartë që ta bësh këtë nuk është e lehtë.

Një tjetër shkak i zakonshëm pse AUTOVACUUM nuk arrin të përpunojë tabelat është prania e transaksioneve të gjata, të cilat nuk e lejojnë atë të pastroni të dhënat sepse ato janë të aksesueshme për këto transaksione. Rekomandimi këtu gjithashtu është i qartë - të shkurtoni transaksionet "në pritje" dhe të minimizoni kohën e transaksioneve aktive. Por nëse ngarkesa e aplikacionit tuaj është një hibrid OLAP dhe OLTP, atëherë ju 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 një raporti të caktuar. Në një situatë të tillë, është e mençur të mendoni për shpërndarjen e ngarkesës në baza të ndryshme, çka do të lejojë optimizimin më të hollë të secilës prej tyre.

Një tjetër shembull - edhe nëse profili është homogjen, por DB është nën një ngarkesë shumë të lartë, atëherë madje edhe AUTOVACUUM më agresiv nuk mund të përballojë, dhe bloati do të shfaqet. Shkallëzimi (vertikalisht ose horizontalisht) është zgjidhja e vetme.

Si të veproni kur AUTOVACUUM e keni konfiguruar, por bloat vazhdon të rritet.

Ekipa VACUUM FULL ristrukturon përmbajtjen e tabelave dhe indekseve dhe lë vetëm të dhënat aktuale në to. Për të eliminuar bloat, ajo punon në mënyrë ideale, por gjatë ekzekutimit të saj, merret një bllokim ekskluziv mbi tabelën (AccessExclusiveLock), i cili nuk lejon ekzekutimin e kërkesave për këtë tabelë, madje as të zgjedhurat. Nëse mund të lejoni një ndalesë të shërbimit tuaj ose të 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ë ky opsion është më i miri. Ne, fatkeqësisht, nuk arrijmë të ekzekutojmë VACUUM FULL brenda kohës së planifikuar për mirëmbajtje, kështu që ky opsion nuk na përshtatet.

Ekipa CLUSTER po ashtu rindĂ«rton pĂ«rmbajtjen e tabelave, siç bĂ«n VACUUM FULL, duke lejuar gjithashtu tĂ« specifikohet indeksi sipas tĂ« cilit tĂ« dhĂ«nat do tĂ« renditen fizikisht nĂ« disk (por nĂ« tĂ« ardhmen pĂ«r rreshtat e rinj renditja nuk garanton). NĂ« disa situata, kjo Ă«shtĂ« njĂ« optimizim i mirĂ« pĂ«r njĂ« sĂ«rĂ« kĂ«rkimesh – me leximin e disa regjistrave pĂ«rmes indeksit. Disavantazhi i komandĂ«s Ă«shtĂ« i njĂ«jtĂ« me atĂ« tĂ« VACUUM FULL – ajo bllokon tabelĂ«n gjatĂ« punĂ«s.

Ekipa REINDEX është e ngjashme me dy të mëparshmet, por kryen rindërtimin e një indeksi të caktuar ose të gjithë indekseve të tabelës. Bllokimet janë pak më të dobëta: ShareLock në tabelë (pengon modifikimet, por lejon ekzekutimin e select) dhe AccessExclusiveLock në indekson e rindërtuar (bllokon kërkesat që përdorin këtë indeks). Megjithatë, në versionin 12 të Postgres u shfaq një parameter CONCURRENTLY, i cili lejon rindërtimin e indeksit, pa bllokuar shtimin, ndryshimin ose fshirjen e regjistrave paralelisht.

NĂ« versionet mĂ« tĂ« hershme tĂ« Postgres, mund tĂ« arrihet njĂ« rezultat, i ngjashĂ«m me REINDEX CONCURRENTLY, pĂ«rmes CREATE INDEX CONCURRENTLY. Ai lejon krijimin e njĂ« indeksi pa bllokim tĂ« rreptĂ« (ShareUpdateExclusiveLock, i cili nuk pengon kĂ«rkesat paralel), pastaj zĂ«vendĂ«simin e indeksit tĂ« vjetĂ«r me tĂ« riun dhe fshirjen e indeksit tĂ« vjetĂ«r. Kjo lejon eliminimin e bloat tĂ« indekseve, pa penguar punĂ«n e aplikacionit tuaj. ËshtĂ« e rĂ«ndĂ«sishme tĂ« kihet parasysh se gjatĂ« rindĂ«rtimit tĂ« indekseve do tĂ« ketĂ« njĂ« ngarkesĂ« tĂ« shtuar nĂ« sistemin e disqeve.

Pra, nëse për indekset ekzistojnë mënyra për të eliminuar bloat "në ngrohje", për tabelat nuk ka të tilla. Këtu hyjnë në lojë disa zgjerime të jashtme: pg_repack (më parë pg_reorg), pgcompact, pgcompacttable dhe të tjerë. Në kuadër të kësaj artikulli, nuk do t'i krahasoj dhe do të flas vetëm për pg_repack, të cilin pas disa përmirësimesh e përdorim në kompaninë tonë.

Si funksionon pg_repack

Postgres: fryrje, pg_repack dhe kufizime të shtyra
Supozoni se kemi njĂ« tabelĂ« krejtĂ«sisht normale – me indekse, kufizime dhe, fatkeqĂ«sisht, me bloat. Hapi i parĂ« qĂ« merr pg_repack Ă«shtĂ« tĂ« krijojĂ« njĂ« tabelĂ« log, pĂ«r tĂ« mbajtur tĂ« dhĂ«nat pĂ«r tĂ« gjitha ndryshimet gjatĂ« punĂ«s. NjĂ« trigger do tĂ« replikohet kĂ«to ndryshime nĂ« çdo insert, update dhe delete. MĂ« pas krijohet njĂ« tabelĂ«, e ngjashme me origjinalen nĂ« strukturĂ«, por pa indekse dhe kufizime, pĂ«r tĂ« mos ngadalĂ«suar procesin e shtimit tĂ« tĂ« dhĂ«nave.

Pastaj pg_repack transferon në një tavolinë të re të dhënat nga e vjetra, duke filtruar automatikisht të gjitha rreshtat e papërshtatshëm, dhe më pas krijon indekset për tavolinën e re. Gjatë përfundimit të këtyre operacioneve, ndryshimet grumbullohen në tabelën e log.

Hapi qëndron në transferimin e ndryshimeve në tavolinën e re. Transferimi kryhet në disa iteracione dhe kur në tabelën e log mbeten më pak se 20 regjistra, pg_repack merr një bllokim të rreptë, transferon të dhënat e fundit dhe zëvendëson tavolinën e vjetër me atë të re në tabelat sistemike të Postgres. Ky është momenti i vetëm dhe shumë i shkurtër kur nuk do të mund të punoni me tavolinën. Pas kësaj, tavolina e vjetër dhe tabela me logjet fshihen dhe në sistemin e skedarëve lirohet hapësirë. Procesi ka përfunduar.

Në teori gjithçka duket e shkëlqyer, çfarë ndodh në praktikë? Ne e testuam pg_repack pa ngarkesë dhe nën ngarkesë, kontrolluam funksionimin e saj në rast të ndalimit të parakohshëm (thjesht përmes Ctrl+C). Të gjitha testet ishin pozitive.

Shkuam në prodhim - dhe këtu gjithçka shkoi ndryshe nga sa prisnim.

Blin i parë në prodhim

Në klasën e parë morëm një gabim për shkelje të kufizimit unik:

$ ./pg_repack -t tablename -o id
INFO: po ribëhet tavolina "tablename"
ERROR: kërkesa dështoi: 
    ERROR: vlera e dyfishuar e çelësit shkel kufizimin unik "index_16508"
DETALJ:  ÇelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.

Ky kufizim kishte njĂ« emĂ«r tĂ« gjeneruar automatikisht index_16508 – e krijoi pg_repack. Sipas atributeve qĂ« pĂ«rbĂ«jnĂ« kĂ«tĂ« kufizim, ne e identifikuam "kufizimin tonĂ«" qĂ« i pĂ«rgjigjej. Problemi ishte se ky nuk ishte njĂ« kufizim tĂ« zakonshĂ«m, por njĂ« i vonuar (deferred constraint), domethĂ«nĂ«, kontrolli i tij kryhet pas komandĂ«s sql, gjĂ« qĂ« çon nĂ« pasoja tĂ« papritura.

Kufizimet e vonuara: përse janë të nevojshme dhe si funksionojnë

Pak teori rreth kufizimeve të vonuara.
Le tĂ« shqyrtojmĂ« njĂ« shembull tĂ« thjeshtĂ«: ne kemi njĂ« tabelĂ« referimi pĂ«r automjete me dy atribute – emrin dhe rendin e automjetit nĂ« referencĂ«.
Postgres: fryrje, pg_repack dhe kufizime të shtyra

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



Supozoni se na nevojitet tĂ« kĂ«mbim automobilin e parĂ« dhe tĂ« dytĂ«. Zgjidhja "e drejtĂ«" – tĂ« pĂ«rditĂ«sojmĂ« vlerĂ«n e parĂ« me atĂ« tĂ« dytĂ« dhe tĂ« dytĂ«n me atĂ« tĂ« parĂ«:

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

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

[23305] ERROR: vlera e kyçit tĂ« pĂ«rsĂ«ritur shkel kufizimin unik “uk_cars”
Detaj: Kyçi (ord)=(2) ekziston tashmë.

Si ta bĂ«jmĂ« ndryshe? Opsioni i parĂ«: shtoni njĂ« zĂ«vendĂ«sim tĂ« shtesave me njĂ« vlerĂ« renditje qĂ« garanton qĂ« nuk ekziston nĂ« tabelĂ«, pĂ«r shembull “-1”. NĂ« programim kjo quhet “ndĂ«rrimi i vlerave tĂ« dy variablave pĂ«rmes njĂ« tĂ« tretĂ«â€. E vetmja disavantazh i kĂ«tij metodi Ă«shtĂ« pĂ«rditĂ«simi shtesĂ«.

Opsioni i dytĂ«: riprojektimi i tabelĂ«s pĂ«r tĂ« pĂ«rdorur njĂ« tip tĂ« dhĂ«nash me pikĂ« float pĂ«r vlerĂ«n e renditjes nĂ« vend tĂ« numrave tĂ« plotĂ«. KĂ«shtu, kur pĂ«rditĂ«soni vlerĂ«n nga 1, pĂ«r shembull, nĂ« 2.5, regjistrimi i parĂ« automatikisht “qĂ«ndron” ndĂ«rmjet tĂ« dytit dhe tĂ« tretit. Ky zgjidhje Ă«shtĂ« funksionale, por ka dy kufizime. SĂ« pari, kjo nuk do tĂ« jetĂ« e pĂ«rshtatshme pĂ«r ju nĂ«se vlera pĂ«rdoret diku nĂ« ndĂ«rfaqe. SĂ« dyti, nĂ« varĂ«si tĂ« saktĂ«sisĂ« sĂ« tipit tĂ« tĂ« dhĂ«nave, do tĂ« keni njĂ« numĂ«r tĂ« kufizuar mundĂ«sish pĂ«r t’u futur pĂ«rpara se tĂ« bĂ«ni ricakullimin e tĂ« gjitha vlerave tĂ« regjistrimeve.

Opsioni i tretë: bëni kufizimin të vonuar, që do 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
);

Pavarësisht se logjika e kërkesës sonë fillestare garanton që në momentin e angazhimit të gjitha vlerat janë unike, ajo do të ekzekutohet me sukses.

Shembulli i trajtuar më sipër, natyrisht, është shumë sintetik, por ideja qartësohet. Në aplikacionin tonë ne përdorim kufizime të vonuara për të implementuar logjikën që ndihmon në zgjidhjen e konflikteve ndërkohë që përdoruesit punojnë paralelisht me objekte të përbashkëta-widget në tabelën. Përdorimi i këtyre kufizimeve na lejon të bëjmë kodin aplikativ pak më të thjeshtë.

Në përgjithësi, në varësi të tipit të kufizimit në Postgres ekzistojnë tre nivele granulariteti për kontrollin e tyre: niveli i rreshtit, transaksioni dhe shprehja.
Postgres: fryrje, pg_repack dhe kufizime të shtyra
Burimi: begriffs

CHECK dhe NOT NULL kontrollohen gjithmonë në nivelin e rreshtit, për kufizimet e tjera, siç shihet nga tabela, ka mundësi të ndryshme. Më shumë mund të lexoni këtu.

Nëse e përmbledhim shkurt, kufizimet e vonuara në disa situata japin një kod më të lehtë për t'u lexuar dhe një numër më të vogël komandash. Megjithatë, për këtë duhet të paguajmë me komplikimin e procesit të debagimit, pasi momenti i shfaqjes së gabimit dhe momenti kur e zbuloni atë janë të shpërndara në kohë. Një problem tjetër i mundshëm lidhet me faktin se planifikuesi nuk mund të ndërtojë gjithmonë 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ë morëm më parë:

$ ./pg_repack -t tablename -o id
INFO: po ribëhet tavolina "tablename"
ERROR: kërkesa dështoi: 
    ERROR: vlera e dyfishuar e çelësit shkel kufizimin unik "index_16508"
DETALJ:  ÇelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.

Ai ndodh në momentin e kopjimit të të dhënave nga tabela e logëve në një tabelë të re. Kjo duket e çuditshme, pasi të dhënat në tabelën e logëve komitohen së bashku me të dhënat e tabelës origjinale. Nëse ato përmbushin kufizimet e tabelës origjinale, si mund të shkelin të njëjtat kufizime në të re?

Siç duket, rrënjët e problemit qëndrojnë në hapin e mëparshëm të funksionimit të pg_repack, ku krijohen vetëm indeksat, por jo kufizimet: në tabelën e vjetër kishte një kufizim unik, ndërsa në të reën është krijuar një indeks unik në vend të tij.

Postgres: fryrje, pg_repack dhe kufizime të shtyra

ËshtĂ« e rĂ«ndĂ«sishme tĂ« theksohet se nĂ«se kufizimi Ă«shtĂ« i zakonshĂ«m dhe jo i vonuar, indeksi unik i krijuar nĂ« vend tĂ« tij Ă«shtĂ« i barabartĂ« me kĂ«tĂ« kufizim, pasi kufizimet unike nĂ« Postgres realizohen pĂ«rmes krijimit tĂ« njĂ« indeksi unik. Por nĂ« rastin e kufizimeve tĂ« vonuara, 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Ă« "vonueshmĂ«rinĂ«" e kontrollit: nĂ« tabelĂ«n origjinale ajo ndodh nĂ« momentin e komitimit, ndĂ«rsa nĂ« tĂ« reĂ«n – nĂ« momentin e ekzekutimit tĂ« komandĂ«s SQL. Pra, na nevojitet tĂ« sigurojmĂ« qĂ« kontrollet tĂ« kryhen nĂ« mĂ«nyrĂ« tĂ« njĂ«jtĂ« nĂ« tĂ« dy rastet: ose gjithmonĂ« tĂ« vonuara, ose gjithmonĂ« menjĂ«herĂ«.

Pra, cilat ide kemi pasur.

Të krijojmë një indeks, të ngjashëm me i vonuar

Ideja e parë është të kryhen të dy verifikimet në mënyrë të menjëhershme. Kjo mund të sjellë disa false positive në kufizimin e mundësive, por nëse ato janë të pakta, nuk duhet të ndikonin në punën e përdoruesve, pasi për ta këto konflikte janë një situatë normale. Ndodhin, për shembull, kur dy përdorues fillojnë të redaktojnë një widget të njëjtë në të njëjtën kohë, dhe klienti i përdoruesit të dytë nuk arrin të marrë informacion se widget-i tashmë është bllokuar për redaktim nga përdoruesi i parë. Në një situatë të tillë, serveri i përgjigjet përdoruesit të dytë me një refuzim, dhe klienti i tij rikthen ndryshimet dhe bllokon widget-in. Pak më vonë, kur përdoruesi i parë përfundon redaktimin, i dyti do të marrë informacionin që widget-i nuk është më i bllokuar dhe do të mund të ripërsërisë veprimin e tij.

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Për të garantuar që verifikimet të jenë gjithmonë në regjim 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ë ambientin e testimit morëm vetëm disa gabime të pritura. Sukces! Përsëri e nisëm pg_repack në prodhim dhe morëm 5 gabime në klasterin e parë për një orë punë. Ky është një rezultat i pranueshëm. Megjithatë, në klasterin e dytë numri i gabimeve u rrit shumë dhe na u detyruam të ndalnim pg_repack.

Pse ndodhi kështu? Probabiliteti i shfaqjes së gabimit varet nga numri i përdoruesve që punojnë njëkohësisht me widget-et e njëjta. Siç duket, në atë moment, të dhënat e ruajtura në klasterin e parë kishin shumë më pak ndryshime konkurruese se në të tjerët, dmth. thjesht kemi pasur 'fat'.

Ideja nuk funksionoi. Në atë moment, ne pamë dy variante të tjera zgjidhjeje: të rishkruhej kodi ynë aplikativ për të braktisur kufizimet e vonuara, ose të 'mësonim' pg_repack të punonte me to. Ne zgjodhëm të dytën.

Të zëvendësohen indekset në tabelën e re me kufizime të vonuara nga tabela origjinale.

QĂ«llimi i pĂ«rmirĂ«simit ishte i qartĂ« – nĂ«se tabela origjinale ka njĂ« kufizim tĂ« vonuar, atĂ«herĂ« pĂ«r tĂ« rejĂ«n duhet tĂ« krijohet njĂ« kufizim tĂ« tillĂ«, e jo njĂ« indeks.

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

  • tabela me kufizim tĂ« vonuar dhe njĂ« regjistrim;
  • nĂ« mĂ«nyrĂ« ciklike fusim tĂ« dhĂ«na qĂ« bien ndesh me regjistrimin ekzistues;
  • po bĂ«jmĂ« update – tĂ« dhĂ«nat tani nuk janĂ« mĂ« nĂ« konflikt;
  • komitimi i ndryshimeve.

krijo tavolinen test_table
(
  id serial,
  val int,
  kushti uk_test_table__val unik (val) deferrable fillimisht i çuar mbrapa 
);

SHKRUAJ në test_table (val) VLERAT (0);
Për i NGA 1..10000 LOOP
  fillo
    SHKRUAJ në test_table VLERAT (0) KTHE ID në v_id;
    UPDATE test_table vendos val = i ku id = v_id;
    COMMIT;
  FUND;
END LOOP;

Versioni origjinal i pg_repack gjithmonë binte në regjistrin e parë, versioni i përmirësuar punonte pa gabime. Shumë mirë.

Shkoni në prodhim dhe përsëri marrim një gabim në të njëjtën fazë të kopjimit të të dhënave nga tabela log në të re:

$ ./pg_repack -t tablename -o id
INFO: po ribëhet tavolina "tablename"
ERROR: kërkesa dështoi: 
    ERROR: vlera e dyfishuar e çelësit shkel kufizimin unik "index_16508"
DETALJ:  ÇelĂ«si (id, index)=(100500, 42) ekziston tashmĂ«.

Situata klasike: nĂ« ambientet testuese gjithçka funksionon, por nĂ« prodhim – jo?!

APPLY_COUNT dhe ndërprerja e dy grupeve

Filluam të analizojmë kodin fjalë për fjalë dhe zbuluam një moment të rëndësishëm: derdhja e të dhënave nga tabela log në të re ndodh me grupe, konstanta APPLY_COUNT tregonte madhësinë e grupit:

për (;;)
{
num = apply_log(connection, table, APPLY_COUNT);

nëse (num > MIN_TUPLES_BEFORE_SWITCH)
     vazhdo;  
 /* mund të ketë ende disa tuple, përsërit. */
...
}

Problemi Ă«shtĂ« se tĂ« dhĂ«nat e transaksionit origjinal, nĂ« tĂ« cilin disa operacione mund potencialisht tĂ« shkelin kufizimin, gjatĂ« transferimit mund tĂ« pĂ«rfundojnĂ« nĂ« ndĂ«rprerjen e dy grupeve – gjysma e komandave do tĂ« jetĂ« e angazhuar nĂ« grupin e parĂ«, ndĂ«rsa gjysma tjetĂ«r – nĂ« tĂ« dytin. Dhe kĂ«tu si do qĂ« tĂ« ndodhĂ«: nĂ«se komandat nĂ« grupin e parĂ« nuk shkelin asgjĂ«, atĂ«herĂ« gjithçka Ă«shtĂ« mirĂ«, por nĂ«se shkelin – ndodh njĂ« gabim.

APPLY_COUNT Ă«shtĂ« 1000 regjistrime, qĂ« shpjegon pse testet tona kaluan me sukses – ato nuk mbulonin rastin e "ndryshimit tĂ« grupeve". Ne pĂ«rdorĂ«m dy komanda – insert dhe update, kĂ«shtu qĂ« 500 transaksione, pĂ«r dy komanda, gjithmonĂ« vendoseshin nĂ« grup dhe nuk patĂ«m probleme. Pas shtimit tĂ« azhurnimit tĂ« dytĂ«, ndryshimi ynĂ« pushoi sĂ« punuari:

PËR i NGA 1..10000 LOOP
  fillo
    SHKRUAJ në test_table VLERAT (1) KTHE ID në v_id;
    UPDATE test_table vendos val = i ku id = v_id;
    UPDATE test_table vendos val = i ku id = v_id; -- një azhurnim më shumë
    COMMIT;
  FUND;
END LOOP;

Pra, detyra e ardhshme është që të bëjmë që të dhënat nga tabela origjinale, të cilat u ndryshuan në një transaksion, të kalonin gjithashtu në tabelën e re brenda të njëjtit transaksion.

Heqja dorë nga batching

NĂ« kĂ«tĂ« rast, kishim sĂ«rish dy opsione zgjidhjeje. E para: le tĂ« heqim dorĂ« krejtĂ«sisht nga ndarja nĂ« grupe dhe tĂ« bĂ«jmĂ« transferimin e tĂ« dhĂ«nave me njĂ« transaksion. NĂ« favor tĂ« kĂ«saj zgjidhjeje ishte thjeshtĂ«sia e saj – ndryshimet e nevojshme nĂ« kod janĂ« minimale (pĂ«r mĂ« tepĂ«r, nĂ« versionet mĂ« tĂ« vjetra, pg_reorg funksiononte pikĂ«risht nĂ« kĂ«tĂ« mĂ«nyrĂ«). 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.

E dyta — njĂ« zgjidhje mĂ« e komplikuar, por ndoshta mĂ« e saktĂ«: tĂ« krijojmĂ« nĂ« tabelĂ«n log njĂ« kolonĂ« me identifikuesin e atij transaksioni qĂ« shtoi tĂ« dhĂ«nat nĂ« tabelĂ«. KĂ«shtu, gjatĂ« kopjimit tĂ« tĂ« dhĂ«nave, ne do tĂ« mund tĂ« grumbullojmĂ« ato sipas kĂ«tij atributi dhe tĂ« garantojmĂ« qĂ« ndryshimet e lidhura tĂ« transferohen sĂ« bashku. Grupi do tĂ« formohet nga disa transaksione (ose njĂ« e madhe) dhe madhĂ«sia e tij do tĂ« varirojĂ« nĂ« varĂ«si tĂ« sasisĂ« sĂ« tĂ« dhĂ«nave qĂ« janĂ« ndryshuar nĂ« kĂ«to transaksione. ËshtĂ« e rĂ«ndĂ«sishme tĂ« theksohet se, pasi tĂ« dhĂ«nat nga transaksione tĂ« ndryshme hyjnĂ« nĂ« tabelĂ«n log nĂ« njĂ« rend tĂ« rastĂ«sishĂ«m, nuk do tĂ« jetĂ« mĂ« e mundur ta lexojmĂ« atĂ« nĂ« mĂ«nyrĂ« tĂ« rregullt, siç ishte mĂ« parĂ«. seqscan gjatĂ« çdo kĂ«rkese me filtrimin sipas tx_id - Ă«shtĂ« shumĂ« e shtrenjtĂ«, ne kemi nevojĂ« pĂ«r njĂ« indeks, por dhe ai do ta ngadalĂ«sojĂ« metodĂ«n pĂ«r shkak tĂ« kostove tĂ« saj tĂ« pĂ«rditĂ«simit. NĂ« pĂ«rgjithĂ«si, si gjithmonĂ« duhet tĂ« sakrifikojmĂ« diçka.

Pra, vendosĂ«m tĂ« fillojmĂ« me opsionin e parĂ«, si mĂ« tĂ« thjeshtin. SĂ« pari, duhej tĂ« kuptonim nĂ«se transaksioni i gjatĂ« do tĂ« ishte njĂ« problem i vĂ«rtetĂ«. Duke qenĂ« se transferimi kryesor i tĂ« dhĂ«nave nga tabela e vjetĂ«r nĂ« tĂ« re po bĂ«hej gjithashtu nĂ« njĂ« transaksion tĂ« gjatĂ«, pyetja u transformua nĂ« “sa do ta rrisim kĂ«tĂ« transaksion?” Zgjatja e transaksionit tĂ« parĂ« varet kryesisht nga madhĂ«sia e tabelĂ«s. Zgjatja e re – nga sa shumĂ« ndryshime do tĂ« grumbullohen nĂ« tabelĂ« gjatĂ« periudhĂ«s sĂ« transferimit tĂ« tĂ« dhĂ«nave, dmth nga intensiteti i ngarkesĂ«s. Rregullimi i pg_repack u realizua gjatĂ« ngarkesĂ«s minimale nĂ« shĂ«rbim, dhe sasia e ndryshimeve ishte asnjĂ«herĂ« e krahasueshme me volumet origjinale tĂ« tabelĂ«s. VendosĂ«m se mund tĂ« injorojmĂ« kohĂ«zgjatjen e transaksionit tĂ« ri (pĂ«r krahasim, mesatarisht Ă«shtĂ« 1 orĂ« dhe 2-3 minuta).

Eksperimentet ishin pozitive. Aktivizimi nĂ« prodhim gjithashtu. PĂ«r qartĂ«si – njĂ« imazh me madhĂ«sinĂ« e njĂ« prej bazave pas rregullimit:

Postgres: fryrje, pg_repack dhe kufizime të shtyra

Për sa kohë që kjo zgjidhje na përmbushi plotësisht, ne nuk u përpoqëm të realizonim të dytën, por po shqyrtojmë mundësinë e diskutimit të saj me zhvilluesit e zgjerimit. Përshkak se përmirësimi ynë aktual, fatkeqësisht, ende nuk është i gatshëm për publikim, pasi ne zgjidhëm problemin vetëm me kufizime të veçanta të shtyra, dhe për një patch të plotë është e nevojshme të bëhet mbështetje e llojeve të tjera. Shpresojmë se do të arrijmë ta realizojmë këtë në të ardhmen.

Ndoshta keni pyetur pse ne u angazhuam në këtë histori me përmirësimin e pg_repack, dhe nuk filluam të përdorim alternativat e tij? Në një moment, edhe ne menduam për këtë, por përvoja pozitive e përdorimit të tij më parë, në tabela pa kufizime të shtyra, na motivoi të përpiqeshim të kuptonim thelbin e problemit dhe ta rregullojmë atë. Për më tepër, për të përdorur zgjidhje të tjera gjithashtu kërkohet kohë për të kryer teste, prandaj ne vendosëm që së pari të përpiqeshim ta rregullojmë problemin në të, dhe nëse kuptojmë se nuk do të jemi në gjendje ta bëjmë këtë në një kohë të arsyeshme, atëherë do të fillojmë të shqyrtojmë alternativat.

Përfundimet

ÇfarĂ« mund tĂ« rekomandojmĂ« nga pĂ«rvoja jonĂ«:

  1. Monitoroni bloat-in tuaj. Në bazë të të dhënave të monitorimit do të mund të kuptoni sa mirë është konfiguruar autovacuum.
  2. Konfiguroni AUTOVACUUM-in për të mbajtur bloat-in në një nivel të pranueshëm.
  3. Nëse bloat-i ende rritet dhe nuk mund ta mposhtni atë me mjete "në kuti", mos e frikësoni të përdorni zgjerime të jashtme. E rëndësishme është që të testoni gjithçka mirë.
  4. Mos kini frikĂ« tĂ« pĂ«rmirĂ«soni zgjidhje tĂ« jashtme sipas nevojave tuaja – ndonjĂ«herĂ« kjo mund tĂ« jetĂ« mĂ« efektive dhe madje mĂ« e lehtĂ« se sa ndryshimi i kodit tuaj.

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster