Postgres outbox: worker'lar mı, WAL'den okumak mı?
Milan Jovanović, PostgreSQL üzerindeki transactional outbox'ı boşaltmak için polling worker'ları WAL'i logical replication ile okuyan bir consumer ile karşılaştırdı. Throughput her iki tarafta da binlerce mesaj/saniye seviyesinde; asıl fark gecikmede ve WAL tarafının getirdiği replication slot, WAL disk kullanımı ve daha çok duplicate gibi operasyonel yüklerde ortaya çıkıyor.
Kilit noktalar
- FOR UPDATE SKIP LOCKED ile worker'lar 1.000 satırlık batch'leri ayrı süreçlerde bile çakışmadan sahiplenebiliyor; transaction broker onayına kadar açık kalıyor.
- Beş worker bir dakikalık boşaltmada ~23.000 mesaj/saniye yayınladı, ancak boş batch sonrası bir saniyelik bekleme yüzünden yeni mesaj 419 ms gecikti.
- Npgsql'in LogicalReplicationConnection, PgOutputReplicationSlot ve pgoutput seçenekleriyle kurulan WAL reader commit'ten consumer'a 0,83 ms ve ~31.000 mesaj/saniye ölçüldü.
- SetReplicationStatus yalnızca broker onayından sonra, CommitMessage geldiğinde çağrılmalı; böylece çökmede mesaj kaybolmuyor, tekrar oynatılıyor.
- Trigger içinden NOTIFY göndermek commit'leri serileştiriyor: 100 kullanıcılı testte API 8.070 sipariş/saniyeden 256'ya düştü.
- Slot, reader kapalıyken WAL'i diskte tutuyor; max_slot_wal_keep_size ile sınırlanabiliyor ama aşılırsa Postgres slot'u geçersiz kılıyor.
Milan Jovanović, PostgreSQL üzerindeki transactional outbox'ı boşaltmanın iki yolunu karşılaştırdı: polling worker'lar ve write-ahead log'u logical replication ile okuyan bir consumer. Bir dakikalık backlog boşaltmada tek worker saniyede yaklaşık 15.000 mesaj yayınladı, beş worker birlikte 23.000'e çıktı, WAL reader ise 31.000 civarına ulaştı. Farkı asıl gecikme yarattı: saniyede 100 siparişte beş worker ortalama 419 ms beklerken reader 1 ms'nin altında kaldı, çünkü worker'lar boş batch sonrası bir saniye uyuyor.
WAL yaklaşımının operasyonel maliyeti daha yüksek: wal_level=logical, bir publication, reader kapalıyken WAL'i diskte tutan bir replication slot ve bir cleanup job gerekiyor; tek slot yatay ölçeklenemiyor. Süreç sert şekilde öldürüldüğünde reader 31.095, worker'lar ise 4.536 duplicate gönderdi, yani consumer'lar idempotent olmak zorunda. Trigger içinden gönderilen NOTIFY ise commit'leri sıraya soktuğu için API'yi saniyede 256 siparişe düşürdü.
Gerçek dünyada kullanımı
- Sipariş veya ödeme API'si: veritabanı transaction'ı commit olduğunda olayın mutlaka yayınlanması, broker'ın o an erişilemez olması sorun değil.
- Yoğun veri alan bir servis: outbox tablosu biriktiğinde aynı deployment'a daha fazla worker ekleyerek throughput'u artırmak.
- Commit ile consumer arasındaki gecikmenin kritik olduğu bildirim veya fiyat akışı senaryolarında WAL reader'a geçmek.
- Dakikada yüz binlerce boş claim sorgusu veritabanı CPU'sunu yiyen bir sistemde polling sıklığını düşürmek veya WAL'e taşımak.
Neden önemli
Polling, outbox'ı basit tutuyor ve worker ekleyerek ölçekleniyor; WAL reader ise gecikme gerçekten önemliyse devreye alınacak bir seçenek, karşılığında replication slot, WAL disk yönetimi ve daha fazla duplicate ile uğraşıyorsunuz.
Benzer haberler
- Critter Stack: mesaj kaybı, kapanış ve multi-tenancy düzeltmeleriJasperFx (Critter Stack) Blog30 Eki
- Range türleriyle kulübe rezervasyonu: .NET 10'da çakışmayı veritabanı önlerDEV Community: #csharp30 Eki
- CritterWatch 1.1 dead letter'ları hata nedenine göre gruplayıp tek işlem olarak yönetirJasperFx (Critter Stack) Blog29 Eki