POSTGRESQL’imin VACUUM’u

Furkan Şahin Kulaksız
10 min readDec 9, 2024

--

GORSELDEKI QR KODU KULLANARAK ILETISIM KANALLARIMDAN BANA ULASABILIRSINIZ

(Evren Tan’a selam, çalışmaya devam)

“Bu yazı, Vacuum üzerine yaptığım çalışmalardan aldığım notların tamamını içerir.”

“PostgreSQL’de VACUUM işlemi, veritabanındaki ölü satırları temizlemek ve disk alanını geri kazanmak için kullanılır.”

Bütün yazı yukarıdaki tek bir cümleyi incelemek üzerine olacak. Vacuum’u anlamak için öncesinde bir yapboz misali parçaları teker teker anlayıp sonrasında bütüne gitmemiz gerekiyor.

Tuple

Elimizde bir database var ve bu database’e eklediğimiz kayıtlar ya da database’in row’ları aslında bir tupleoluyor.

Bir tuble 2 bileşenden oluşur.
-> Veri: Tabloya ait sütunlarla ilişkili alanlardaki değerler. Örneğin; id, isim, yaş, email, identityNumber vs vs ….
-> Metadata: Tuple ile ilgili bazı ek bilgiler (örneğin, ne zaman oluşturulduğu, ne zaman silindiği, hangi işlemler tarafından görünür olduğu gibi).

MVCC (Multi-Version Concurrency Control)

PostgreSQL, aynı anda birden fazla transaction’u yürütmek için MVCC adında bir sistemi kullanır. Bu, aynı veriye birden fazla kişi ya da işlem erişmeye çalıştığında onları birbirine karıştırmadan iş yapabilmelerini sağlar.

Database’lerde aynı anda birden fazla işlemi (transaction) sorunsuz gerçekleştirmek için kullanılan bir yöntemdir. Temel amacı, veritabanının performansını artırmak ve aynı veriye erişmeye çalışan transaction’ların birbirlerini engellemeden çalışabilmelerini sağlamaktır.

Örneğin:
==> Ahmet bir tabloya veri ekliyor.
==> Aynı anda Ayşe o tabloyu okuyor.

Ayşe’nin okuma işlemi sırasında Ahmet’in yaptığı değişiklikler henüz tamamlanmamış olabilir. Ancak Ayşe’nin bu durumdan etkilenmemesi için PostgreSQL, Ahmet’in değişikliklerini bitirene kadar Ayşe’ye önceki haliyle veriyi göstermeye devam eder. İşte bu noktada MVCC devreye girer.

MVCC, aynı verinin farklı zamanlardaki versiyonlarını saklayarak, işlemlerin birbirini etkilemeden yürütülmesini sağlar.

Database’in birden fazla transaction’ı aynı anda işlemesini sağlayarak performansı artırır. Aynı veriye erişmeye çalışan transaction’ları engellemek yerine, MVCC her bir entity’nin birden fazla versiyonunun aynı anda var olmasına izin verir ve yalnızca çakışan değişiklikler getiren transaction’ları geri alır.

Bu yöntem, özellikle transaction’ların birbirinden bağımsız entity’leri değiştirdiği veya genellikle veri okuma işlemlerinin yapıldığı durumlarda çok avantajlıdır. Bu tür durumlarda, geçersiz değişiklikler nedeniyle bir transaction’ı geri almak nadiren gereklidir. Ayrıca, transaction’lar birbirini beklemeden bağımsız bir şekilde ilerleyebilir, bu da PostgreSQL’in verilerini nasıl yönettiğinin temel ilkelerinden biridir.

Ancak, bu yaklaşım değişiklikleri biraz daha zor hale getirir. Transaction’ları durdurmak istemediğimizden, hangi değişikliklerin hangi transaction’lara görünür olduğunu bir şekilde kontrol etmemiz gerekir. Bu, t_xmin ve t_xmax alanları ile görünürlük haritalarına dayanarak belirlenir. Bu da veritabanında aynı satırın birden fazla versiyonunun bulunmasına yol açabilir.

Basit Anlatımlarla MVCC

Bir veritabanı aynı anda birden fazla işlem yapmaya çalışırken, bu işlemler bazen aynı veriye erişir.

Örneğin:

==> Ali bir bankadaki hesabında 100 TL olduğunu görüyor.
==> Ayşe aynı hesaba 50 TL yatırıyor.
==> Ali ve Ayşe’nin aynı veriye (Ali’nin hesabındaki para) eriştiğini fark ettiniz mi? Şimdi veritabanı aynı anda iki işlem (Ali’nin bakiyesini kontrol etme ve Ayşe’nin para yatırma işlemi) gerçekleştirmek zorunda. İşte burada MVCC devreye giriyor.

MVCC’nin Çalışma Şekli:
Ali bakiyesini kontrol ettiğinde, veritabanı ona şu anki bakiyeyi (100 TL) gösterir. Bu sırada Ali’nin yaptığı işlem, sadece bir okuma işlemidir. Yani veri üzerinde herhangi bir değişiklik yapmaz.

Ayşe’nin para yatırma işlemi başladığında, veritabanı mevcut bakiyeyi (100 TL) alır ve üzerine 50 TL ekler. Ancak bu yeni durumu hemen Ali’ye göstermez, çünkü Ayşe’nin işlemi tamamlanmadan yeni veri diğer transaction’lara görünmez.

Ali hala 100 TL’yi görmeye devam eder, çünkü MVCC her bir transaction’a “verinin farklı bir versiyonunu” gösterir. Ali’nin gördüğü,
**eski versiyon**dur. Ayşe’nin yaptığı değişiklik ise başka bir versiyonda tutulur ve Ayşe’nin transaction’ı tamamlanana kadar diğer transaction’lara görünmez.

Örnek:
Ali ve Ayşe’nin bankadaki hesaplarına ilişkin bir örnekte, veritabanındaki Ali’nin bakiyesine dair iki farklı versiyon oluşturulur:

Versiyon 1: 100 TL (Ali’nin gördüğü eski versiyon)
Versiyon 2: 150 TL (Ayşe’nin para yatırma işleminin sonucunda oluşan yeni versiyon)
MVCC sayesinde her iki işlem de (Ali’nin bakiyesini kontrol etmesi ve Ayşe’nin para yatırma işlemi) birbirini beklemeden ve engellemeden devam edebilir.

==> t_xminve t_xmax
Veritabanı, hangi transaction’ın hangi veriyi gördüğünü kontrol etmek için iki alan kullanır: t_xmin ve t_xmax.

t_xmin (transaction minimum): Bu alan, verinin hangi transaction tarafından oluşturulduğunu veya güncellendiğini gösterir.
t_xmax (transaction maximum): Bu alan ise verinin hangi transaction tarafından geçersiz kılındığını (örneğin, verinin silindiğini ya da güncellendiğini) gösterir.

Örneğin:

Ayşe, Ali’nin hesabına 50 TL eklediğinde, bu işlem veritabanında yeni bir satır yaratır. Bu satırın t_xmin değeri, Ayşe’nin yaptığı transaction’ı gösterir.
Ali’nin hala 100 TL’yi görmesinin nedeni, Ali’nin transaction’ı başladığında bu yeni satırın t_xmax değeri olmamasıdır, yani bu satır onun transaction’ına görünmezdir.

Bu şekilde, aynı verinin birden fazla versiyonu aynı anda tutulur ve her transaction, verinin kendi “görmesi gereken” versiyonunu görür.

Örnek2:

==> 1. adım:

Yeni bir tablo oluşturuyoruz.

CREATE TABLE test (id INT);
INSERT INTO test VALUES (1);

Bu işlem sonunda tablo şu şekilde görünür.

id
— — —
1

==> 2. adım:

Transaction başlatalım. Ama COMMIT ETMEYELİM.!!!

BEGIN TRANSACTION;
SELECT * FROM test;

Bu database’den dönen kayıt yine aynı şekildedir.

==> 3. adım:

Başka bir güncelleme transasctionu başlatalım. Ama COMMIT ETMİYORUZ.

BEGIN TRANSACTION;
UPDATE test SET id = 2;
SELECT * FROM test;

Bu işlem, veritabanındaki id değerini 1'den 2'ye günceller ve yeni veriyi okuduğumuzda:

id
— — —
2

Ancak, bu transaction’ı da henüz commit etmiyoruz. Bu durumda:

* İlk transaction veriyi hala 1 olarak görüyor çünkü o sırada veritabanında id = 1 olan satır onun için görünür.

* İkinci transaction ise tabloyu güncellediği için id = 2'yi görüyor.

İki farklı transaction’ın aynı satırı farklı şekilde görmesinin sebebi MVCC’nin çalışma prensibidir. Veritabanı aynı satırın farklı versiyonlarını oluşturarak transaction’ların birbirinden bağımsız hareket etmesine olanak tanır.

Her satır, hangi transaction tarafından oluşturulduğunu ve güncellendiğini gösteren iki alan içerir: t_xmin ve t_xmax.

t_xmin: Bu satırın hangi transaction tarafından oluşturulduğunu gösterir.
t_xmax: Bu satırın hangi transaction tarafından geçersiz kılındığını veya güncellendiğini gösterir. Eğer t_xmax = 0 ise, bu satır hala geçerli demektir.

* Veritabanına ilk id = 1 değeri eklendiğinde, satırın t_xmin değeri transaction numarası olur, ve t_xmax = 0 olarak kalır (yani bu satır hala geçerli).

* İkinci transaction başladığında ve id = 2 olarak güncellendiğinde:

İlk satırın (id = 1) t_xmax değeri, güncellemeyi yapan transaction numarasıyla güncellenir.
Yeni bir satır oluşturulur ve bu satırda id = 2 olur, t_xmin değeri ise güncelleme yapan transaction’ı gösterir.

Böylece veritabanı aynı satırın iki farklı versiyonunu tutar, biri id = 1, diğeri id = 2. Transaction’lar bu versiyonlara göre işlem yapar.

İlk transaction, id = 1 versiyonunu görür.
İkinci transaction ise id = 2 versiyonunu görür.

Dead Tuple
— — — — — — —

Dead tuple, bir transaction tamamlandıktan sonra görünmeyen ve artık hiçbir transaction tarafından kullanılmayan satırlardır.

→ Örnek 1: İkinci Transaction İptal Edilirse (ROLLBACK)
Eğer ikinci transaction iptal edilirse (rollback yapılırsa), id = 2 olan satır artık geçerli değildir çünkü bu işlem geri alınır. Bu durumda:

İlk transaction hala id = 1 olan satırı görecektir.
id = 2 olan satır veritabanında dead tuple (ölü satır) olarak kalır. Çünkü bu satır, hiçbir transaction tarafından görünmez ve kullanılamaz hale gelir.

→ Örnek 2: İkinci Transaction Commit Edilirse
Eğer ikinci transaction commit edilirse, bu kez:

id = 1 olan eski satır dead tuple haline gelir. Çünkü artık hiçbir transaction bu satırı görmeyecektir; herkes yeni id = 2 olan satırı görecektir.

Yani, dead tuple’lar işlemden sonra görünmeyen ve kullanılamayan satırlardır. Güncelleme veya silme işlemlerinden sonra veritabanında bu tür satırlar oluşabilir.

Dead tuple’lar veritabanında yer kaplar ve performansı olumsuz etkiler. Veritabanı her seferinde bu ölü satırları okumak zorunda kalırsa, işlemler yavaşlar. Bu yüzden dead tuple’lar veritabanından tamamen temizlenmelidir. Ancak bu işlem transaction’lar sona erdiğinde otomatik olarak yapılmaz. Bunun yerine vacuum adı verilen bir işlemle veritabanı düzenli olarak dead tuple’ları temizler.

Her satır (tuple), bir kayıt ya da veri parçasıdır. Bir tuple, bir veriyi temsil eder ve bu veri zamanla değiştirilebilir.

PostgreSQL, her değişiklik yapıldığında eski versiyonunu tamamen silmez, çünkü o anda bu eski versiyonu kullanan başka bir işlem olabilir. Bunun yerine, verinin her değiştirildiğinde yeni bir kopyasını (versiyonunu) saklar.

Yani:
- Bir tuple’ın eski versiyonunu bir işlem kullanırken,
- Bir başka işlem aynı tuple’ın yeni versiyonunu kullanabilir.

Bu versiyonlar, farklı işlemlerin aynı anda veriyi nasıl gördüğünü belirler. Buna snapshot(anlık görüntü)denir.

Bu sistemin bir yan etkisi olarak, PostgreSQL sürekli aynı verinin farklı versiyonlarını sakladığı için zamanla bu versiyonlar birikir ve gereksiz depolama alanı kullanılır. Örneğin, bir kaydı 10 kez güncellediysen, her güncellemenin ardından eski versiyonlar da saklandığı için veritabanında 10 farklı kopya olabilir. Zamanla bu birikmiş versiyonlar disk alanını doldurmaya başlar.

Bir tuple, artık o versiyona referans verebilecek aktif işlemler kalmadığında (yani snapshot’larında o tuple versiyonunu içermediğinde) algılanamaz (görünmez) hale gelir. Algılanamaz bir tuple genellikle ölü tuple olarak adlandırılır, bu da onun artık veritabanı yaşam döngüsünde gerekli olmadığını gösterir. İşte bu ölü tuple’ların silinmesi işlemi de Vacuum’a ait.

VACUUM, disk alanını geri kazanıp serbest bırakması gerektiği için, I/O açısından yoğun bir işlem olabilir ve bu nedenle müdahaleci bir işlem olabilir. Bu sebeple, VACUUM’un el ile manuel olarak çok sık çalıştırılması önerilmez. PostgreSQL, mevcut veritabanı etkinliğine bağlı olarak VACUUM’u bizler için çalıştırabilen autovacuum adlı bir backgorund taskı da sağlar.

Manuel Vacuum
— — — — — — — — —

Manuel VACUUM, tek bir tablo, bir tablonun sütunlarının bir alt kümesi veya tüm veritabanı üzerinde çalıştırılabilir ve söz dizimi aşağıdaki gibidir:

VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ tablo_ve_sütunlar [, …] ]

Üç ana VACUUM versiyonu vardır ve bunlar giderek daha agresif veri yeniden düzenlemesi gerçekleştirir:

Normal Vacuum: (default), mikro-alan geri kazanımı yapar, yani ölü tuple versiyonlarını atar ancak tabloyu yeniden düzenlemez, bu yüzden nihai sonuç olarak herhangi bir alan geri kazanılmaz.

Vacuum Full: tüm tabloyu yeniden yazar, ölü tuple’ları atar ve parçalanmayı giderir, böylece agresif bir şekilde disk alanı geri kazanır.

Vacuum Freeze: zaten konsolide edilmiş tuple’ları dondurulmuş olarak işaretler ve bu sayede xid wraparound (xid çevrimi) sorununu önler. Öte yandan, VACUUM FULL yalnızca tablodaki boş alanı serbest bırakmakla kalmaz, aynı zamanda bu alanı geri kazanarak tabloyu minimum boyutuna indirir.

VACUUM, bir transaction içinde, bir fonksiyon ya da prosedür içerisinde çalıştırılamaz. Ek seçenekler olan VERBOSE ve ANALYZE sırasıyla ayrıntılı çıktı sağlar ve tablonun içeriği hakkında istatistiksel bir güncelleme yapar (bu performans kazancı için faydalıdır)

genel bir kural olarak, normal VACUUM depolama alanını serbest bırakmaz ve VACUUM FULL’e göre çok daha az agresiftir. Buna karşılık, VACUUM FULL disk alanını serbest bırakır. Ancak, normal VACUUM’un çok küçük bir depolama alanı geri kazanabileceği özel bir durum vardır: eğer son sayfadaki tüm tuple’lar ölü ise, sayfa kendiliğinden serbest bırakılır.

Genellikle, VACUUM’u elle çalıştırmazsınız çünkü PostgreSQL, parçalanmayı kontrol altında tutmak için otomatik vacuuming ile çok daha iyi bir yaklaşım sağlar.

Auto Vacuum
— — — — — — —

PostgreSQL 8.4'ten bu yana, autovacuum adlı bir background job’u bulunmaktadır ve bu job, sistem yöneticisi adına VACUUM işlemlerini yürütmekle sorumludur. Fikir şudur: VACUUM, I/O açısından yoğun bir işlem olduğundan, background job küçük mikro-vacuum işlemlerini gerçekleştirerek normal veritabanı faaliyetlerine müdahale etmez.

Genellikle, autovacuum hakkında endişelenmenize gerek yoktur, çünkü varsayılan olarak etkinleştirilmiştir ve birçok senaryoda işe yarayan genel ayarlarla birlikte gelir; ancak, PostgreSQL’de hemen her şeyde olduğu gibi, otomatik vacuuming işlemini ince ayarlarla optimize etmek için özel ayarlar kullanabilirsiniz. İyi bir autovacuum yapılandırmasına sahip bir sistem genellikle manuel VACUUM’a ihtiyaç duymaz ve genellikle manuel VACUUM’un özellikleri, autovacuum’un daha sık çalışması gerektiğini gösterir.

Otomatik vacuum, autovacuum worker adı verilen bir grup background process çalışır. Her bir worker, üzerinde çalışacağı bir veritabanına atanır; işlem tamamlandığında worker sona erer. PostgreSQL rutin olarak yeni autovacuum worker işlemleri başlatır, böylece cluster’daki her veritabanı (ve tablo) otomatik olarak vacuum işlemine tabi tutulur. Ancak PostgreSQL, veritabanı yöneticisine bu işlem başlatma etkinliğinin davranışını dikkatlice ayarlama imkanı verir: autovacuum_max_workers yapılandırma ayarı, aynı anda çalışabilecek en fazla aktif işlem sayısını belirler.

Autovacuum worker üç ana faaliyeti gerçekleştirir:

Normal VACUUM işlemini veri tabloları üzerinde yürütür, parçalanmayı azaltmak ve yeni tuple versiyonlarını işlemek için sürekli olarak yeni alan ayırmak amacıyla çalışır.

Kullanıcı tablolarında depolanan verilerin miktarı ve kalitesi hakkında sistem istatistiklerini günceller, bu da manuel bir ANALYZE işleminin yaptığı gibi çalışır. Bu, sorgu executor’un verilere erişmek için en iyi planı seçmesini sağlamak açısından çok faydalıdır; ayrıca gerekli olduğunda tuple freeze işlemi yaparak xid wraparound sorununu önler.

Gerektiğinde tuple freeze işlemini yaparak xid wraparound sorununu önler.

Autovacuum varsayılan olarak açık durumdadır, ancak isterseniz her zaman kapatabilirsiniz, ki bu genellikle mantıklı değildir; genellikle autovacuum’un daha fazla çalışmasına ihtiyaç duyarsınız, daha az değil. Ancak, autovacuum kapalı olsa bile, xid wraparound sorununu önlemek için acil durum autovacuum işlemi başlatılabileceğini unutmamak önemlidir.

Başka bir deyişle, PostgreSQL, yanlış yapılandırmış olsanız bile çalışır durumda kalmak için çok çaba sarf eder!

Autovacuum için temel ayarlar, $PGDATA/postgresql.conf yapılandırma dosyasından veya pg_settings katalogundan incelenebilir. En önemli yapılandırma parametreleri şunlardır:

autovacuum: autovacuum arka plan mekanizmasını etkinleştirir veya devre dışı bırakır. Autovacuum’u kapalı tutmanın deneyler dışında bir nedeni yoktur.

autovacuum_vacuum_threshold: autovacuum’un bir tabloda çalışmaya başlamadan önce kaç yeni tuple versiyonunun izin verileceğini belirtir. Buradaki fikir, tabloda sadece az sayıda tuple değişmişse autovacuum’un tetiklenmesini istemememizdir, çünkü bu durum I/O yükü oluşturacak, ancak etkili bir kazanç sağlamayacaktır. Varsayılan olarak bu parametre 50 tuple olarak ayarlanmıştır, yani tablolarınızdaki değişiklikler en az 50 yeni tuple versiyonu üretmedikçe autovacuum tetiklenmeyecektir.

autovacuum_vacuum_scale_factor: bir tabloda autovacuum’un çalışması için gerekli olan değişmiş tuple’ların yüzdesini belirtir. Fikir, tablo büyüdükçe autovacuum’un daha fazla ölü tuple birikene kadar beklemesidir. Varsayılan kurulumda bu ayar 0.2'dir, yani autovacuum, tuple’ların en az %20'si ölü olarak işaretlendiğinde tetiklenecektir.

autovacuum_cost_limit: arka plan işleminin kendisini durdurması ve daha sonra devam etmesi gereken maksimum sınırı ölçen bir değerdir.

autovacuum_cost_delay: autovacuum’un diğer veritabanı faaliyetlerine müdahale etmemesi için kaç milisaniye (on milisaniyelik katlar halinde) askıya alınacağını belirtir. Askıya alma işlemi, yalnızca maliyet gecikmesi sınırına ulaşıldığında gerçekleştirilir.

Özetle, autovacuum’un faaliyeti şöyle işler: Veritabanındaki her tabloyu tarar ve değişen tuple sayısı

autovacuum_vacuum_threshold + (tablodaki tuple sayısı * autovacuum_vacuum_scale_factor)

formülünden büyükse, autovacuum süreci aktif hale gelir. Daha sonra tablodaki işi ölçerek vacuum işlemi yapar. Yapılan iş miktarı autovacuum_cost_limit değerine ulaştığında, süreç kendisini autovacuum_cost_delay milisaniye boyunca askıya alır, ardından devam eder ve işleme devam eder. Autovacuum her sınırı aştığında, kendisini askıya alır, bu da kademeli bir VACUUM etkisi yaratır. Autovacuum’un bu dur-kalk davranışı, çalışan cluster üzerindeki genel etkiyi azaltmak için tasarlanmıştır: autovacuum, etkileşimli bağlantılar ve kullanıcılar için kaynak bırakmak amacıyla kendisini askıya alır.

select * from pg_stat_activity

Yukarıdaki sorgu ile sistemdeki vacuum’ları görebilirsiniz.

Ama burada şöyle önemli bir konu var.

Dokümantasyonlarda kuvvetle muhtemel “vacuum lock yapmaz” diye bir ibare görmeniz çok normal. Ama, an geldiğinde vacuum eline sazı alır ve öyle bir lock koyar ki.. ((:

Yani evet, vacuum lock yapmaz. Ama, ama, ama,
yeri geldiğinde de eline çok güzel sazı alır. (:

Bunu lütfen unutmayalım. ((:

İletişim, İşbirlikleri ve Teklifler için,

github: https://github.com/fsk

bitbucket: https://bitbucket.org/furkandev

twitter: https://twitter.com/0xfsk

mail: furkansahinkulaksiz@gmail.com

linkedIn: https://www.linkedin.com/in/frknshnklksz/

superpeer: https://superpeer.com/fsk

--

--

No responses yet