Partial index on beş dakikada üç yüz seksen kat şişti — ve autovacuum bir kez bile gelmedi
Sürekli churn altındaki bir kuyruk tablosunda partial index'in küçüklüğü kalıcı mı, ve varsayılan autovacuum ayarları ona yetişiyor mu?
Bulgu
Canlı küme beş bin satırda sabit dururken partial index 0,1 MB'dan 38,2 MB'a çıktı — üç yüz seksen kat. Küçüklüğü canlı satır sayısından geliyor, şişme hızı iş hacminden, ve ikisi arasında hiçbir bağ yok. Composite index oransal olarak daha az şişti (%42) ama mutlak olarak daha çok (+126 MB) ve şişerken belleğe sığmayı bıraktı: gecikmesi 0,52 ms'den 61 saniyeye çıktı, kuyruğu 126 bine tırmandı. On beş dakikada 1,75 milyon ölü satır birikti ve autovacuum **bir kez bile koşmadı** — varsayılan eşik tablonun tamamına göre ölçekleniyor (50 + 0,2 × 10 milyon ≈ 2 milyon), churn ise küçük canlı kümede oluyor.
- Partial index, 15 dakikada
- 0,1 → 38,2 MB 380×
- Composite index, aynı sürede
- 301 → 427 MB +%42
- Autovacuum koşma sayısı
- 0
- Composite gecikmesi, 900. saniye
- 61.533 ms
Yöntem
Partial index ölçümüyle aynı tezgâh, aynı Postgres 17 ayarları, aynı 10 milyon ölü + 5.000 canlı satırlık tablo. Üç fark var, üçü de bu soruyu sorabilmek için zorunlu. Tüketici satırı `pending`'e geri koymuyor, `done` işaretliyor — sınanan iddia satırın index'ten DÜŞMESİYLE ilgili ve hiç düşmeyen satırda ölçülemez. Yanında sabit varış hızında (2.000/sn) bir üretici koşuyor, yani tablo önceki kaydın eksik ilan etmek zorunda kaldığı insert trafiğini de görüyor. Ve talep, `\gset` yerine alt sorgulu `UPDATE` biçiminde: kısıtsız bir tüketici kuyruğu bir saniyenin altında boşaltıyor ve `\gset` boş sonuçta istemciyi öldürüyor. Tüketici de üretici hızına bağlandı, böylece kuyruk derinliği sabit kalıyor — ve sabit kaldığı örneklenerek doğrulanıyor, varsayılmıyor. On beş saniyede bir tablo boyutu, index boyutu, kuyruk derinliği, ölü satır sayısı ve autovacuum sayaçları kaydedildi; strateji başına 60 örnek.
- Ölçüm tarihi
- Yayın
dün ölçüldü
Ortam
- Postgres
- 17-alpine · shared_buffers 1 GB · autovacuum varsayılan ayarlarda
- Tablo
- 10M ölü + 5.000 canlı satır · 1.421 MB başlangıç
- Yük
- üretici 2.000/sn sabit · tüketici 8 istemci, aynı hızda
- Süre
- strateji başına 900 sn · 15 saniyede bir örnek · 60 örnek
- Donanım
- Apple M4 Pro · 12 çekirdek · 24 GB · Docker Desktop 29.7.2
Teknolojiler
Tekrarlamak için
DURATION=900 ./bench/endurance.sh Partial index ölçümü otuz saniyelik pencerelerle çalıştı ve sınırlar kutusunda şunu yazdı: “30 saniyelik koşular autovacuum’un uzun vadeli etkisini ölçmeye yetmez, bu ayrı bir kayıt konusu.” Bu o kayıt.
Sınanan iddia, Sor Bakalım cevabının tek ölçülmemiş maddesiydi: “Bir satır pending’den done’a döndüğünde partial index’ten düşer — bu iyi. Ama yoğun update trafiği bloat üretebilir; autovacuum’un sağlıklı çalışması önemli. İyi haber: index küçük olduğu için vacuum’u da ucuzdur.”
Bloat eğrisi
Index boyutu, sürekli churn altında
Kuyruk derinliği koşu boyunca beş bin satır civarında sabit. Büyüyen tek şey ölü index girdileri.
- partial
- composite
MB düşük olan iyi Kaynak: pg_relation_size, 15 saniyede bir örnek, bench/endurance.sh
Veri tablosu
| Seri | 0 sn | 135 | 286 | 436 | 587 | 738 | 888 |
|---|---|---|---|---|---|---|---|
| partial | 0,1 MB | 5,9 MB | 12,4 MB | 18,8 MB | 25,3 MB | 31,7 MB | 38,2 MB |
| composite | 301 MB | 321 MB | 344 MB | 366 MB | 388 MB | 409 MB | 427 MB |
Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.
Partial index üç yüz seksen kat büyüdü. Composite %42. Mutlak artış composite’te daha fazla (+126 MB), oransal olarak partial’da karşılaştırılamaz.
Sebep tek cümlede duruyor: pending → done geçişi satırı partial index’ten
düşürüyor, ama ölü girdi vacuum gelene kadar orada kalıyor. Yani index’in
küçüklüğü canlı satır sayısından, şişme hızı iş hacminden geliyor — ve
ikisi arasında hiçbir bağ yok. Beş bin satırlık bir kuyruk, saniyede iki bin iş
işlerken, on beş dakikada otuz sekiz megabaytlık ölü girdi biriktiriyor.
Autovacuum hiç gelmedi
On beş dakikada 1.753.949 ölü satır birikti. Autovacuum sayacı: 0.
Aritmetik acımasız:
eşik = autovacuum_vacuum_threshold + scale_factor × canlı satır
= 50 + 0,2 × 10.005.000
≈ 2.001.050 ölü satır
Eşiğin hemen altında kaldık. Varsayılan ayarlarla bu tablo autovacuum’u yaklaşık on yedi dakikada bir tetikleyecek ve arada index’ler serbestçe şişecek.
Asıl mesele oran değil, ölçekleme yönü: eşik tablonun tamamına göre büyüyor, churn ise küçük canlı kümede oluyor ve tablo büyüdükçe hızlanmıyor. Yani tablo ne kadar büyükse autovacuum o kadar geç geliyor.
Şişen index sürdürülebilir yükü taşımıyor
Her iki koşu da saniyede 2.000 iş hedefiyle başladı.
| Strateji | Başlangıç | 900. saniye | Kuyruk derinliği |
|---|---|---|---|
| partial | 2.024 tps · 1,7 ms | 2.016 tps · 4.906 ms | 5.003 → 14.364 |
| composite hedefi tutamadı | 2.000 tps · 0,52 ms | 1.204 tps · 61.533 ms | 5.000 → 126.024 |
Composite hedefi tutamadı — 2.000’den 1.204 tps’ye düştü, gecikmesi 61 saniyeye çıktı, kuyruğu 126 bine tırmandı. Partial hedefi tuttu.
Bu, otuz saniyelik ölçümün tersi yönde bir sonuç. Orada ikisi neredeyse eşitti (10.795 vs 11.537 tps). Fark sürdürülebilir yükte açılıyor, çünkü 427 megabayta şişmiş bir index artık belleğe sığmıyor ve her tarama diske gidiyor.
Cevaba dönüş
Sor Bakalım cevabının altı maddesinden beşincisi buydu ve tek sınanmayan oydu. Sonuç: madde doğru ama uyarısı zayıf. “Autovacuum’un sağlıklı çalışması önemli” cümlesi, varsayılan ayarlarla autovacuum’un bu tabloda sağlıklı çalışmadığını söylemiyor.
Partial index’i seçmenin bedeli iki kalemde: ölü index girdileri iş hacmiyle birikiyor, ve o birikimi temizleyecek mekanizma tablo bazında ayarlanmadıkça gelmiyor.
İlgili yazılar
Dual-write'tan outbox'a: idempotent tüketim ve alan-seviyesi şifreleme günlüğü
Aynı projede dual-write yüzünden kaybolan event'ler beni transactional outbox'a, at-least-once teslim de idempotent tüketime götürdü. Bir de GDPR alanlarını x-gdpr-sensitive ile satır seviyesinde şifreledim. Gerçek bir projeden notlar.