PostgreSQL zaključavanje: Kako ga optimizovati i smanjiti broj grešaka?

Danas vam donosimo članak na temu zaključivanja u Postgresu Petra Partlova, software developera u Nordeusu.

Petar Partlov - 8. Avgust, 2016.

Zaključavanje je veoma važan deo PostgreSQL-a (kao i većine drugih modernih sistema za upravljanje relacionim bazama podataka – RDBMS) i trebalo bi da bude poznato svakom programeru DB aplikacija (posebno onima koji rade sa visoko konkurentnim kodom). Zaključavanjem se uglavnom bavimo i ono se istražuje kada nastane neki problem.

U većini slučajeva, ovi problemi se odnose na uzajamnu blokadu transakcija (deadlock) ili nedoslednost podataka usled nerazumevanja oko načina na koji funkcioniše zaključavanje u Postgresu.

Uprkos njegovoj važnosti, mehanizam za zaključavanje u Postgresu je slabo dokumentovan, a u nekim slučajevima čak i ne funkcioniše kao što se očekuje (odnosno, kako je navedeno u dokumentaciji).

Ovim putem ću pokušati da predstavim sve što treba da znate kako biste radili sa Postgresom, imajući u vidu mehanizme za zaključavanje, kao i brže rešavanje problema zaključavanja.

Šta navodi dokumentacija?

Generalno, u Postgresu imamo tri mehanizma za zaključavanje: zaključavanje na nivou tabele (table-level), na nivou reda (row-level) i neobavezno (advisory) zaključavanje.

Zaključavanje na nivou tabele i reda mogu biti eksplicitna ili implicitna. Neobavezno zaklučavanje je uglavnom eksplicitno. Eksplicitno zaključavanje se uspostavlja na osnovu eksplicitnih zahteva korisnika (putem posebnih upita), dok se implicitno uspostavlja putem standardnih SQL komandi.

Pored zaključavanja za tabele i redove, deljeno/ekskluzivno zaključavanjena nivou stranice koristi se za kontrolu čitanja/pisanja pristupa stranicama tabele u deljenom baferu. Ovo zaključavanje se oslobađa odmah nakon učitavanja ili ažuriranja reda.

Programeri aplikacija obično ne moraju da se bave zaključavanjem na nivou stranica.

Mehanizmi za zaključavanje su se vremenom promenili, tako da ću obuhvatiti zaključavanje u 9.x verzijama Postgresa. Verzije 9.1 i 9.2 su uglavnom iste, a verzije 9.3 i 9.4 poseduju neke razlike uglavnom vezane za zaključavanje na nivou reda.

Zaključavanje na nivou tabele

Većina zaključavanja na nivou tabele se uspostavlja ugrađenim SQL komandama, ali mogu se uspostaviti i eksplicitno pomoću LOCK komande. Dostupne opcije za zaključavanje na nivou tabele su sledeće:

ACCESS SHARE – SELECT komanda uspostavlja ovo zaključavanje za tabelu(e) navedenu(e) u upitu. Opšte pravilo je da svi upiti koji samo čitaju tabelu uspostavlja ovu opciju zaključavanja.

ROW SHARE – SELECT FOR UPDATE i SELECT FOR SHARE komande uspostavljaju ovu opciju za zaključavanje za ciljanu tabelu (kao i ACCESS SHARE zaključavanjeza sve tabele navedene u upitu).

ROW EXCLUSIVE – UPDATE, INSERT i DELETE komande uspostavljaju ovu opciju za zaključavanje za ciljanu tabelu (kao i ACCESS SHARE zaključavanje za sve tabele navedene u upitu). Opšte pravilo je da svi upiti koji modifikuju tabelu uspostavljaju ovu opciju zaključavanja.

SHARE UPDATE EXCLUSIVE – VACUUM (bez FULL-a), ANALYZE, CREATE INDEX CONCURRENTLY i neki oblici ALTER TABLE komandi uspostavljaju ovu opciju zaključavanja

SHARE – CREATE INDEX komanda uspostavlja zaključavanje za tabelu navedenu u upitu.

SHARE ROW EXCLUSIVE – Ne uspostavlja se implicitno putem bilo koje komande.

EXCLUSIVE – Ovaj režim zaključavanja omogućava samo obradu čitanja paralelno sa transakcijom koja je stekla ovu opciju zaključavanja. Ne uspostavlja se implicitno putem bilo koje komande.

ACCESS EXCLUSIVE – ALTER TABLE, DROP TABLE, TRUNCATE, REINDEX, CLUSTER i VACUUM FULL komande uspostavljaju zaključavanje za tabelu navedenu u upitu. Ovaj režim je podrazumevani režim LOCK komande.

Važno je znati da su sve ovo opcije za zaključavanje na nivou tabele, čak iako imaju reč ROW u svom nazivu.

Najvažniju informaciju za rezim zaključavanja predstavlja lista režima koji su u konfliktu jedni sa drugima.

Dve transakcije ne mogu posedovati zaključavanja sa konfliktnim režimima za istu tabelu u isto vreme.

Transakcija nikada nije u konfliktu sama sa sobom. Nekonfliktna zaključavanja mogu istovremeno da budu u posedu više transakcija.

Takođe je važno da znate da postoje i samo-konfliktni režimi. Kada se stekne neki režim zaključavanja on se održava do kraja transakcije. Međutim, ako se zaključavanje stekne nakon uspostavljanja odredišne tačke (savepoint), zaključavanje se oslobađa odmah nakon vraćanja (rollback) na odredišnu tačku. Sledi tabela koja prikazuje koji režimi su u sukobu jedni sa drugima:

Screen Shot 2016-08-08 at 09.59.51

Zaključavanje na nivou reda

U Postgresu 9.1 i 9.2 postoje dva režima zaključavanja na nivou reda, dok u Postgresu 9.3 i 9.4 postoje četiri režima.

Postgres ne pamti nikakve informacije o modifikovanim redovima u memoriji, tako da ne postoji ograničenje broja zaključanih redova u datom trenutku. Međutim, zaključavanje reda može da izazove disk write, na primer, SELECT FOR UPDATE modifikuje izabrane redove kako bi se obeležili kao zaključani, što dovodi do disk write-a.

Zaključavanje na nivou reda u Postgresu 9.1 i 9.2

U ovim verzijama postoje samo dve vrste zaključavanja na nivou reda: ekskluzivno ili deljeno zaključavanje.

Ekskluzivno zaključavanje na nivou reda se automatski uspostavlja kada se red ažurira ili briše.

Zaključavanja na nivou reda ne blokiraju upite koji citaju podatke, oni blokiraju samo upise u isti red. Ekskluzivna zaključavanja na nivou reda mogu se uspostaviti eksplicitno bez stvarne promene reda pomoću komande SELECT FOR UPDATE.

Deljeno zaključavanje na nivou reda može se uspostaviti pomoću komande SELECT FOR SHARE. Deljeno zaključavanje ne sprečava druge transakcije da steknu isto deljeno zaključavanje. Međutim, nijednoj transakciji nije dozvoljeno da ažurira, briše ili ekskluzivno zaključa red za koji bilo koja druga transakcija poseduje deljeno zaključavanje.

Zaključavanje na nivou reda u Postgresu 9.3 i 9.4

U Postgresu 9.3 i 9.4 postoje četiri vrste zaključavanja na nivou reda:

FOR UPDATE – Ovaj režim dovodi do toga da red koji je učitan pomoću SELECT-a bude zaključan radi ažuriranja. Ovo sprečava njegovo zaključavanje, modifikovanje ili brisanje od strane drugih transakcija. Drugim rečima, druge transakcije koje pokušaju da izvrše UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR NO KEY UPDATE, SELECT FOR SHARE ili SELECT FOR KEY SHARE ovih redova, biće blokirane.

Ovaj režim se takođe uspostavlja pomoću DELETE-a za red, kao i pomoću UPDATE-a nekih kolona (trenutno skup kolona koje se uzimaju u obzir su one koje poseduju jedinstveni indeks koji se može koristiti kao strani ključ – ali to se može promeniti u budućnosti).

FOR NO KEY UPDATE – Ovaj režim je sličan kao FOR UPDATE ali je slabiji – on neće blokirati SELECT FOR KEY SHARE režim zaključavanja. Uspostavlja se pomoću UPDATE komande koja ne uspostavlja FOR UPDATE zaključavanje.

FOR SHARE – Ovaj režim je sličan kao FOR NO KEY UPDATE osim što uspostavlja deljeno zaključavanje (a ne ekskluzivno). Deljeno zaključavanje blokira druge transakcije da izvrše UPDATE, DELETE, SELECT FOR UPDATE ili SELECT FOR NO KEY UPDATE ovih redova, ali ih ne sprečava da izvrše SELECT FOR SHARE ili SELECT FOR KEY SHARE.

FOR KEY SHARE – Deluje slično kao FOR SHARE, osim što je zaključavanje slabije: SELECT FOR UPDATE je blokiran, ali ne i SELECT FOR NO KEY UPDATE. Key-shared zaključavanje blokira druge transakcije da izvrše DELETE ili bilo koji UPDATE koji menja ključne vrednosti, ali ne sprečava bilo koji drugi UPDATE, niti SELECT FOR NO KEY UPDATE, SELECT FOR SHARE, ili SELECT FOR KEY SHARE.

Konflikti zaključavanja na nivou reda:

Screen Shot 2016-08-08 at 10.03.34

Neobavezno (advisory) zaključavanje

Postgres obezbeđuje sredstva za kreiranje zaključavanja koji imaju značenja definisana aplikacijom. Oni se nazivaju neobavezno zaključavanje, jer sistem ne nameće njihovu upotrebu – na aplikaciji je da ih pravilno iskoristi.

Postoje dva načina za uspostavljanje neobaveznih zaključavanja u Postgresu: na nivou sesije ili na nivou transakcije.

Jednom uspostavljeno na nivou sesije, neobavezno zaključavanje se čuva sve dok se eksplicitno ne oslobodi ili dok se sesija ne završi.

Za razliku od zahteva za standardnim zaključavanjem, zahtevi za neobaveznim zaključavanjem na nivou sesije ne poštuju semantiku transakcije: zaključavanje uspostavljeno tokom transakcije koja je kasnije vraćena u prvobitno stanje i dalje će se zadržati posle vraćanja, a isto tako će i otključavanje biti efektivno čak iako pozivna transakcija ne uspe kasnije.

Zaključavanje se može uspostaviti više puta putem svog procesa posedovanja; za svaki završeni zahtev za zaključavanjem mora postojati odgovarajući zahtev za otključavanjem pre nego što se zaključavanje oslobodi.

Zahtevi za zaključavanjem na nivou transakcije, sa druge strane, deluju više kao obični zahtevi za zaključavanjem: oni se automatski oslobađaju na kraju transakcije i ne postoji eksplicitna operacija otključavanja. Ovo postupanje je često pogodnije od postupanja na nivou sesije za kratkoročnu upotrebu neobaveznog zaključavanja.

Zahtevi za zaključavanjem na nivou sesije i na nivou transakcije za isti identifikator neobaveznog zaključavanja blokiraće jedni druge na očekivani način.

Ako sesija već poseduje određeno neobavezno zaključavanje, njeni dodatni zahtevi će uvek biti uspešni, čak iako ostale sesije čekaju na zaključavanje; ova tvrdnja je istinita bez obzira na to da li je reč o postojećem vlasništvu nad zaključavanjem i novom zahtevu na nivou sesije ili na nivou transakcije. Kompletan spisak funkcija za manipulaciju neobaveznim zaključavanjem može se naći u okviru dokumentacije.

Slede neki primeri uspostavlja ekskluzivnih neobaveznih zaključavanja na nivou transakcije (pg_locks je sistemski prikaz, opisan dalje u ovom postu. On sadrži informacije o transakciji koja ima zaključavanje na nivou tabele i neobavezno zaključavanje.):

Otvorite prvu psql sesiju, otvorite transakciju i steknite neobavezno zaključavanje:

Screen Shot 2016-08-08 at 10.05.48

Sada otvorite drugu psql sesiju i pokrenite novu transakciju sa istim neobaveznim zaključavanjem:

Screen Shot 2016-08-08 at 10.06.52

U trećoj psql sesiji možemo videti kako ovo zaključavanje izgleda:

Screen Shot 2016-08-08 at 10.07.39

Screen Shot 2016-08-08 at 10.08.55

Takođe možemo načiniti poziv prema neblokirajućim metodama radi uspostavlja zaključavanja. Ove metode će pokušati da steknu zaključavanje i vratiće se kao tačne (ako uspeju u tome) ili netačne (ako zaključavanje ne može da se stekne).

Slika_07

Slika_08

Slika_09

a sada malo vežbe..

Praćenje zaključavanja

Bazna stanica za praćenje zaključavanja u posedu svih aktivnih transakcija je sistemski prikaz pg_locks.

Ovaj prikaz sadrži po jedan red za aktivan objekat koji se može zaključati, traženi režim zaključavanja i relevantnu transakciju. Ono što je veoma važno znati je da pg_locks sadrži informacije o zaključavanjima koje se prate u memoriji, tako da ne prikazuje zaključavanje na nivou reda!

Ovaj prikaz prikazuje zaključavanje na nivou tabele i neobavezno zaključavanje. Ako transakcija čeka zaključavanje na nivou reda, obično će se pojaviti u prikazu kao čekanje na identifikaciju trajne transakcije sadašnjeg vlasnika tog zaključavanja za red. To je nešto što čini otklanjanje grešaka zaključavanja na nivou reda mnogo težim.

U stvari, nećete videti zaključavanja na nivou reda nigde, dok neko ne blokira transakciju koja ga poseduje (tada ćete videti torku koja je zaključana u pg_locks tabeli). pg_locks nije baš pregledan prikaz (nije baš korisnički nastrojen) tako da ćemo napraviti naš prikaz koji će prikazati zaključavanje informacija koje je prihvatljivije za nas:

Slika_10

Sada imamo igralište za neke eksperimente…

Jednostavni primeri

Kreirajmo neke tabele za vežbu:

Slika_11

i pokušajmo neke jednostavne transakcije, da bismo videli kako izgleda zaključavanje:

Slika_12

Vidimo da smo nakon ubacivanja jednog reda u parent tabelu, stekli ROW EXCLUSIVE zaključavanje za parent tabelu. parent_id_seq je sekvenca za parent primarni ključ. Kako je izabran ovaj odnos (kao tabela) stekli smo ACCESS SHARE zaključavanje za njega.

Pokušajmo da unesemo nešto u child tabelu:

Slika_13

Sada je situacija interesantnija. Možemo videti dodatno ROW SHARE zaključavanje za parent tabelu. Ono što ne možemo da vidimo je da se ovim INSERT-om steklo i deljeno zaključavanje na nivou reda navedenom u matičnoj tabeli. To možemo da vidimo izvršenjem dve transakcije paralelno:

Slika_14 Slika_15

Sada otvorite treću sesiju da vidimo kako naše opcije za zaključavanje izgledaju:

Slika_16

DELETE upit je blokiran, čeka da se transakcija 1 završi. Možemo videti da je uspostavljeno zaključavanje na torci 1. Ali zašto je DELETE upit blokiran ako se vidi da su sve opcije za zaključavanje odobrene?

Ove dve transakcije nisu sinhronizovane za zaključavanje u bilo kom odnosu. U stvari, ako jedna transakcija ima zaključavanje za neki red a druga transakcija traži to zaključavanje, druga transakcija će onda pokušati da stekne SHARE zaključavanje za transakciju koja poseduje to zaključavanje.

Kada se prva transakcija završi, druga transakcija će se nastaviti. To je moguće zato što svaka transakcija ima EXCLUSIVE zaključavanje za sebe. Možemo videti da je to pg_locks prikaz, a evo ga i izlaz (samo deo koji je bitan):

Slika_17

Vidimo da transakcije 707 (pid 9000) i 708 (pid 4428) imaju EXCLUSIVE zaključavanje za svoje transakcione identifikacije i da transakcija 708 pokušava da stekne SHARE zaključavanje za transakciju 707.

Sledi najinteresantniji primer. Možemo da se igramo sa ažuriranjem child tabele, ali bez stvarnog menjanja bilo čega što je u korelaciji sa parent tabelom (u ovom slučaju parent_id column).

Slika_18

Ovo je veoma interesantno i najvažnije da se zapamti. Kao što možemo da vidimo, izvršavamo UPDATE upit koji ne dotiče ništa što se odnosi na parent tabelu. Posle prvog izvođenja možemo videti da ovde samo child tabela sadrži zaključavanja na nivou tabele. Isto je po pitanju zaključavanja na nivou reda.

Samo je red child tabele zaključan za UPDATE. Ovo je optimizacija koja postoji u Postgresu.

Ako menadžer za zaključavanje može da zaključi iz prvog upita da se strani ključ ne menja (da se ne pominje u upitu za ažuriranje ili je postavljen na istu vrednost), on neće zaključati parent tabelu. Ali u drugom upitu će se ponašati kao što je opisano u dokumentaciji (zaključaće parent tabelu u ROW SHARE režimu zaključavanja a navedeni red u FOR SHARE režimu).

To je veoma opasno jer može dovesti do najopasnije uzajamne blokade transakcija (deadlock) koja se izuzetno teško pronalazi. Možemo koristiti eksplicitno zaključavanje na početku transakcije kako bismo ovo izbegli. Postupak se razlikuje za Postgres 9.1 i 9.2 sa jedne strane, i 9.3 i 9.4 sa druge.

Razlika se odnosi na zaključavanje na nivou reda. Postgres 9.3 i 9.4 će uspostaviti slabiji FOR KEY SHARE režim zaključavanja za parent tabelu. Ovaj režim zaključavanja nije u konfliktu sa FOR NO KEY UPDATE režimom tako da se mogu uspostaviti paralelno od strane dve transakcije. Ovo je mnogo bolje, tako da postoji manja verovatnoća za uzajamnom blokadom transakcija u 9.3 i 9.4.

Najbolji način da se spreči uzajamna blokada transakcija, u slučaju kada smo svesni da do toga može doći između dve transakcije, je uspostavljanje zaključavanja na nivou reda prema nekom redosledu (na primer naredbom ORDER BY primarni ključ) tako što će se prvo uspostaviti najrestriktivnije opcije za zaključavanje. Imajući u vidu Postgres optimizaciju putem zaključavanja koje je opisano u prethodnom pasusu, eksplicitno zaključavanje je ponekad jedini način da se izbegne uzajamna blokada transakcija.

Kada dođe do uzajamne blokade transakcija u Postgresu, to će se rešiti obustavjanjem jedne od transakcija koja je uključena u uzajamnu blokadu. Koja će tačno transakcija biti obustavljena je teško predvideti i ne treba se na to oslanjati.

Zaključna razmatranja

Veoma je važno imati na umu kako funkcioniše zaključavanje u Postgresu. Uzajamne blokade transakcija u veoma konkurentnim sredinama su verovatno neizbežne, ali je važno da znate kako da ih otkrijete, pratite i rešite.

Čak iako radite sve „prema knjizi“, nećete uvek rešiti sve moguće probleme u vezi sa zaključavanjem, ali ćete ih smanjiti i time ih lakše ispraviti. Zaključavanja na nivou tabele se mogu videti putem pg_locks sistemskog prikaza, ali ne i zaključavanja na nivou reda, što čini otklanjanje grešaka zaključavanja mnogo težim, tako da se nadam da će to biti omogućeno u budućim verzijama Postgresa.

Kompanija Nordeus nudi tromesečnu plaćenu praksu za poziciju Backend Developera.

backend-praksa-web big