Press ESC to close

PostgreSQL MVCC (MULTI VERSION CONCURRENCY CONTROL)

MVCC eş zamanlı olarak read (okuma) write (yazma) işlemlerinin birbirlerini engellemeden ve tutarlı bir şekilde yapılabilmesi için geliştirilmiştir.   MVCC postgresql in  eş zamanlı okuma ve yazma yapabilmesi için kullandığı veritabanı modelidir.  Bu model  sayesinde read işlemleri write işlemlerini ,write işlemleri read işlemlerini bloklamadan (Lock) işlem yapabilmelerine olanak sağlayan bir mekanizmadır.  Tamamen bloklama yok Lock hiç olmayacak) demek imkansızdır, zaten tutarlılık açısından bunun mutlaka olması gerekmektedir (ACID). Contention yaratabilecek tek kullanım durumu, iki eşzamanlı transactionın aynı kaydı değiştirmeye çalışmasıdır, çünkü bir kez değiştirildiğinde, bu kaydı değiştiren transaction commit edene veya rollback edene kadar ilgili satırlar her zaman lock alacaktır.

Eşzamanlı read ve write işlemlerinin birbirini engellememesi, ve yine iş zamanlı transactionların tutarlılığı sağlıklı bir şekilde lock mekanizmasının yönetilmesi MVCC ile sağlanmaktadır, PostgreSQL bunu ise Transaction ID bilgilerini kullanarak yapmaktadır.

Transaction ID nedir?

Transaction ID bilgisi veritabanı işlemi yapılırken, işlemleri kontrol etmek adına VTYS programı tarafından üretilen unique bir değerdi (txid)r. Bilginin 32-bit olarak tutulmasından dolayı 4,294,967,295 farklı değer üretilebilmektedir. Postgresql MVCC modeline göre her satır ekleme işlemi sırasında iki farklı bilgi daha kayıt altına alınmaktadır, bu versiyonlamalar tabloların veri yapısında depolanır ve bunlar iki sütun olarak sıralanmaktadır.

Xmin : Insert Transaction ID bilgisi 

Xmax: Delete Transaction ID bilgisi 

O anki geçerli Transaction ID

postgres=# select txid_current(); txid_current--------------         100

MVCC kullanılan veritabanlarında, bir satır güncellendiğinde veya silindiğinde (update , delete), orijinal veri öğesinin üzerine yeni veriler yazmaz veya eski veri doğrudan silinmez, bunun yerine veri öğesinin daha yeni bir sürümünü oluşturur. Bu yaklaşım güncellenen yada silinen bir verinin birden fazla sürümünün oluşmasına yol açar. Her transactionın görebileceği sürüm, uygulanan izolasyon seviyesine bağlı olarak değişir. Okuma ve yazma işlemleri böylece kilitlemeye gerek kalmadan birbirinden izole edilir. MVCC, eskiyen ve asla okunmayacak sürümlerin nasıl kaldırılacağına dair bir zorluk getirmektedir, bu durum ise eski sürümleri periyodik olarak taranması ve silinmesi için bir işlem uygulanır. PostgreSQL, VACUUM süreciyle bu yaklaşımı benimser.

Konuyu bir Örnek Üzerinden anlamaya çalışalım;

mvcc_test adında bir tablo oluşturalım

postgres=# create table mvcc_test (id int, name varchar(50),surname varchar(50));
CREATE TABLE

postgres=# insert into mvcc_test values (1,'Faruk','Cevik');
INSERT 0 1

postgres=# insert into mvcc_test values (2,'Betul','Cevik');
INSERT 0 1

2 Adet session açalım ve psql üzerinden bağlanarak autocommit özelliğini kapatalım

Session A

Session B

Session A da yapılan işlemler; bir kayıt daha ekliyoruz ve mevcut bir kaydıda güncelliyoruz, ancak commit etmiyoruz, sonrasında current txid ye bakalım;

postgres=# \set AUTOCOMMIT OFF
postgres=*# select txid_current();
 txid_current
--------------         
1400

postgres=# insert into mvcc_test values (3,'Zeynep','Cevik');
INSERT 0 1

postgres=*# update mvcc_test set  name ='Aysegul' where id=2;
UPDATE 1

postgres=*# select xmin, xmax, id, name,surname from mvcc_test ; 

xmin | xmax | id |  name   | surname
------+------+----+---------+--------- 
1408 |    0 |  1 | Faruk   | Cevik 
1410 |    0 |  3 | Zeynep  | Cevik 
1410 |    0 |  2 | Aysegul | Cevik
(3 rows)

postgres=*# select txid_current(); 
txid_current
--------------         
1400

Session A üzerinde tabloda 3 Kayıt var,

Session B üzerinden bu tabloyu ve txid mizi sorgulayalım;

postgres=# select xmin, xmax, id, name,surname from mvcc_test ; 
xmin | xmax | id | name  | surname
------+------+----+-------+--------- 
1408 |    0 |  1 | Faruk | Cevik 
1409 | 1410 |  2 | Betul | Cevik
(2 rows)

postgres=# select txid_current(); 

txid_current
--------------         
1411

Session B üzerinden sorguladığımızda session A da yapılan hiçbir değişikliği görmüyoruz, yani MVCC ye göre eski versiyonları görüyoruz, çünkü Session A da transaction henüz commit olmadı. Değişiklikler Commit olmadı ancak halen select sorgularımız çalışıyor herhangi bir Lock da yaşamadık.

Session B de Insert, Update işlemleri yapmak isteyelim;

postgres=# insert into  mvcc_test values (4,'Yusuf','Cevik');
INSERT 0 1
postgres=# update mvcc_test set  name ='Betus' where id=2;

ERROR:  canceling statement due to user request
CONTEXT:  while updating tuple (0,2) in relation "mvcc_test

Session B de yeni bir kayıt eklemek istediğimizde herhangi bir problem olmadı çünkü row bazlı olarak bir problem yok yeni kayıt eklendi, A session da henüz commit olmayan id=2 kaydı burada da bir değişiklik yapılmak istendiğinde Lock oluştu ve psql komut satırı diğer tarafın commit veya rollback olmasını bekleyecek, bu yüzden bu işlemi cancel yaptık. 

Yukarıdaki Örnekte LOCK nasıl oluyor gördük, bu Lock ları nasıl tespit ederiz kim kimi Lock yapıyor ona bakalım

Session A da bir delete işlemi yapalım ve commit etmeyelim yine ve Session A için Prosess ID PID değerine bakalım;

postgres=*# delete from mvcc_test where id=4;
DELETE 2
postgres=*# select pg_backend_pid(); pg_backend_pid
----------------        
2149241
(1 row)

Session B da id=4 olan kaydı Update etmek isteyelim;

postgres=# select pg_backend_pid();
 pg_backend_pid
----------------        
2148180(1 row)

postgres=# update mvcc_test set name='Betus' where id=4;

Lock oluştu ve B session ı, Session A nın bu satırlar üzerindeki işlemini bitirmesini bekliyor. Başka bir session üzerinden Lock durumunu kontrol edelim;

postgres=# select locktype, mode, granted, pid, pg_blocking_pids(pid) as blocked from pg_locks where relation='mvcc_test'::regclass; 
locktype |       mode       | granted |   pid   |  blocked
----------+------------------+---------+---------+-----------
relation | AccessShareLock  | t       | 2149241 | {} 
relation | RowExclusiveLock | t       | 2149241 | {} 
relation | RowExclusiveLock | t       | 2148180 | {2149241} 
tuple    | ExclusiveLock    | t       | 2148180 | {2149241}
(4 rows)


##Diğer Bir Sql İle Bakalım

postgres=# select pid, wait_event_type, wait_event, pg_blocking_pids(pid) from pg_stat_activity where wait_event_type='Lock';   

pid   | wait_event_type |  wait_event   | pg_blocking_pids
---------+-----------------+---------------+------------------ 
2148180 | Lock            | transactionid | {2149241}
(1 row)

A session commit ederek tekrar sorgulayalım;

Session A
postgres=*# commit;
COMMIT

##Diğer Sessionlardan Lock kontrolü yapalım

postgres=# select locktype, mode, granted, pid, pg_blocking_pids(pid) as blocked from pg_locks where relation='mvcc_test'::regclass; 
locktype | mode | granted | pid | blocked
----------+------+---------+-----+---------
(0 rows)

#################

postgres=# select pid,wait_event_type,wait_event,pg_blocking_pids(pid) from pg_stat_activity where wait_event_type='Lock'; 
pid | wait_event_type | wait_event | pg_blocking_pids
-----+-----------------+------------+------------------
(0 rows)

Lock sorguları DBA ler açısından önemlidir ve daha anlaşılır bir Lock sorgusunu view haline getirerek Lock kontrolünü view üzerinden yapabiliriz. Aşağıda örnek bir Lock view sorgusu verilmiştir. 

create view  my_lock_query as
SELECT blocked_locks.pid     AS blocked_pid,
         blocked_activity.usename  AS blocked_user,
         blocking_locks.pid     AS blocking_pid,
         blocking_activity.usename AS blocking_user,
         blocked_activity.query    AS blocked_statement,
         blocking_activity.query   AS blocking_statement,
         blocked_activity.application_name AS blocked_application,
         blocking_activity.application_name AS blocking_application
   FROM  pg_catalog.pg_locks         blocked_locks
    JOIN pg_catalog.pg_stat_activity blocked_activity  ON blocked_activity.pid = blocked_locks.pid
    JOIN pg_catalog.pg_locks         blocking_locks
        ON blocking_locks.locktype = blocked_locks.locktype
        AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
        AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
        AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
        AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
        AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
        AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
        AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
        AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
        AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
        AND blocking_locks.pid != blocked_locks.pid
    JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
   WHERE NOT blocked_locks.GRANTED;

View ı sorgulayalım;

postgres=# select * from my_lock_query ;
-[ RECORD 1 ]--------+------------------------------------------------
blocked_pid          | 2148180
blocked_user         | postgres
blocking_pid         | 2149241
blocking_user        | postgres
blocked_statement    | update mvcc_test set  name='frkcvk' where id=1;
blocking_statement   | delete from mvcc_test where id=1;
blocked_application  | psql
blocking_application | psql

Lock konusu sadece update ve delete ten ibaret değildir, bir çok Lock çeşidi mevcuttur,, ayrıca Vacuum full işlemi , index create , reindex işlemleri de Lock a sebep olur ayrıca memory üzerinde oluşan bazı lock çeşitleri de vardır. Lock olan bir tablo vacuum full işlemi tamamlanamaz taaki tablo üzerindeki Lock kaldırılana kadar.

Deadlock

2 session birbirini blokladığında (Lock) Deadlock durumu oluşacaktır, deadlock tespit edildiğinde session lardan biri deadlock_timeout kadar bekledikten sonra otomatik olarak rollback olacaktır

Örnek

Session A

postgres=# update mvcc_test set name='Zeyno' where id=3;
UPDATE 1

Session B

postgres=# update mvcc_test set name='Ayse' where id=2;
UPDATE 1

Session A
postgres=*# update mvcc_test set name='A' where id=2;

Session B

postgres=*# update mvcc_test set name='Z' where id=3;
ERROR:  deadlock detected
DETAIL:  Process 2149241 waits for ShareLock on transaction 1427; blocked by process 2148180.
Process 2148180 waits for ShareLock on transaction 1428; blocked by process 2149241.
HINT:  See server log for query details.
CONTEXT:  while updating tuple (0,3) in relation "mvcc_test"


Session A

postgres=*# select * from mvcc_test ;
 id | name  | surname
----+-------+---------
  3 | Zeyno | Cevik
  2 | A     | Cevik
(2 rows)

deadlock_timeout; Lock tespit edildikten sonra bu süre kadar beklenir ve deadlock bu süre sonunda session lardan biri rollback yapılarak giderilir. deadlock hataları alınıyorsa genelde uygulama tasarımının gözden geçirilmesi önerilmektedir.

lock_timeout; Varsayılan 0 dır yani kapalıdır, milisaniye cinsinden lock ların ne kadar bekleyeceğini belirtir bu süre sonunda blocknan transaction sonlandırılır.

log_lock_waits; Aktif edilmesi durumunda deadlock_timeout süresinden daha uzun süren sorgular log dosyasına yazılacaktır.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir