Sorunun tarihi.
Danışmanlık kilitleri ilk olarak PostgreSQL 8.2'de, MVCC tüp görünürlük sistemi dışında çalışan hafif, uygulama düzeyinde senkronizasyon araçları sağlamak amacıyla ortaya çıkmıştır. Tablo tabanlı kilitlemenin mantıksal olarak uygunsuz veya performans açısından engelleyici olabileceği iş akışları, kuyruk işleme ve idempotent alım gibi durumlar için tasarlanmışlardır. Belirli tablo tüplerine bağlı olan ve xmax sistem sütununda kaydedilen satır seviyesi kilitlerinin aksine, danışmanlık kilitleri tamamen paylaşılan bellek kilit yöneticisi içinde yer alır ve, ölü tüpler veya WAL trafiği üretmeden soyut kaynaklara erişimi düzenlemek için bir mekanizma sunar.
Sorun.
Yüksek eş zamanlı idempotent alım hatları içinde, iş anahtarları (örn. harici UUID'ler) üzerinde birincillik uygulamak için geleneksel INSERT ... ON CONFLICT veya SELECT FOR UPDATE kullanmak ciddi darboğazlara neden olur. Satır seviyesi yaklaşımlar, kilit bitlerini ayarlamak için yığın üzerinde yazma gerektirir, bu da tabloları şişirir, VACUUM baskısını hızlandırır ve çelişki çözümleme sırasında benzersiz dizinlerde sıcak noktalar oluşturur. Zorluk, depolama katmanını etkilemeden mantıksal varlıklar—örneğin, bir karma iş anahtarı—için karşılıklı dışlama sağlamaktır, aynı zamanda kilit hatalarının kaynakları kalıcı bağlantı havuzlarına sızdırmadığından emin olmaktır.
Çözüm.
Kritik özellik, danışmanlık kilitlerinin yalnızca paylaşılan bellek içindeki LOCKTAG hash tablosunda saklanmasıdır, bu nedenle temel ilişki sayfalarını asla değiştirmez. pg_advisory_xact_lock(hashtext(business_key)) kullanarak, uygulama COMMIT veya ROLLBACK sırasında otomatik olarak serbest bırakılan işlem kapsamlı bir mutex elde eder, bu da oturum düzeyindeki pg_advisory_lock ile ilişkili kilit sızıntısını önler. Bu yaklaşım, kilidin yalnızca bellek içindeki hafif bir giriş olarak var olduğu için tablo şişmesini ve dizin rekabetini ortadan kaldırır, aşağıda gösterildiği gibi:
BEGIN; -- Hash'lenmiş iş anahtarı üzerinde işlem sınırlı kilit edin SELECT pg_advisory_xact_lock(hashtext('a1b2c3d4')); -- Eklemek güvenli; başka bir oturum kilidi tutuyorsa benzersiz dizin rekabeti yoktur INSERT INTO events (business_key, payload) VALUES ('a1b2c3d4', '{"event":"click"}') ON CONFLICT (business_key) DO NOTHING; COMMIT;
Bir telemetri şirketindeki veri platformu ekibi, Kafka'dan PostgreSQL'ye alım yapılan her bir olay için müşteri tarafından oluşturulan UUID'nin idempotentlik anahtarı olarak kullanıldığısa, saniyede 50.000 olay için tam bir işlem garantisi vermek zorundaydı. Başlangıçta yapılan yük testleri, benzersiz UUID sütunu üzerine INSERT ... ON CONFLICT DO NOTHING kullanarak ciddî bir kuyruk gecikmesi yaratmıştı, bu da benzersiz B-tree dizininde spinlock rekabeti ve hızla artan şişme sorununa yol açmıştı. WAL üretim oranı, zirve saatlerde iki katına çıkarak çoğaltma gecikmesi ve depolama kapasitesini tehdit ediyordu.
Önerilen bir çözüm, öncelikle mevcut anahtarın varlığı için SELECT * FROM events WHERE business_key = $1 FOR UPDATE kullanarak kontrol etmeyi, sonra sonuç boşsa eklemeyi içeriyordu. Bu, tekrarlamaları önlese de, her yazarın ya mevcut satırda ya da bir rezervasyon satırında kilit almasını zorunlu kılıyordu, bu da rezervasyon tablosu sayfalarında büyük bir sıcak nokta oluşturuyordu. Bu yaklaşım, her on beş dakikada bir ölü tüpleri geri kazanmak için VACUUM gerektiriyordu ve kilidin tüm işlem süresince tutulmasını gerektiriyordu, bu da verimliliği ciddi şekilde kısıtlıyordu.
Mimari ekip, eklemeleri kontrol etmek için dış bir Redis önbelleğine SETNX işlemleri kullanmayı önerdi. Bu, veritabanı şişmesini ortadan kaldırdı ve PostgreSQL yükünü azalttı, ancak kritik hatalı durumlar oluşturdu: Redis kümesi ile veritabanı arasında ağ bölünmeleri, Redis kilidi süresi dolduğunda ancak PostgreSQL işlemi henüz tam olarak onaylanmadığında tekrar eden eklemelere izin verebilirdi. Ayrıca, iki dağıtık sistem arasında tutarlılığı sağlamak işletme karmaşıklığını artırdı ve Redlock veya benzeri algoritmaların uygulanmasını gerektiriyordu, bu da her işlem başına yaklaşık 5 milisaniye gecikmeye yol açıyordu.
Seçilen tasarım, pg_advisory_xact_lock(hashtext(business_key)) kullanarak PostgreSQL'nin yerel danışmanlık kilitlerini kullanmayı tercih etti; bu, ekleme girişiminde bulunmadan önce hash'lenmiş UUID üzerinde işlem sınırlı bir kilit alıyordu. Bu kilitler sadece paylaşılan bellek içinde yaşadığı ve yığını etkilemediği için depolama maliyeti sıfırdı ve işlem sona erdiğinde otomatik olarak serbest bırakıldı, oturum düzeyindeki kilitlerde gözlemlenen kilit sızıntılarını önledi. Görünmez ölü kilitlerden kaçınmak için, uygulama katmanı, tüm UUID'leri her partide kendi hash'lenmiş tam sayı değerine göre sıraladı, böylece tüm eşzamanlı işçiler arasında küresel bir sıralama protokolü sağladı.
Danışmanlık kilitleri, en düşük gecikmeyi (alt milisaniye edinimi) ve sıfır depolama yan etkileri sağladığı için seçildi ve dış bağımlılıklar olmadan katı doğrulama sağladı. Redis yaklaşımının aksine, kilidin ömrü veritabanı işlemiyle bağlıydı, kilit edinimi ile ekleme onayı arasında atomiklik sağlıyordu. SELECT FOR UPDATE ile karşılaştırıldığında, hiçbir tablo şişmesi oluşturmadı ve ham ON CONFLICT ile karşılaştırıldığında, benzersiz dizin, çelişen eşzamanlı eklemeler tarafından asla zorlanmadı çünkü serileştirme yığın erişiminden önce gerçekleşti.
Dağıtım sonrasında, alım hattı saniyede 80.000 olayı sürdü ve p99 gecikmesi 10 milisaniyenin altında kaldı, önceki 200 milisaniyelik gecikmelere kıyasla. Tablo şişmesi önemsiz seviyelere düştü, böylece autovacuum yalnızca yoğun olmayan saatlerde çalıştı ve WAL hacmi %40 azaldı, arşivleme depolama maliyetlerini ve kopyalama gecikmesini önemli ölçüde azaltarak. Sistem, bir tane bile tekrar eden olay veya kilit zaman aşımı olmaksızın çoklu veritabanı yeniden başlatmaları ve bağlantı havuzu kaoslarına rağmen tam bir kez kullanma semantiğini korudu.
Yüksek bir çıkış işçisi mimarisinde pg_advisory_lock (oturum kapsamlı) kullanmanın neden pg_advisory_xact_lock yerine bağlantı havuzunun tükenmesi ve tekrar eden alım riski taşıdığını adaylar genellikle fark etmezler?
Adaylar genellikle pg_advisory_lock'un açıkça kilidi açılana veya oturum bağlantısı kesilene kadar sürdüğünü, işlem iptal edilse bile fark etmezler. Uzun süreli bağlantıları yeniden kullanan bir havuz ortamında, kilidi açma çağrısını atlayan bir mantık hatası veya istisna, kilidi süresiz olarak tutar, bu da aynı iş anahtarını işleyen sonraki işçilerin sonsuza dek beklemesine neden olur. Bu nedenle, kilit ömrünü işlem sınırına bağladığı için pg_advisory_xact_lock kullanılmalıdır, bu da otomatik olarak ROLLBACK üzerinde serbest bırakır ve aksi takdirde çalışan havuzunu aç bırakacak olan mutex sızıntılarını önler.
Birden fazla danışmanlık kilidi edinirken toplam sıralama garantisinin yokluğu, görünmez ölü kilitlere nasıl yol açar ve bu tehlikeyi ortadan kaldıran belirli uygulama modeli nedir?
Satır seviyesi ölü kilitlerden farklı olarak, PostgreSQL'in deadlock_timeout dedektörü kurban bir işlemi öldürerek çözdüğü danışmanlık kilit ölülükleri motor için görünmezdir çünkü kullanıcı tanımlı ad alanlarında gerçekleşir. Eğer İşçi A kaynak X sonra Y'yi kilitlerken, İşçi B Y sonra X'i kilitlerse, her iki oturum hata olmaksızın sonsuza dek bekler. Zorunlu model, herhangi bir kilit talebinde bulunmadan önce tüm kaynak tanımlayıcılarını (örn. hashtext(uuid) değerleri) sıkı bir monotonik sırada (artan veya azalan) sıralamaktır. Bu küresel sıralama, bekleme grafiğinin döngüsüz kalmasını sağlar ve dairesel bağımlılıkları imkansız kılarak sessiz beklemelerin riskini ortadan kaldırır.
Bir tekil işlemin tutabileceği danışmanlık kilidi sayısını kısıtlayan paylaşılan bellek kısıtlaması nedir ve max_locks_per_transaction sınırını aşmanın satır seviyesindeki kilit tükenmesi ile karşılaştırması nasıldır?
Birçok aday danışmanlık kilitlerinin sonsuz olduğunu varsaysa da, bu kilitler max_locks_per_transaction yapılandırma parametresi (varsayılan 64) tarafından yönetilen paylaşılan kilit tablosundaki girişleri tüketir. Bir işlemde bu sınırdan fazla kilit tutmak, ERROR: out of shared memory (SQLSTATE 53200) hatasına yol açarak işlemi anında iptal eder. Bu, satır seviyesi kilitlerde, sınırları aşmanın genellikle bir kilit yükseltme veya bekleme ile sonuçlandığı durumlara kıyasla, sabit bir paylaşılan bellek havuzunu tükenmez. Çözüm, işlemleri daha küçük alt işlemler halinde gruplamak veya birden fazla mantıksal kaynağı tek bir danışmanlık kilidi anahtarı altında bileşik hashleme ile toplamak olarak belirlenir, böylece aynı anda binlerce bireysel anahtarı kilitlemeye çalışmak yerine.