POSTGRESQL LOCKİNG: BLOCKİNG, DEADLOCK VE İZLEME TEKNİKLERİ
Üretimde “yavaşladı” şikâyetlerinin önemli bir kısmı aslında CPU ya da diskten değil, sessizce büyüyen kilit beklemelerinden gelir. Bir işlem diğerini bekler, o da bir başkasını… ve zincir uzadıkça en basit sorgular bile saniyelerce askıda kalır. PostgreSQL güçlü eşzamanlılık özellikleri sunar; fakat kilitlerin nasıl çalıştığını bilmeden yapılan tasarımlar, blocking ve deadlock gibi sorunları kaçınılmaz hâle getirir.
Bu yazıda “PostgreSQL locking” dünyasını pratik bir bakışla ele alacağız: Hangi kilit türleri hangi durumda oluşur, blocking nasıl tespit edilir, deadlock nasıl meydana gelir ve en önemlisi bunu nasıl önleriz? Ayrıca pg_locks, pg_stat_activity ve log ayarlarıyla izleme tarafını da netleştirip, olay anında uygulanabilecek bir kontrol listesi çıkaracağız.
Hedef, kilitleri tamamen ortadan kaldırmak değil; kilitleri öngörülebilir, izlenebilir ve uygulama davranışıyla uyumlu hâle getirmektir. Böylece hem veri tutarlılığını korur hem de gecikme patlamalarını kontrol altına alırsınız.

PostgreSQL’de kilitleme mantığı: Temel kavramlar ve lock türleri
PostgreSQL’de kilitler kabaca iki kategoriye ayrılır: tablo düzeyi kilitler (ör. ACCESS SHARE, ROW EXCLUSIVE) ve satır düzeyi kilitler (ör. FOR UPDATE ile oluşan satır kilitleri). Günlük işlerde çoğu sorgu, beklediğinizden daha “hafif” kilitler alır; ancak uzun transaction’lar ve DDL işlemleri tablo düzeyinde güçlü kilitler oluşturarak tüm trafiği etkileyebilir.
Özellikle MVCC sayesinde “okuma” genelde “yazmayı” bloke etmez; fakat bazı kombinasyonlarda okuma-yazma etkileşimi artar. Örneğin SELECT ... FOR UPDATE gibi satır kilidi alan okumalar, yazanları ve diğer kilitlemeli okumaları bekletebilir. Ayrıca indeks bakım işleri, VACUUM, DDL ve foreign key kontrolleri gibi süreçler de kilit grafiğini beklenmedik biçimde karmaşıklaştırabilir.
Table-level lock modları: DDL ile DML arasındaki sürtünme
Basit bir SELECT çoğunlukla ACCESS SHARE alır ve yazma işlemleriyle uyumludur. INSERT/UPDATE/DELETE ise genelde ROW EXCLUSIVE gibi modlar kullanır. Sorunlar, ALTER TABLE, CREATE INDEX (özellikle concurrent olmayan) gibi DDL komutlarının daha güçlü kilitlere ihtiyaç duymasıyla büyür. DDL’in “hızlı” olduğunu düşünmek aldatıcıdır; tablo büyüdükçe kilit süresi uzar.
Row-level lock’lar: FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE
Satır kilitleri, yüksek eşzamanlılıkta bile “sıcak satır” üzerinde darboğaz yaratabilir. Örneğin bir sipariş kaydı aynı anda farklı iş akışları tarafından güncelleniyorsa, satır bazında beklemeler artar. Bu noktada “kilit süresini kısaltmak” ve “kilit sırasını standardize etmek” kritik olur.
Transaction süresi: Asıl düşman uzun açık kalan transaction
Kilitler çoğu zaman transaction bitene kadar tutulur. Bu yüzden tek bir “unutulmuş” transaction, yalnızca blocking değil, aynı zamanda VACUUM gecikmeleri ve bloat gibi ikincil sorunlar da üretir. Kural basit: Transaction’ı kısa tut, “kullanıcı etkileşimi” ile aynı transaction içinde bekleme yaratma, batch işlemleri parçala.
Blocking nedir, nasıl görünür ve neden genellikle “yan etki” olarak çıkar?
Blocking, bir işlemin başka bir işlemin tuttuğu kilit yüzünden ilerleyememesi durumudur. Buradaki tehlike, blocking’in genellikle tek bir sorgu değil, zincir etkisi yaratmasıdır. Bir UPDATE kilidi alır; arkasından gelenler bekler; bekleyenler başka tabloları da kilitlemeye başlayınca sistem genelinde gecikme yayılır.
Blocking’i yalnızca “kimin kimi beklediği” olarak düşünmeyin. Aynı zamanda hangi kilit modu, hangi nesne, bekleme süresi ve transaction yaşı birlikte değerlendirilmelidir. Aksi hâlde yanlış bağlantıyı sonlandırıp problemi büyütmek mümkündür.
Bekleme zinciri: Head blocker’ı bulmak neden önemlidir?
Çoğu olayda asıl sorun “en baştaki” bloklayan transaction’dır. Arkada yüzlerce oturum bekliyor olabilir; fakat esas kazanım, zincirin tepesindeki işlemi çözmektir. Head blocker bazen bir batch job, bazen sessiz bir migration, bazen de uygulamanın açık bıraktığı transaction’dır.
Uygulama davranışı: Pool, retry ve uzun sorguların birleşimi
Bağlantı havuzu (pool) küçükse, birkaç bağlantının kilit beklemesi tüm servis kapasitesini tüketir. Ayrıca agresif retry mekanizmaları, kilit basıncını daha da artırır. Bu yüzden blocking’e yaklaşım yalnızca veritabanı değil, uygulama tarafını da kapsamalıdır: idempotent retry, jitter, kuyruklama ve backoff gibi stratejiler kilit fırtınasını engeller.
Blocking’i izleme: pg_stat_activity ve pg_locks ile pratik sorgular
Blocking olaylarını çözmek için elinizde iki temel pencere vardır: pg_stat_activity (kim ne yapıyor?) ve pg_locks (kim hangi kilidi tutuyor ve bekliyor?). Bu ikisini birleştirince, bekleyen ve bloklayan oturumları, sorgu metinlerini ve transaction yaşlarını hızlıca görebilirsiniz.
Hızlı teşhis: Aktif beklemeleri listeleme
Aşağıdaki sorgu, bekleyen oturumları ve onları bloklayan PID’leri görünür kılar. Bu, “nereden başlamalıyım?” sorusuna pratik bir cevap verir.
SELECT
a.pid AS waiting_pid,
a.usename AS waiting_user,
a.application_name AS waiting_app,
now() - a.query_start AS waiting_query_age,
a.wait_event_type,
a.wait_event,
pg_blocking_pids(a.pid) AS blocking_pids,
a.query AS waiting_query
FROM pg_stat_activity a
WHERE a.state = 'active'
AND a.wait_event_type IS NOT NULL
ORDER BY waiting_query_age DESC;Çıktıda wait_event_type ve wait_eventpg_blocking_pids bir dizi döndürür; birden fazla bloklayan olabilir. Bu durumda zinciri takip etmek gerekir.

pg_locks ile kilit modlarını birleştirerek görmek
pg_locks tek başına yorumlanması zor olabilir; ancak relation ve activity ile birleştirince kilidin hangi tabloda oluştuğu anlaşılır. Aşağıdaki örnek, bekleyen kilitleri tablo adıyla birlikte listeler.
SELECT
a.pid,
a.usename,
a.application_name,
a.state,
now() - a.xact_start AS xact_age,
l.locktype,
l.mode,
l.granted,
c.relname AS relation,
a.query
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
LEFT JOIN pg_class c ON c.oid = l.relation
WHERE a.datname = current_database()
ORDER BY l.granted ASC, xact_age DESC NULLS LAST;Burada granted = false olan satırlar bekleyen kilitlerdir. relation boş ise kilit tablo yerine transaction ID veya başka bir kaynak üzerinde olabilir. Bu tür kilitler, özellikle deadlock analizinde daha görünür hâle gelir.
Deadlock nedir: Döngüsel bekleme nasıl oluşur?
Deadlock, iki (veya daha fazla) transaction’ın birbirinin kilidine ihtiyaç duyduğu ve hiçbirinin ilerleyemediği döngüsel bekleme durumudur. PostgreSQL deadlock’u tespit edince bir transaction’ı “kurban” seçip iptal eder; böylece döngü kırılır. Bu iyi bir güvenlik mekanizmasıdır; ancak uygulama açısından hata üretir ve iş akışını bozar.
Deadlock, çoğu zaman “aynı iki tabloya farklı sırayla erişme” kaynaklıdır. Örneğin Transaction A önce accounts sonra orders güncelliyor; Transaction B ise tam tersini yapıyorsa, yoğunluk altında deadlock ihtimali hızla artar. Bu yüzden kilit sırası standardizasyonu en etkili önlemlerden biridir.
Tipik deadlock örneği: Ters sıra ile güncelleme
Aşağıdaki senaryo iki oturumda çalıştırıldığında deadlock üretebilir. Örnek, kavramsal olarak “aynı iki kaynağı ters sırayla kilitleme” durumunu gösterir.
-- Oturum 1
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
-- Bu noktada uygulama başka bir işlem yapıyor gibi düşünün
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
COMMIT;
-- Oturum 2
BEGIN;
UPDATE accounts SET balance = balance - 10 WHERE id = 2;
UPDATE accounts SET balance = balance + 10 WHERE id = 1;
COMMIT;İki oturum farklı satırları kilitleyip sonra karşı tarafın kilitlediği satıra ihtiyaç duyduğunda döngü oluşur. PostgreSQL bunu algılar ve genellikle ikinci geleni iptal eder. Uygulamada bu durumda retry mantığı gerekir; fakat asıl çözüm, iki iş akışının aynı sırayı takip etmesini sağlamaktır.
Deadlock logları: Hata mesajını okumak ve bağlama oturtmak
Deadlock hatası yalnızca “deadlock detected” değildir; log’da hangi işlem hangi kilidi bekliyordu, hangi nesnede döngü kuruldu gibi detaylar bulunur. Bunun verimli olabilmesi için log seviyeleri ve parametreler önemlidir. “İzleme” bölümünde bu ayarların pratik yönüne değineceğiz.
İzleme ve teşhis araçları: log_lock_waits, deadlock_timeout ve pratik dashboard yaklaşımı
Blocking ve deadlock’ları sadece anlık sorgularla değil, sürdürülebilir şekilde izlemek gerekir. Bunun için PostgreSQL’in log parametreleri ve istatistik görünümleri birlikte düşünülmelidir. Buradaki amaç, olay çıktıktan sonra değil, olay büyümeden önce sinyal almaktır.
log_lock_waits ve deadlock_timeout nasıl birlikte çalışır?
deadlock_timeout, sistemin deadlock kontrolünü ne kadar sürede bir tetikleyeceğini belirler. log_lock_waits açıksa, bu süreyi aşan kilit beklemeleri log’a düşer. Yani deadlock_timeout sadece deadlock tespiti değil, aynı zamanda “uzun kilit beklemesi” için bir eşik gibi de davranır. Çok düşük değer, log gürültüsünü artırır; çok yüksek değer ise sinyal kaçırabilir.
pg_stat_activity ile transaction yaşı ve durum alanları
Olay anında sadece “aktif” sorgulara değil, idle in transaction gibi riskli durumlara da bakın. “Idle in transaction” genellikle uygulamanın transaction’ı açıp beklettiğini gösterir. Bu, kilitlerin beklenenden uzun süre tutulmasına ve VACUUM’ın zorlanmasına yol açabilir.
Uygulama düzeyi izler: request id, kullanıcı, endpoint eşlemesi
Veritabanında PID görürsünüz; fakat üretimde “hangi istek bunu yaptı?” sorusu çok daha değerlidir. connection string’de application_name kullanmak, log’ları request id ile ilişkilendirmek ve gerektiğinde sorgu etiketleme yapmak, çözüm süresini dramatik biçimde kısaltır. İzleme birimi “SQL” değil, “iş etkisi” olmalıdır.
Önleme ve kontrol: lock_timeout, statement_timeout ve güvenli erişim kalıpları
Kilit sorunlarını tamamen yok etmek mümkün değildir; ancak etkisini sınırlamak mümkündür. İki temel yaklaşım öne çıkar: (1) transaction süresini kısaltmak, (2) beklemeyi kontrollü hâle getirmek. PostgreSQL bu amaçla lock_timeout ve statement_timeout gibi mekanizmalar sunar.
lock_timeout ile “sonsuz bekleme”yi engellemek
Uygulamanın kritik yollarında kilit beklemeyi sınırsız bırakmak yerine, makul bir eşik belirlemek daha sağlıklıdır. lock_timeout aşıldığında işlem hata alır; bu hatayı uygulama tarafında yöneterek kullanıcı deneyimini ve sistem stabilitesini koruyabilirsiniz.
NOWAIT ve SKIP LOCKED: Kuyruk tüketimi ve yarış durumları
Özellikle iş kuyruğu (job queue) benzeri senaryolarda, kilit beklemek yerine NOWAIT ya da SKIP LOCKED kullanmak throughput’u artırabilir. Ancak bu kalıplar dikkat ister: SKIP LOCKED ile bazı kayıtların “sonra” işlenmesi normaldir; iş mantığı buna uygun olmalıdır.
Advisory lock’lar: İş kuralına göre kilit
Satır kilitleri veri modeliyle sınırlıdır. Bazı durumlarda “aynı müşteri için aynı anda tek işlem” gibi iş kuralları vardır ve bu kuralı veri seviyesinde satır kilidiyle ifade etmek zor olabilir. PostgreSQL’in advisory lock’ları, bu tür senaryolarda kontrollü bir eşzamanlılık sağlar. Yine de advisory lock kullanıyorsanız, kilit anahtarının tasarımını ve olası sızıntı senaryolarını gözden geçirmelisiniz.

Uygulama tasarımı: Deadlock riskini azaltan pratik kurallar
Deadlock çoğu zaman veritabanı ayarlarından değil, erişim deseninden doğar. Aynı veri setine farklı sırayla dokunmak, transaction içinde gereksiz iş yapmak ve “okudum sonra güncellerim” kalıbını kontrolsüz kullanmak riski büyütür. Aşağıdaki pratik kurallar, deadlock ve blocking olasılığını anlamlı biçimde düşürür.
Kilit sırasını standardize et: Aynı tablolar, aynı sıra
İki tabloya dokunan tüm iş akışları aynı sıralamayı takip etmelidir. Örneğin önce accounts sonra orders güncellenecekse, her yerde bu sıra korunmalıdır. Bu kuralı kod incelemesinde kontrol edilebilir hâle getirmek, sürprizleri azaltır.
Transaction içinde “dış çağrı” yapma: API/IO beklemeleri kilit süresini uzatır
Transaction açıp sonra HTTP çağrısı yapmak, kullanıcıdan input beklemek veya uzun hesaplama çalıştırmak kilitleri gereksiz yere elde tutar. Bu, “normal şartlarda sorun yok” gibi görünen bir sistemi yoğunluk altında kırılganlaştırır. Transaction sınırlarını iş adımlarına göre yeniden tasarlamak genellikle en hızlı kazanımdır.
Retry stratejisi: Körlemesine değil, ölçülü ve idempotent
Deadlock hatası aldığınızda tekrar denemek çoğu zaman doğrudur; ancak retry’ı kontrolsüz yapmak kilit basıncını artırabilir. Backoff, jitter ve maksimum deneme sayısı ile birlikte, işlemin idempotent olması kritik önem taşır. Aksi hâlde “iki kez çalıştı” gibi tutarlılık sorunları doğabilir.
Olay anı playbook: Blocking ve deadlock çıktığında adım adım ne yapılmalı?
Üretimde kilit olayı yaşandığında panik yerine sistematik hareket etmek gerekir. Aşağıdaki adımlar, “sinyal”den “aksiyon”a giden yolu kısaltır. Amaç, önce etkisi en büyük noktayı bulmak, sonra en az riskli müdahaleyle sistemi toparlamaktır.
- Bekleyenleri listele: pg_stat_activity + pg_blocking_pids ile bekleme zincirini çıkar.
- Head blocker’ı belirle: Zincirin tepesindeki transaction yaşı ve sorgu metnini incele.
- İş etkisini ölç: Hangi endpoint/iş akışı etkilendi, pool doldu mu, retry fırtınası var mı?
- En az riskli müdahale: Gerekirse tek bir oturumu sonlandır, ardından kalıcı kök nedeni analiz et.
- Kalıcı düzeltme: Kilit sırası, transaction sınırları, timeout ve indeks/migration planı gözden geçir.
Oturum sonlandırma kararını verirken dikkat edilmesi gerekenler
Bir PID’i sonlandırmak hızlı çözüm gibi görünür; fakat veri tutarlılığı ve kullanıcı deneyimi etkileri olabilir. Önce transaction’ın ne kadar süredir açık olduğuna, hangi tablolara dokunduğuna, kritik bir yazma işlemi mi yoksa rapor sorgusu mu olduğuna bakın. Ayrıca bazı işlemler “yeniden çalıştırılırsa” sorun çıkarabilir; bu nedenle uygulama ekibiyle eşgüdüm önemlidir.
Sonradan analiz: Aynı olay tekrar eder mi?
Olayı söndürmek yeterli değildir. Deadlock ise, iki erişim deseninin ters sırada çalıştığını gösterir. Blocking ise, genellikle uzun transaction veya DDL/DML çakışması sinyali verir. Bu noktada, migration süreçlerini “online” çalışacak şekilde planlamak (ör. uygun yöntemler, parçalı dönüşümler) ve kritik sorguları gözden geçirmek gerekir.
DDL, indeks ve bakım işleri: Kilitleri büyüten görünmez riskler
Üretimde en sık sürpriz yaratan senaryolardan biri, iş saatinde koşan bakım/DDL işleridir. Örneğin concurrent olmayan indeks oluşturma, tablo büyüdükçe uzun süre güçlü kilit tutabilir. Benzer şekilde büyük tabloda yapılan ALTER işlemleri, beklenmedik kuyruklar oluşturabilir.
Migration planı: Küçük adımlar, ölçüm ve geri dönüş
Şema değişikliklerini mümkünse küçük adımlara bölmek, etkisini ölçmek ve gerektiğinde geri dönüş planı hazırlamak gerekir. “Tek seferde büyük değişiklik” yaklaşımı, kilit olaylarını tetikleyebilir. Bu noktada ekiplerin ortak bir kontrol listesiyle hareket etmesi, operasyonel riski azaltır.
VACUUM ve autovacuum: Dolaylı kilit etkileri
VACUUM çoğu zaman “kilit problemi değil” gibi düşünülür; ancak uzun açık transaction’lar vacuum’ın temizlemesini engelleyip bloat yaratır. Bloat büyüdükçe sorgular uzar; sorgular uzadıkça kilit tutma süresi artar. Yani kilit problemleri bazen bir performans döngüsünün parçası hâline gelir.
Öğrenmeyi derinleştirmek: Postgres eğitiminde bu konular nasıl ele alınır?
Kilit davranışını yalnızca teorik anlatımla kavramak zordur; gerçekçi senaryolarda oturum açıp bekleme zincirini görmek çok daha öğreticidir. Bu nedenle PostgreSQL kilitleme, blocking ve deadlock konularını uygulamalı ele almak istiyorsanız PostgreSQL Eğitimi sayfasına göz atabilirsiniz. Orada transaction yönetimi, izleme yaklaşımı ve üretim pratikleri de bütünlüklü biçimde işlenir.
Sonuç: Kilitleri yönetilebilir hâle getirmek
PostgreSQL’in kilitleme modeli güçlüdür; doğru kullanıldığında yüksek eşzamanlılıkta bile güvenli çalışma sağlar. Kilit sorunları çoğu zaman “veritabanı hatası” değil, tasarım ve işlem akışı hatasıdır. Transaction’ları kısa tutmak, kilit sırasını standardize etmek, beklemeyi timeout’larla sınırlamak ve pg_locks/pg_stat_activity ile düzenli izleme yapmak, blocking ve deadlock kaynaklı üretim vakalarını belirgin biçimde azaltır.
En iyi yaklaşım, sorun çıkınca müdahale etmekten ziyade, davranışı görünür kılıp ölçülebilir hedefler koymaktır: maksimum transaction yaşı, lock bekleme eşikleri, migration pencereleri ve uygulama retry politikaları gibi. Böylece kilitler “sürpriz” olmaktan çıkar, operasyonun yönetilen bir parçasına dönüşür.


