- Postgres LISTEN/NOTIFY’daki küresel dışlayıcı kilit, basit bir uygulamanın verimini sınırlasa da bildirimler arabelleğe alınıp toplu gönderildiğinde tek bir sunucuda saniyede 60 bine kadar stream yazma işlenebilir
- NOTIFY çağıran işlem, bildirimlerin commit sırasını garanti etmek için commit ve
fsync()bitene kadar küresel kilidi tutar; bu da commit’leri seri hale getirir ve grup commit’ten yararlanmayı engeller - Stream tablosundaki her yazmada trigger ile NOTIFY çağıran ilk uygulama düşük gecikme sağladı, ancak CPU, bellek ve IOPS’u yeterince kullanamadan saniyede 2.900 işlemde darboğaza girdi
- Bildirimleri değil veritabanı tablosunu gerçeğin kaynağı kabul edip bellekte toplanan bildirimleri periyodik olarak tek bir işlemde göndermek, kilit edinme sayısını büyük ölçüde azaltabilir
- Süreç arızası nedeniyle arabellekteki bildirimlerin kaybolma olasılığı düşük frekanslı polling ile telafi edilir; eşzamanlı okuma ortamında da 15–100 ms gecikme ve öncekiye göre 20 kat verim elde edilir
Postgres ile uygulanmış düşük gecikmeli stream
- Postgres tabanlı stream, her stream parçasını
streamstablosunda yeni bir satır olarak saklar; LLM yanıt token’ı da tek bir parça olabilir - Okuma tarafı bir sonraki parçanın ne zaman geleceğini bilemediği için yalnızca basit sorgularla verimli şekilde beklemek zordur
- Periyodik polling aralığı uzun olursa çevrimiçi sohbet gibi etkileşimli kullanım senaryolarında gecikme artar; kısa olursa eşzamanlı poller’lar veritabanını boğar
- LISTEN/NOTIFY kullanıldığında okuma süreci bloklanmış halde bekler, yeni bir parçanın yazıldığı bildirimiyle hemen uyanır ve gereksiz polling’den kaçınılır
Her yazmada gönderilen NOTIFY’nin darboğazı
- İlk uygulamada
streamstablosuna yeni bir parça yazıldığında trigger bir fonksiyon çalıştırarak her seferinde bir NOTIFY gönderiyor, okuma süreci de bildirimi bekledikten sonra yeni parçayı okuyordu - Doğruluk ve düşük gecikme sağlandı, ancak büyük bir Postgres veritabanında bile saniyede 2.900’den fazla stream yazma sürdürülemedi
- Darboğaz sırasında CPU, bellek ve IOPS kullanımı belirgin şekilde artmadı; neden NOTIFY commit yolundaki küresel kilitti
Commit sırasını garanti eden küresel kilit
- NOTIFY çağıran işlem, commit başlangıcında küresel dışlayıcı kilidi edinir ve tamamen commit edilip içerik
fsync()ile diske yazılana kadar bırakmaz - Postgres, bildirimlerin işlem commit sırasına göre iletilmesini garanti eder ve tüm giden bildirimleri commit sırasıyla tam olarak eşleşmesi gereken küresel bir iç kuyruğa kaydeder
- Bildirimleri kuyruğa ekleme işi de commit’in parçası olarak işlemsel biçimde yürütülmelidir; ancak her işlemin commit süresi farklı olduğundan commit tamamlanmadan sıra belirlenemez
- Küresel kilit, bildirim içeren işlemlerin commit’lerini seri hale getirerek sırayı önceden kesinleştirir ve iç bildirim kuyruğuna da aynı sırayla eklenmelerini sağlar
Commit serileştirmenin verimi nasıl sınırladığı
- Tüm stream yazmaları trigger aracılığıyla NOTIFY çağırdığı için her yazma işlemi, tüm commit ve disk flush boyunca küresel kilidi tutar
- İşlemler sırayla commit edilirken Postgres’in birden fazla işlemi tek bir
fsync()ile işleyen grup commit özelliğinden yararlanılamaz - Verim, Postgres’in tekil işlemleri commit etme hızını aşamaz; işler kilitte beklediği için CPU ve disk de yeterince kullanılamaz
- Postgres 19’a dahil edilecek yama küresel kilidi kaldırmadığı için bu darboğazı çözmez
- Bunun yerine çok sayıda bildirim kanalı olduğu ve her listener’ın yalnızca belirli bir kanalı beklediği sınırlı durumları optimize eder
Bildirimleri arabelleğe alma ve toplu gönderim
- Stream’ler dahil birçok LISTEN/NOTIFY kullanımında bildirimler gerçeğin kaynağı değil, yalnızca gerçek verinin saklandığı tabloyu kontrol etmeye yönelik bir sinyaldir
- Bu yapıda bildirimin kendisinin kusursuz küresel sıraya veya tam dayanıklılığa sahip olması gerekmez; bellekte arabelleğe alınıp periyodik olarak tek bir batch işleminde gönderilebilir
- Küresel kilit, tek tek stream yazmalarında değil yalnızca arabellek boşaltılırken edinilir
- Tekil yazmalar arka plandaki bildirim gönderiminden ayrıldığı için hızlı ilerler; grup commit gibi Postgres optimizasyonlarından yararlanılarak verim artırılabilir
Bildirim kaybını telafi eden düşük frekanslı polling
- Bildirimler bellekteyken süreç durursa ilgili bildirimler iletilmeyebilir
- Okuma süreci, bildirimi beklerken aynı zamanda veritabanını periyodik olarak sorgular ve bildirim olmadan yazılmış stream verisi olup olmadığını kontrol eder
- Bu polling yalnızca kayıp bildirimleri kurtarmaya yarayan yardımcı bir araç olduğundan düşük frekansta çalıştırılabilir ve performansa etkisi de büyük değildir
Verim ve gecikme
- Optimize edilmiş uygulama, eşzamanlı okuma süreçlerinin olduğu bir ortamda saniyede 60 bine kadar stream yazma işleyerek ilk uygulamaya göre 20 kat daha yüksek verim kaydetti
- Verim artırılmış durumda bile gecikme 15–100 ms aralığında kaldı
- Maksimum verimde Postgres CPU’sunun tamamen kullanılması, kilit çekişmesi değil veritabanının kendisinin gerçek doygunluk noktasına ulaşıldığını gösterir
- Tüm benchmark kodu dbos-postgres-benchmark adresinde görülebilir
1 yorum
Hacker News yorumları
Ölçeklenebilirlik süreklilik gösteren bir ölçektir, ikili bir ayrım değildir. Saniyede 60 bin işlem bazı sistemler için ihtiyaçtan 100 bin kat fazla, başka sistemler içinse 100 bin kat yetersiz olabilir. Yaygın geliştirici hataları arasında “erken optimizasyon”dan çok, ölçeklenme özellikleri uygun olmayan bir teknoloji seçimini saymak isterim.
Fazla küçük bir teknoloji sınırları aşılınca açıkça başarısız olur; ancak aşırı ölçeklenebilir teknolojiler de operasyonel yük ve kısıtlar getirir. Daha zengin bir modelle geliştirme eforunu ciddi ölçüde azaltabilecek küçük sistemlere böyle teknolojileri sokmak da kötü bir tercihtir.
LISTEN/NOTIFY’nin sınırları dikkat etmeyi gerektirecek kadar düşük olduğundan, kötümser azami yükü hesapladıktan sonra bile en az 10 kat pay bırakmak iyi olur; ancak birçok proje için yeterlidir. Veritabanıyla entegrasyonu, erişilebilirliği ve ayrı bir servis işletme gerektirmemesi nedeniyle peşinen elenecek bir seçenek değildir; daha önce verilen saniyede 2 bin işlem bile bir mesajı saniyeler ölçeğinde işleyen sistemler için büyük bir sayıdır.
Gerçekçi beklenen ölçeğe makul bir pay ekleyerek tasarlamak, ancak ek ölçeklenebilirlik fiilen bedavaysa bunun ötesini seçmek daha iyidir. Birkaç bin dolara daha büyük donanım alabiliyorsanız ya da ölçeklenebilirlik dışında seçenekler eşdeğerse büyük olanı seçin.
LISTEN/NOTIFY ile Rust GraphQL abonelik broker’ını birleştirerek büyük başarı elde ettik. Abonelik sayısı on binlerceydi ama LISTEN bağlantısı host başına bir tane olmak üzere toplam sadece 3–4 adetti.
Tüm değişiklikleri her host’a gönderiyor, host gerçek kullanıcı aboneliklerini yönetiyor ve neyin yayımlanacağına karar veriyordu. Yüzlerce Ruby veya Node host’unu birkaç Rust host’uyla değiştirince mimari ciddi ölçüde sadeleşebiliyor; ölçeklenmediği düşünülen yöntemler de oldukça iyi çalışabiliyor.
CTO olarak çalıştığım dönemde tüm servislerde günde yaklaşık 100 bin işlem işlerken önce milyonlara, sonunda on milyonlara kadar büyüdük. Bu süreçte bir mühendis, veri modeliyle güçlü tutarlılıktan yararlanmak için LISTEN/NOTIFY semantiğinin üzerine bir kuyruk kurdu. Anlaması zor değildi ve ayrı depolama/iletim katmanını da ortadan kaldırabildiği için o dönemde makul görünüyordu.
Ancak kendi yaptığımız bu özelliği ölçeklerken PostgreSQL’in iç işleyişini dolanmak zorunda kalmak çok rahatsız edici hale geldi ve başka bir sisteme daha erken geçmeliydik. Ölçeklenebilirliği de iyi değildi; RDS’te nedenini saptaması zor ciddi disk çekişmesi oluştu ve ilgili tablonun VACUUM’u da kabusa dönüştü. Tanıdık bir kuyruğu PostgreSQL’in iç özellikleriyle alışılmadık biçimde uygulamak, diğer mühendisleri korkuttu; hata ayıklamaktan ve sahiplenmekten kaçındılar.
Şema ve indeks gibi ayrıntılar dışında temel ders her zaman basit ve öngörülebilir teknolojilerden başlamaktır. Çok güçlü veri tutarlılığı gerçekten gerekmiyorsa, altyapıya bir bileşen daha eklemek pahasına da olsa SQS veya Redis kuyruğu gibi API sözleşmesi açısından basit bir kuyruk kullanıp geri kalanını ona uydurmak daha iyidir. Çekirdek veri deposunun üstlendiği mekanik sorumluluk ne kadar azsa o kadar iyiydi.
Dağıtık sistem ekosistemi geliştikçe her bileşenin ne yapabileceğini anlamaya başlıyoruz. Başta az sayıda bileşenle başlayıp gerçekten gerektiğinde eklemek daha sağlıklı sistemler üretir.
Postgres’i ve artık SQLite’ı da doğru şekilde kullanan DBOS giderek daha çok hoşuma gidiyor. Mevcut CRUD stack’lerine de neredeyse hiç efor harcamadan eklenebiliyor.
Dayanıklı workflow’lar kullanmaya başlayınca uygulanacak yerler sürekli görünmeye başlıyor. Son dönemde her bir e-postayı dayanıklı bir workflow olarak ele alıp, kullanıcının ve karşı tarafın, ajanların, GitHub veya Attio gibi araçların sırayla akışa katılmasını deniyorum.
https://housecat.com/blog/gmail-durable-workflows-sandbox-vm
Bu tür yazılar genellikle herkesin kendi problemini, anlayışını ve çözümünü bağımsız değerlendirmesinin sonucudur. Bir aracın varsayılan ayarlarıyla belirli bir performans bekledi diye bunu uzmanlık eksikliği olarak adlandırmak biraz muğlak; herkes başarısızlıklar üzerinden öğrenmeye devam eder.
Deneyde 96 çekirdekli, 384 GB RAM’li bir veritabanı sunucusu kullanılmış olması (https://github.com/dbos-inc/dbos-postgres-benchmark/blob/mai...) çok önemli ve açıkça belirtilmeliydi. Veritabanları dikey ölçeklenebilir, ama bunun da sınırları vardır. Kimin nereden bağlandığı da performansı ve toplam gecikmeyi etkiler.
Saniyede 60 bin büyük görünebilir, ancak gerçek sistemleri çökerten şey olağan trafikten çok ani trafik patlamalarıdır. Büyük bir şirket değilseniz böyle büyük bir sunucuyla başlamazsınız. Okuma replikaları ve bölgeler arası yedeklilik dahil edildiğinde tek bir production veritabanı kümesinin maliyeti 100 bin doları aşar.
İlgili bir yazı gibi görünüyor: Postgres LISTEN/NOTIFY does not scale - https://news.ycombinator.com/item?id=44490510 - Temmuz 2025, 321 yorum
Yazıdaki en önemli kısmın, tüketicilerin nereye kadar okuduğunu izlemeyi ve alternatif yoldan yeni iletileri sorgulamayı sağlayan offset veya sıra numarası atama yönteminin eksik olduğu anlaşılıyor. Birden fazla yöntem var, ancak karmaşıklık ya da kilit çekişmesi olmadan çözmek kolay değil; yanlış uygulanırsa tüketicilerle yarış durumu da oluşabilir. Çoğu durumda yazar, olay konusunun bir sonraki numarasını atamak için durum tablosundaki tek bir satıra vb. kilit koyacaktır.
En iyi yöntemin ne olduğunu merak ediyorum. Değişiklik verisi yakalamayı (CDC) okuyup atanmış olay numarasını başka bir tabloya yazan bir araç iyi olabilir, ancak gecikme süresi uzayabilir; ayrıca bu CDC işleyicisinin NOTIFY işlemini de yapması gerekir.
Tüketici tarafında ise toplu işleme ile aktarım hızını ciddi ölçüde artırabilecek kullanım alanları da var. Bu durumda LISTEN/NOTIFY olmadan tüketiciyi döngüsel olarak çalıştırıp her seferinde işlenmemiş tüm yeni iletileri işlemek ve döngüler arasında son sıra numarasını kaydetmek yeterli olur.
LISTEN/NOTIFY desteğinin ilk geldiği sürümde kilit uygulamasının iyi olmadığını ve performans sorunları yaşandığını hatırlıyorum. Burada eleştirilen önceki yazı da ilk paragraftan hemen sonraki düzeltme notunda bunu düzeltmişti.
Düzeltme tarihi 8 Mayıs ise, 24 Temmuz tarihli yazının, bu özelliğin ölçeklenmediğini söyleyen ünlü yazının kötü niyetle yazılmadığını ve o dönem için yanlış da olmayabileceğini kabul etmesi gerekir.
Bunun yerine, bildirim kanallarının çok olduğu ve her alıcının yalnızca belirli tek bir kanalı beklediği daha sınırlı durumu optimize ediyor.
Son kontrol ettiğimde LISTEN/NOTIFY’de bildirim verisi için 8.000 bayt üst sınırı vardı; bu da açıkça ölçeklenmeyen bir yönüydü. Bildirimi satır olarak saklayıp yalnızca ID geçiremeyeceğiniz bir veriyse kullanımı zor.
Web oyunundaki olaylar, durum değişikliğini açıklayan geçici verilerdi; veritabanına kaydetmek için bir neden yoktu ve 8.000 baytı aşabiliyordu, bu yüzden bu kullanım için uygun değildi.
Yazı, küresel kuyruğun kilit çekişmesini ele alıyor; ancak sabit boyutlu küresel kuyruğun başka bir sorunundan söz etmiyor gibi. Bir kanaldaki tek bir yavaş alıcı, tüm kanallara yazmayı engelleyebiliyordu. En azından birkaç yıl önce böyle bir arıza biçimi mümkündü; şimdi değişmiş olabilir.