Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Ang epekto ng bloat sa mga talahanayan at index ay malawak na kilala at naroroon hindi lamang sa Postgres. May mga paraan upang harapin ito sa labas ng kahon, tulad ng VACUUM FULL o CLUSTER, ngunit nila-lock nila ang mga talahanayan sa panahon ng operasyon at samakatuwid ay hindi palaging magagamit.

Ang artikulo ay maglalaman ng isang maliit na teorya tungkol sa kung paano nangyayari ang bloat, kung paano mo ito malalabanan, tungkol sa mga ipinagpaliban na mga hadlang at ang mga problemang dinadala nila sa paggamit ng extension ng pg_repack.

Ang artikulong ito ay isinulat batay sa ang aking talumpati sa PgConf.Russia 2020.

Bakit nangyayari ang bloat?

Ang mga postgres ay batay sa isang multi-bersyon na modelo (MVCC). Ang kakanyahan nito ay ang bawat hilera sa talahanayan ay maaaring magkaroon ng ilang mga bersyon, habang ang mga transaksyon ay nakakakita ng hindi hihigit sa isa sa mga bersyong ito, ngunit hindi palaging pareho. Nagbibigay-daan ito sa ilang transaksyon na gumana nang sabay-sabay at halos walang epekto sa isa't isa.

Malinaw, ang lahat ng mga bersyong ito ay kailangang maimbak. Ang mga postgres ay gumagana sa memorya ng pahina sa bawat pahina at ang isang pahina ay ang pinakamababang dami ng data na maaaring basahin mula sa disk o nakasulat. Tingnan natin ang isang maliit na halimbawa upang maunawaan kung paano ito nangyayari.

Sabihin nating mayroon kaming isang talahanayan kung saan nagdagdag kami ng ilang mga tala. Ang bagong data ay lumitaw sa unang pahina ng file kung saan naka-imbak ang talahanayan. Ito ay mga live na bersyon ng mga row na available sa iba pang mga transaksyon pagkatapos ng isang commit (para sa pagiging simple, ipapalagay namin na ang antas ng paghihiwalay ay Read Committed).

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Pagkatapos ay na-update namin ang isa sa mga entry, sa gayon ay minarkahan ang lumang bersyon bilang hindi na nauugnay.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Hakbang-hakbang, pag-update at pagtanggal ng mga bersyon ng row, napunta kami sa isang pahina kung saan humigit-kumulang kalahati ng data ay "basura". Ang data na ito ay hindi nakikita sa anumang transaksyon.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

May mekanismo ang mga postgres VACUUM, na naglilinis ng mga hindi na ginagamit na bersyon at nagbibigay ng puwang para sa bagong data. Ngunit kung hindi ito na-configure nang sapat na agresibo o abala sa pagtatrabaho sa iba pang mga talahanayan, mananatili ang "data ng basura", at kailangan nating gumamit ng mga karagdagang pahina para sa bagong data.

Kaya sa aming halimbawa, sa isang punto sa oras ang talahanayan ay bubuo ng apat na pahina, ngunit kalahati lamang nito ang maglalaman ng live na data. Bilang resulta, kapag ina-access ang talahanayan, magbabasa kami ng higit pang data kaysa sa kinakailangan.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Kahit na tanggalin na ngayon ng VACUUM ang lahat ng walang kaugnayang bersyon ng row, hindi bubuti nang husto ang sitwasyon. Magkakaroon kami ng libreng espasyo sa mga pahina o kahit sa buong mga pahina para sa mga bagong row, ngunit magbabasa pa rin kami ng higit pang data kaysa sa kinakailangan.
Sa pamamagitan ng paraan, kung ang isang ganap na blangko na pahina (ang pangalawa sa aming halimbawa) ay nasa dulo ng file, ang VACUUM ay magagawang i-trim ito. Pero ngayon ay nasa gitna na siya, kaya walang magawa sa kanya.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Kapag ang bilang ng mga walang laman o lubhang kalat-kalat na pahina ay naging malaki, na tinatawag na bloat, nagsisimula itong makaapekto sa pagganap.

Ang lahat ng inilarawan sa itaas ay ang mekanika ng paglitaw ng bloat sa mga talahanayan. Sa mga index, nangyayari ito sa halos parehong paraan.

May bloat ba ako?

Mayroong ilang mga paraan upang matukoy kung mayroon kang bloat. Ang ideya ng una ay ang paggamit ng mga panloob na istatistika ng Postgres, na naglalaman ng tinatayang impormasyon tungkol sa bilang ng mga hilera sa mga talahanayan, ang bilang ng mga "live" na hilera, atbp. Makakahanap ka ng maraming mga pagkakaiba-iba ng mga yari na script sa Internet. Kinuha namin bilang batayan iskrip mula sa PostgreSQL Experts, na maaaring suriin ang mga bloat table kasama ng toast at bloat btree index. Sa aming karanasan, ang error nito ay 10-20%.

Ang isa pang paraan ay ang paggamit ng extension pgstattuple, na nagbibigay-daan sa iyong tumingin sa loob ng mga pahina at makakuha ng parehong tinantyang at eksaktong bloat na halaga. Ngunit sa pangalawang kaso, kakailanganin mong i-scan ang buong talahanayan.

Isinasaalang-alang namin ang isang maliit na halaga ng bloat, hanggang sa 20%, katanggap-tanggap. Maaari itong ituring bilang isang analogue ng fillfactor para sa mga talahanayan и mga indeks. Sa 50% pataas, maaaring magsimula ang mga problema sa pagganap.

Mga paraan upang labanan ang bloat

Ang mga postgres ay may ilang mga paraan upang harapin ang bloat out of the box, ngunit hindi sila palaging angkop para sa lahat.

I-configure ang AUTOVACUUM para hindi mangyari ang bloat. O mas tiyak, para panatilihin ito sa antas na katanggap-tanggap sa iyo. Ito ay tila payo ng "kapitan", ngunit sa katotohanan ay hindi ito laging madaling makamit. Halimbawa, mayroon kang aktibong pag-develop na may mga regular na pagbabago sa schema ng data, o may nagaganap na uri ng paglilipat ng data. Bilang resulta, ang iyong load profile ay maaaring madalas na magbago at karaniwang nag-iiba-iba sa bawat talahanayan. Nangangahulugan ito na kailangan mong patuloy na magtrabaho nang kaunti sa unahan at ayusin ang AUTOVACUUM sa pagbabago ng profile ng bawat talahanayan. Ngunit malinaw na hindi ito madaling gawin.

Ang isa pang karaniwang dahilan kung bakit hindi nakakasabay ang AUTOVACUUM sa mga talahanayan ay dahil may mga matagal nang transaksyon na pumipigil sa paglilinis nito sa data na available sa mga transaksyong iyon. Ang rekomendasyon dito ay halata din - alisin ang "nakakalawit" na mga transaksyon at bawasan ang oras ng mga aktibong transaksyon. Ngunit kung ang pag-load sa iyong aplikasyon ay hybrid ng OLAP at OLTP, maaari kang magkasabay na magkaroon ng maraming madalas na pag-update at maikling query, pati na rin ang mga pangmatagalang operasyon - halimbawa, pagbuo ng isang ulat. Sa ganoong sitwasyon, ito ay nagkakahalaga ng pag-iisip tungkol sa pagkalat ng load sa iba't ibang mga base, na magbibigay-daan para sa higit pang fine-tuning ng bawat isa sa kanila.

Ang isa pang halimbawa - kahit na ang profile ay homogenous, ngunit ang database ay nasa ilalim ng napakataas na pag-load, kung gayon kahit na ang pinaka-agresibong AUTOVACUUM ay maaaring hindi makayanan, at ang bloat ay magaganap. Ang pag-scale (vertical o horizontal) ang tanging solusyon.

Ano ang gagawin sa isang sitwasyon kung saan na-set up mo ang AUTOVACUUM, ngunit patuloy na lumalaki ang bloat.

Koponan PUNO ANG VACUUM muling itinatayo ang mga nilalaman ng mga talahanayan at mga index at nag-iiwan lamang ng mga nauugnay na data sa mga ito. Upang maalis ang bloat, ito ay gumagana nang perpekto, ngunit sa panahon ng pagpapatupad nito, ang isang eksklusibong lock sa talahanayan ay nakuha (AccessExclusiveLock), na hindi papayag na magsagawa ng mga query sa talahanayang ito, kahit na pumili. Kung kaya mong ihinto ang iyong serbisyo o bahagi nito sa loob ng ilang oras (mula sampu-sampung minuto hanggang ilang oras depende sa laki ng database at iyong hardware), kung gayon ang pagpipiliang ito ay ang pinakamahusay. Sa kasamaang palad, wala kaming oras upang patakbuhin ang VACUUM FULL sa panahon ng naka-iskedyul na pagpapanatili, kaya ang paraang ito ay hindi angkop para sa amin.

Koponan MGA KLUSTER Binubuo muli ang mga nilalaman ng mga talahanayan sa parehong paraan tulad ng VACUUM FULL, ngunit nagbibigay-daan sa iyong tukuyin ang isang index ayon sa kung saan ang data ay pisikal na iuutos sa disk (ngunit sa hinaharap ang order ay hindi ginagarantiyahan para sa mga bagong hilera). Sa ilang mga sitwasyon, ito ay isang mahusay na pag-optimize para sa isang bilang ng mga query - sa pagbabasa ng maramihang mga tala sa pamamagitan ng index. Ang kawalan ng utos ay kapareho ng sa VACUUM FULL - ni-lock nito ang talahanayan sa panahon ng operasyon.

Koponan REINDEX katulad ng naunang dalawa, ngunit muling bumubuo ng isang tiyak na index o lahat ng mga index ng talahanayan. Ang mga lock ay bahagyang mahina: ShareLock sa talahanayan (pinipigilan ang mga pagbabago, ngunit pinapayagan ang pagpili) at AccessExclusiveLock sa index na itinayong muli (bina-block ang mga query gamit ang index na ito). Gayunpaman, sa ika-12 na bersyon ng Postgres isang parameter ang lumitaw KASABAY, na nagbibigay-daan sa iyong muling buuin ang index nang hindi hinaharangan ang kasabay na pagdaragdag, pagbabago, o pagtanggal ng mga tala.

Sa mga naunang bersyon ng Postgres, makakamit mo ang isang resultang katulad ng REINDEX na KASABAY gamit GUMAWA NG INDEX NG KASABAY. Pinapayagan ka nitong lumikha ng isang index nang walang mahigpit na pag-lock (ShareUpdateExclusiveLock, na hindi nakakasagabal sa mga parallel na query), pagkatapos ay palitan ang lumang index ng bago at tanggalin ang lumang index. Ito ay nagpapahintulot sa iyo na alisin ang index bloat nang hindi nakakasagabal sa iyong aplikasyon. Mahalagang isaalang-alang na kapag muling nagtatayo ng mga index ay magkakaroon ng karagdagang pagkarga sa subsystem ng disk.

Kaya, kung para sa mga index ay may mga paraan upang maalis ang bloat "on the fly," kung gayon walang para sa mga talahanayan. Dito pumapasok ang iba't ibang panlabas na extension: pg_repack (dating pg_reorg), pgcompact, pgcompacttable at iba pa. Sa artikulong ito, hindi ko ihahambing ang mga ito at magsasalita lamang tungkol sa pg_repack, na, pagkatapos ng ilang pagbabago, ginagamit namin ang aming sarili.

Paano gumagana ang pg_repack

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang
Sabihin nating mayroon tayong ganap na ordinaryong talahanayan - na may mga index, mga paghihigpit at, sa kasamaang-palad, may bloat. Ang unang hakbang ng pg_repack ay gumawa ng log table para mag-imbak ng data tungkol sa lahat ng pagbabago habang ito ay tumatakbo. Gagawin ng trigger ang mga pagbabagong ito para sa bawat pagpasok, pag-update at pagtanggal. Pagkatapos ay nilikha ang isang talahanayan, katulad ng orihinal sa istraktura, ngunit walang mga index at paghihigpit, upang hindi mapabagal ang proseso ng pagpasok ng data.

Susunod, inililipat ng pg_repack ang data mula sa lumang talahanayan patungo sa bagong talahanayan, awtomatikong sinasala ang lahat ng hindi nauugnay na mga hilera, at pagkatapos ay lumilikha ng mga index para sa bagong talahanayan. Sa panahon ng pagpapatupad ng lahat ng mga operasyong ito, ang mga pagbabago ay naiipon sa talaan ng log.

Ang susunod na hakbang ay ilipat ang mga pagbabago sa bagong talahanayan. Isinasagawa ang paglipat sa ilang mga pag-ulit, at kapag wala pang 20 entry ang natitira sa log table, nakakakuha ang pg_repack ng matibay na lock, nililipat ang pinakabagong data, at pinapalitan ang lumang talahanayan ng bago sa mga talahanayan ng Postgres system. Ito ang tanging at napakaikling panahon na hindi mo magagawang magtrabaho kasama ang talahanayan. Pagkatapos nito, ang lumang talahanayan at ang talahanayan na may mga log ay tinanggal at ang espasyo ay nabakante sa file system. Kumpleto na ang proseso.

Ang lahat ay mukhang mahusay sa teorya, ngunit ano ang nangyayari sa pagsasanay? Sinubukan namin ang pg_repack nang walang load at under load, at sinuri ang operasyon nito kung sakaling napaaga ang paghinto (sa madaling salita, gamit ang Ctrl+C). Lahat ng mga pagsusuri ay positibo.

Nagpunta kami sa tindahan ng pagkain - at pagkatapos ay hindi nangyari ang lahat tulad ng inaasahan namin.

Ibinebenta ang unang pancake

Sa unang cluster nakatanggap kami ng error tungkol sa isang paglabag sa isang natatanging hadlang:

$ ./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.

Ang limitasyong ito ay may awtomatikong nabuong pangalan na index_16508 - ito ay nilikha ng pg_repack. Batay sa mga katangiang kasama sa komposisyon nito, natukoy namin ang "aming" hadlang na tumutugma dito. Ang problema ay hindi ito isang ganap na ordinaryong limitasyon, ngunit isang ipinagpaliban (ipinagpaliban ang pagpilit), ibig sabihin. ang pag-verify nito ay isinasagawa sa ibang pagkakataon kaysa sa sql command, na humahantong sa hindi inaasahang mga kahihinatnan.

Mga ipinagpaliban na hadlang: bakit kailangan ang mga ito at kung paano ito gumagana

Isang maliit na teorya tungkol sa mga ipinagpaliban na paghihigpit.
Isaalang-alang natin ang isang simpleng halimbawa: mayroon kaming isang table-reference na libro ng mga kotse na may dalawang katangian - ang pangalan at pagkakasunud-sunod ng kotse sa direktoryo.
Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

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



Sabihin nating kailangan nating palitan ang una at pangalawang kotse. Ang direktang solusyon ay ang pag-update ng unang halaga sa pangalawa, at ang pangalawa sa una:

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

Ngunit kapag pinapatakbo namin ang code na ito, inaasahan namin ang isang paglabag sa pagpilit dahil ang pagkakasunud-sunod ng mga halaga sa talahanayan ay natatangi:

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

Paano ko ito magagawa nang iba? Opsyon isa: magdagdag ng karagdagang pagpapalit ng halaga sa isang order na garantisadong hindi iiral sa talahanayan, halimbawa "-1". Sa programming, ito ay tinatawag na "pagpapalit ng mga halaga ng dalawang variable sa pamamagitan ng isang pangatlo." Ang tanging disbentaha ng pamamaraang ito ay ang karagdagang pag-update.

Opsyon dalawa: Muling idisenyo ang talahanayan upang gumamit ng isang floating point na uri ng data para sa halaga ng order sa halip na mga integer. Pagkatapos, kapag ina-update ang halaga mula 1, halimbawa, hanggang 2.5, ang unang entry ay awtomatikong "tatayo" sa pagitan ng pangalawa at pangatlo. Gumagana ang solusyon na ito, ngunit may dalawang limitasyon. Una, hindi ito gagana para sa iyo kung ang halaga ay ginagamit sa isang lugar sa interface. Pangalawa, depende sa katumpakan ng uri ng data, magkakaroon ka ng limitadong bilang ng mga posibleng pagsingit bago muling kalkulahin ang mga halaga ng lahat ng mga tala.

Ikatlong opsyon: gawin ang pagpilit na ipinagpaliban upang ito ay masuri lamang sa oras ng commit:

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

Dahil ang lohika ng aming paunang kahilingan ay nagsisiguro na ang lahat ng mga halaga ay natatangi sa oras ng commit, ito ay magtatagumpay.

Ang halimbawang tinalakay sa itaas ay, siyempre, napaka-synthetic, ngunit ito ay nagpapakita ng ideya. Sa aming application, gumagamit kami ng mga ipinagpaliban na hadlang upang ipatupad ang lohika na responsable para sa paglutas ng mga salungatan kapag ang mga user ay sabay-sabay na nagtatrabaho sa mga nakabahaging widget na bagay sa board. Ang paggamit ng gayong mga paghihigpit ay nagpapahintulot sa amin na gawing mas simple ang application code.

Sa pangkalahatan, depende sa uri ng pagpilit, ang Postgres ay may tatlong antas ng granularity para sa pagsuri sa mga ito: row, transaksyon, at mga antas ng expression.
Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang
Pinagmulan: mga begriff

Ang CHECK at NOT NULL ay palaging sinusuri sa antas ng hilera; para sa iba pang mga paghihigpit, tulad ng makikita mula sa talahanayan, mayroong iba't ibang mga opsyon. Maaari mong basahin ang higit pa dito.

Upang maikling buod, ang mga ipinagpaliban na mga hadlang sa ilang mga sitwasyon ay nagbibigay ng mas nababasang code at mas kaunting mga utos. Gayunpaman, kailangan mong magbayad para dito sa pamamagitan ng pagpapakumplikado sa proseso ng pag-debug, dahil sa sandaling mangyari ang error at sa sandaling malaman mo ang tungkol dito ay pinaghihiwalay sa oras. Ang isa pang posibleng problema ay ang scheduler ay maaaring hindi palaging makakagawa ng isang pinakamainam na plano kung ang kahilingan ay nagsasangkot ng isang ipinagpaliban na pagpilit.

Pagpapabuti ng pg_repack

Sinaklaw namin kung ano ang mga ipinagpaliban na mga hadlang, ngunit paano nauugnay ang mga ito sa aming problema? Tandaan natin ang error na natanggap natin kanina:

$ ./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.

Ito ay nangyayari kapag ang data ay kinopya mula sa isang log table patungo sa isang bagong table. Mukhang kakaiba ito dahil... ang data sa log table ay naka-commit kasama ang data sa source table. Kung natutugunan nila ang mga hadlang ng orihinal na talahanayan, paano nila malalabag ang parehong mga hadlang sa bago?

Sa lumalabas, ang ugat ng problema ay nakasalalay sa nakaraang hakbang ng pg_repack, na lumilikha lamang ng mga index, ngunit hindi mga hadlang: ang lumang talahanayan ay may natatanging pagpilit, at ang bago ay lumikha ng isang natatanging index sa halip.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Mahalagang tandaan dito na kung ang pagpilit ay normal at hindi ipinagpaliban, ang natatanging index na ginawa sa halip ay katumbas ng pagpilit na ito, dahil Ang mga natatanging hadlang sa Postgres ay ipinapatupad sa pamamagitan ng paglikha ng isang natatanging index. Ngunit sa kaso ng isang ipinagpaliban na pagpilit, ang pag-uugali ay hindi pareho, dahil ang index ay hindi maaaring ipagpaliban at palaging sinusuri sa oras na ang sql command ay naisakatuparan.

Kaya, ang kakanyahan ng problema ay nakasalalay sa "pagkaantala" ng tseke: sa orihinal na talahanayan ito ay nangyayari sa oras ng commit, at sa bagong talahanayan sa oras na ang sql command ay naisakatuparan. Nangangahulugan ito na kailangan nating tiyakin na ang mga pagsusuri ay isinasagawa nang pareho sa parehong mga kaso: alinman sa palaging naantala, o palaging kaagad.

Kaya anong mga ideya ang mayroon tayo?

Gumawa ng index na katulad ng ipinagpaliban

Ang unang ideya ay gawin ang parehong mga pagsusuri sa agarang mode. Maaari itong makabuo ng ilang maling positibong paghihigpit, ngunit kung kakaunti ang mga ito, hindi ito dapat makaapekto sa gawain ng mga gumagamit, dahil ang mga naturang salungatan ay isang normal na sitwasyon para sa kanila. Nangyayari ang mga ito, halimbawa, kapag nagsimulang i-edit ng dalawang user ang parehong widget sa parehong oras, at ang kliyente ng pangalawang user ay walang oras upang makatanggap ng impormasyon na ang widget ay naka-block na para sa pag-edit ng unang user. Sa ganoong sitwasyon, tinatanggihan ng server ang pangalawang user, at ibinabalik ng kliyente nito ang mga pagbabago at hinaharangan ang widget. Maya-maya, kapag nakumpleto ng unang user ang pag-edit, ang pangalawa ay makakatanggap ng impormasyon na ang widget ay hindi na naka-block at magagawang ulitin ang kanilang pagkilos.

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Upang matiyak na ang mga pagsusuri ay palaging nasa non-deferred mode, gumawa kami ng bagong index na katulad ng orihinal na ipinagpaliban na pagpilit:

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

Sa kapaligiran ng pagsubok, nakatanggap lang kami ng ilang inaasahang error. Tagumpay! Nagpatakbo kaming muli ng pg_repack sa produksyon at nakakuha ng 5 error sa unang cluster sa isang oras ng trabaho. Ito ay isang katanggap-tanggap na resulta. Gayunpaman, nasa pangalawang cluster na ang bilang ng mga error ay tumaas nang malaki at kinailangan naming ihinto ang pg_repack.

Bakit nangyari? Ang posibilidad na magkaroon ng error ay depende sa kung gaano karaming mga user ang nagtatrabaho sa parehong mga widget sa parehong oras. Tila, sa sandaling iyon ay may mas kaunting mga pagbabago sa mapagkumpitensya sa data na nakaimbak sa unang kumpol kaysa sa iba, i.e. "maswerte" lang kami.

Hindi gumana ang ideya. Sa puntong iyon, nakakita kami ng dalawa pang solusyon: muling isulat ang aming code ng aplikasyon para mawala ang mga ipinagpaliban na hadlang, o "turuan" ang pg_repack na makipagtulungan sa kanila. Pinili namin ang pangalawa.

Palitan ang mga index sa bagong talahanayan ng mga ipinagpaliban na limitasyon mula sa orihinal na talahanayan

Ang layunin ng rebisyon ay halata - kung ang orihinal na talahanayan ay may ipinagpaliban na pagpilit, kung gayon para sa bago kailangan mong lumikha ng gayong pagpilit, at hindi isang index.

Upang subukan ang aming mga pagbabago, nagsulat kami ng isang simpleng pagsubok:

  • talahanayan na may ipinagpaliban na pagpilit at isang talaan;
  • magpasok ng data sa isang loop na sumasalungat sa isang umiiral na tala;
  • gumawa ng pag-update - hindi na sumasalungat ang data;
  • gawin ang mga pagbabago.

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;

Ang orihinal na bersyon ng pg_repack ay palaging nag-crash sa unang insert, ang binagong bersyon ay gumana nang walang mga error. Malaki.

Pumunta kami sa produksyon at muling nakakuha ng error sa parehong yugto ng pagkopya ng data mula sa log table patungo sa bago:

$ ./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.

Klasikong sitwasyon: gumagana ang lahat sa mga kapaligiran ng pagsubok, ngunit hindi sa produksyon?!

APPLY_COUNT at ang junction ng dalawang batch

Sinimulan naming pag-aralan ang code nang literal na linya sa linya at natuklasan ang isang mahalagang punto: inililipat ang data mula sa log table patungo sa bago sa mga batch, ang APPLY_COUNT na pare-pareho ay nagpahiwatig ng laki ng batch:

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

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

Ang problema ay ang data mula sa orihinal na transaksyon, kung saan ang ilang mga operasyon ay maaaring lumabag sa pagpilit, kapag inilipat, ay maaaring mapunta sa junction ng dalawang batch - kalahati ng mga utos ay gagawin sa unang batch, at ang iba pang kalahati sa pangalawa. At dito, depende sa iyong swerte: kung ang mga koponan ay hindi lumalabag sa anuman sa unang batch, kung gayon ang lahat ay maayos, ngunit kung gagawin nila, isang error ang nangyayari.

Ang APPLY_COUNT ay katumbas ng 1000 record, na nagpapaliwanag kung bakit naging matagumpay ang aming mga pagsubok - hindi nila sinakop ang kaso ng "batch junction." Gumamit kami ng dalawang utos - ipasok at i-update, kaya eksaktong 500 transaksyon ng dalawang utos ang palaging inilalagay sa isang batch at hindi kami nakaranas ng anumang mga problema. Pagkatapos idagdag ang pangalawang pag-update, tumigil sa paggana ang aming pag-edit:

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;

Kaya, ang susunod na gawain ay tiyakin na ang data mula sa orihinal na talahanayan, na binago sa isang transaksyon, ay mapupunta sa bagong talahanayan din sa loob ng isang transaksyon.

Pagtanggi sa batching

At muli mayroon kaming dalawang solusyon. Una: ganap nating iwanan ang paghahati sa mga batch at maglipat ng data sa isang transaksyon. Ang bentahe ng solusyon na ito ay ang pagiging simple nito - ang mga kinakailangang pagbabago sa code ay minimal (sa pamamagitan ng paraan, sa mas lumang mga bersyon pg_reorg gumana nang eksakto tulad na). Ngunit mayroong isang problema - lumilikha kami ng isang matagal na transaksyon, at ito, tulad ng naunang sinabi, ay isang banta sa paglitaw ng isang bagong bloat.

Ang pangalawang solusyon ay mas kumplikado, ngunit malamang na mas tama: gumawa ng column sa log table na may identifier ng transaksyon na nagdagdag ng data sa talahanayan. Pagkatapos, kapag kinopya namin ang data, maaari naming igrupo ito ayon sa katangiang ito at matiyak na magkakasamang ililipat ang mga kaugnay na pagbabago. Ang batch ay mabubuo mula sa ilang mga transaksyon (o isang malaki) at ang laki nito ay mag-iiba depende sa kung gaano karaming data ang nabago sa mga transaksyong ito. Mahalagang tandaan na dahil ang data mula sa iba't ibang mga transaksyon ay pumapasok sa log table sa isang random na pagkakasunud-sunod, hindi na posible na basahin ito nang sunud-sunod, tulad ng dati. seqscan para sa bawat kahilingan na may pag-filter sa pamamagitan ng tx_id ay masyadong mahal, isang index ay kailangan, ngunit ito rin ay pabagalin ang paraan dahil sa overhead ng pag-update nito. Sa pangkalahatan, gaya ng dati, kailangan mong isakripisyo ang isang bagay.

Kaya, nagpasya kaming magsimula sa unang opsyon, dahil mas simple ito. Una, ito ay kinakailangan upang maunawaan kung ang isang mahabang transaksyon ay magiging isang tunay na problema. Dahil ang pangunahing paglilipat ng data mula sa lumang talahanayan tungo sa bago ay nangyayari rin sa isang mahabang transaksyon, ang tanong ay nabago sa "magkano natin tataas ang transaksyong ito?" Ang tagal ng unang transaksyon ay pangunahing nakasalalay sa laki ng talahanayan. Ang tagal ng bago ay depende sa kung gaano karaming mga pagbabago ang naipon sa talahanayan sa panahon ng paglilipat ng data, i.e. sa intensity ng load. Ang pg_repack run ay naganap sa panahon ng kaunting pag-load ng serbisyo, at ang dami ng mga pagbabago ay hindi proporsyonal na maliit kumpara sa orihinal na laki ng talahanayan. Napagpasyahan namin na maaari naming pabayaan ang oras ng isang bagong transaksyon (para sa paghahambing, sa average na ito ay 1 oras at 2-3 minuto).

Ang mga eksperimento ay positibo. Ilunsad din sa produksyon. Para sa kalinawan, narito ang isang larawan na may sukat ng isa sa mga database pagkatapos tumakbo:

Postgres: bloat, pg_repack at ipinagpaliban na mga hadlang

Dahil ganap kaming nasiyahan sa solusyon na ito, hindi namin sinubukan na ipatupad ang pangalawa, ngunit isinasaalang-alang namin ang posibilidad na talakayin ito sa mga developer ng extension. Ang aming kasalukuyang rebisyon, sa kasamaang-palad, ay hindi pa handa para sa paglalathala, dahil nalutas lamang namin ang problema sa mga natatanging ipinagpaliban na mga paghihigpit, at para sa isang ganap na patch kinakailangan na magbigay ng suporta para sa iba pang mga uri. Umaasa kaming magagawa namin ito sa hinaharap.

Marahil ay may tanong ka, bakit tayo nasangkot sa kwentong ito sa pagbabago ng pg_repack, at hindi, halimbawa, gumamit ng mga analogue nito? Sa ilang mga punto naisip din namin ito, ngunit ang positibong karanasan ng paggamit nito nang mas maaga, sa mga talahanayan nang walang ipinagpaliban na mga hadlang, ay nag-udyok sa amin na subukang maunawaan ang kakanyahan ng problema at ayusin ito. Bilang karagdagan, ang paggamit ng iba pang mga solusyon ay nangangailangan din ng oras upang magsagawa ng mga pagsubok, kaya napagpasyahan namin na susubukan muna naming ayusin ang problema dito, at kung napagtanto namin na hindi namin magagawa ito sa isang makatwirang oras, pagkatapos ay magsisimula kaming tumingin sa mga analogue .

Natuklasan

Ano ang maaari naming irekomenda batay sa aming sariling karanasan:

  1. Subaybayan ang iyong bloat. Batay sa data ng pagsubaybay, mauunawaan mo kung gaano kahusay na na-configure ang autovacuum.
  2. Ayusin ang AUTOVACUUM upang mapanatili ang bloat sa isang katanggap-tanggap na antas.
  3. Kung ang bloat ay lumalaki pa rin at hindi mo ito malalampasan gamit ang mga out-of-the-box na tool, huwag matakot na gumamit ng mga panlabas na extension. Ang pangunahing bagay ay upang subukan ang lahat ng mabuti.
  4. Huwag matakot na baguhin ang mga panlabas na solusyon upang umangkop sa iyong mga pangangailangan - kung minsan ito ay maaaring maging mas epektibo at mas madali kaysa sa pagbabago ng iyong sariling code.

Pinagmulan: www.habr.com

Magdagdag ng komento