Linux 7.0 PostgreSQL’u Nasıl Bozdu
(read.thecoder.cafe)- Linux 7.0’da, önceki sunucu varsayılanı olan PREEMPT_NONE preemption modu kaldırılınca, aynı donanım üzerinde PostgreSQL throughput’unda yarı yarıya düşüşe varan ciddi bir performans regresyonu ortaya çıktı
- Bir AWS mühendisi, 96-vCPU’lu Graviton4 makinede pgbench çalıştırdığında, Linux 6.x’e kıyasla Linux 7.0’da saniye başına işlem sayısının 98.565’ten 50.751’e düştüğünü ve CPU’nun %55’inin tek bir spinlock fonksiyonunda harcandığını gördü
- PostgreSQL’in shared buffer pool erişimini koruyan spinlock, 4KB bellek sayfalarındaki minor page fault’larla birleşince, kilit tutulurken scheduler tarafından preemption yaşanması durumunda bekleyen tüm backend’ler CPU’yu boşa döndürmeye başlıyor
- Huge Pages (2MB veya 1GB) etkinleştirildiğinde, potansiyel page fault sayısı 31 milyondan on binler ya da yüzler seviyesine düşerek regresyon ortadan kalkıyor
- Kernel tarafı Restartable Sequences (rseq) kullanılmasını önerse de, PostgreSQL topluluğu kernel yükseltmesinden kaynaklanan performans düşüşünün başlı başına “userspace’i bozmaz” ilkesine aykırı olduğu görüşünde
Sorunun görünümü
- AWS mühendisi Salvatore Dipietro, 96-vCPU Graviton4 işlemcide pgbench çalıştırarak scale factor 8.470 (yaklaşık 847 milyon satırlık tablo), 1.024 istemci ve 96 thread yapılandırmasıyla yüksek paralellikli yük testi yaptı
- Linux 6.x’te 98.565 TPS, Linux 7.0’da 50.751 TPS ile throughput neredeyse yarıya düştü
perfprofilleme sonucunda CPU süresinin %55,60’ınıns_lockfonksiyonu içinde harcandığı görüldü- Çağrı yolu:
StartReadBuffer→GetVictimBuffer→StrategyGetBuffer→s_lock
- Çağrı yolu:
Preemption nedir?
- OS scheduler’ın çalışan bir thread’i durdurup CPU’yu başka bir thread’e vermesi preemption olarak adlandırılır
- Linux 7.0 öncesinde üç seçenek vardı
- PREEMPT_NONE: Thread, gönüllü olarak CPU’yu bırakana kadar (
syscall, I/O bloklanması, sleep) neredeyse hiç kesilmez. Geleneksel sunucu varsayılanıydı; context switch sayısı az, throughput yüksekti - PREEMPT_FULL: Güvenli kabul edilen hemen her noktada çalışan thread durdurulabilir. Yanıt süresi azalır ama context switch overhead’i artar. Geleneksel masaüstü varsayılanıydı
- PREEMPT_LAZY: Linux 6.12’de gelen bir ara çözüm; doğal sınırları beklerken gerektiğinde preemption’a izin verir. PREEMPT_NONE’ın throughput özelliklerini yaklaşık olarak korumak için tasarlandı
- PREEMPT_NONE: Thread, gönüllü olarak CPU’yu bırakana kadar (
- Linux 7.0’da PREEMPT_NONE modern CPU mimarilerinde kaldırıldı ve geriye yalnızca PREEMPT_FULL ile PREEMPT_LAZY kaldı
- PREEMPT_LAZY çoğu sunucu yazılımında yerine geçebilse de, PostgreSQL’de kritik bir fark yarattı
PostgreSQL bellek yönetimi
- PostgreSQL, sabit boyutlu veri sayfalarını (varsayılan 8KB) temel depolama birimi olarak kullanır; tablo satırları, B-tree indeks düğümleri ve metadata bu sayfalarda tutulur
- Disk okumalarını azaltmak için, yakın zamanda okunan veri sayfalarını büyük bir paylaşımlı bellek alanı olan shared buffer pool içinde cache’ler
- Bir istemci bağlandığında özel bir backend process oluşturulur; buffer pool’da olmayan bir sayfa diskten okunur ve ardından boş ya da çıkarılabilir bir buffer bulunması gerekir
- Bu buffer seçimini yapan fonksiyon
StrategyGetBuffer’dır
- Bu buffer seçimini yapan fonksiyon
PostgreSQL’in spinlock’ları
- Spinlock, kilidi beklerken uyumak yerine döngü içinde sürekli kontrol eden bir kilit mekanizmasıdır
- Çok kısa kritik bölgelerde, thread’i uyutup uyandırmanın maliyetinden daha verimli olabilir
- Temel varsayım şudur: Kilidi tutan thread çok hızlı şekilde serbest bırakacaktır
StrategyGetBuffer, buffer seçimini korumak için tek bir global spinlock kullanır- 96-vCPU ve 1.024 istemcili ortamda tüm backend’ler aynı kilit için yarışır
Sanal bellek ve TLB
- Tüm process’ler sanal bellek adresleri kullanır ve donanım bunları page table’lar (çok seviyeli ağaç yapısı) üzerinden fiziksel adreslere çevirir
- Her seferinde page table üzerinde gezinmek yavaş olduğundan, CPU son çevirileri cache’leyen bir TLB (Translation Lookaside Buffer) bulundurur
- TLB hit hızlı erişim sağlar; TLB miss olduğunda page table walk gerekir ve bu zaman alır
- Linux, lazy allocation ilkesini kullanır; sanal bellek ayrıldığında gerçek fiziksel sayfa ilk erişimde eşlenir
- İlk erişimde minor page fault oluşur: kernel fiziksel sayfayı ayırır, mapping’i kaydeder ve bu işlem normal okuma/yazmadan mikrosaniye düzeyinde daha yavaştır
4KB sayfaların sorunu
- Benchmark’ta
shared_buffers120GB olarak ayarlandı; 4KB bellek sayfası bazında bu yaklaşık 31 milyon bellek sayfası, yani 31 milyon potansiyel ilk erişim page fault’u demek - 120GB shared buffer pool kullanan uzun süreli benchmark’larda yeni bellek bölgeleri working set’e sürekli girdiğinden, page fault’lar yalnızca başlangıçta değil, sürekli olarak meydana gelir
StrategyGetBufferiçinde spinlock tutulurken paylaşımlı belleğe erişildiğinde, ilgili bölge henüz eşlenmemişse minor page fault oluşur- PREEMPT_NONE (Linux 7.0 öncesi): Backend A page fault handler’a girse bile, gönüllü yeniden zamanlama noktalarından kaçındığı için fault çözülmeden schedule out edilme olasılığı düşüktür. Bekleme süresi uzar ama etkisi sınırlı kalır
- PREEMPT_LAZY (Linux 7.0 sonrası): Scheduler, backend A’yı page fault handler içindeyken preempt edip başka bir process’i çalıştırabilir. Fault tamamlandıktan sonra bile scheduler kontrolü geri verene kadar ek bir
tbekleme süresi oluşur- Bu ek süre yalnızca
tdeğil, o anda spin halinde bekleyen tüm backend sayısı × t kadar CPU israfına dönüşür - 96-vCPU ve yüzlerce backend bulunan ortamda bu çarpan etkisi yıkıcı olur; sonuçta CPU’nun %56’sı
s_lockiçinde harcanır
- Bu ek süre yalnızca
Huge Pages ile çözüm
shared_buffers120GB iken, bellek sayfası boyutu değiştirildiğinde potansiyel page fault sayısı dramatik biçimde azalır- 4KB sayfalar: ~31.000.000 potansiyel page fault
- 2MB Huge Pages: ~61.440
- 1GB Huge Pages: ~120
- Sayfa boyutunun artması yalnızca page fault sayısını azaltmaz, aynı zamanda TLB baskısını da hafifletir: çok daha az TLB girdisiyle aynı bellek kapsanabildiği için TLB miss ve page table walk sayısı düşer
StrategyGetBuffer, kilit tutulurken fault üretmemeye başlar; böylece kilit sahibi hızlıca tamamlar, diğer backend’ler milisaniyeler yerine yalnızca mikrosaniyeler bekler. Regresyon ortadan kalkar- PostgreSQL’de huge pages ayarı
huge_pagesparametresiyle kontrol ediliroff,on,try(varsayılan) olmak üzere üç değer desteklenirtry, mümkünse huge pages kullanır; değilse sessizce 4KB’ye fallback yapar, bu da yanlış yapılandırmanın fark edilmemesi riskini doğururonolarak ayarlanırsa, huge pages kullanılamadığında PostgreSQL başlangıçta hata verir ve sorun hemen fark edilir
- Trade-off: huge pages ön ayırma ve rezervasyonla çalışır; PostgreSQL hepsini kullanmasa bile bu bellek sistemin geri kalanı tarafından kullanılamaz. Sayfanın yalnızca bir kısmı kullanılırsa kalanı boşa gider. Ancak büyük
shared_bufferskullanan üretim ortamlarında bu trade-off çoğunlukla kabul edilebilir
Sonraki gelişmeler
- Preemption değişikliğini tasarlayan Intel kernel mühendisi Peter Zijlstra, PostgreSQL’in Restartable Sequences (rseq) benimsemesini önerdi
rseq, userspace kodunun kritik bölüm sırasında preemption veya migration olup olmadığını algılayıp ilgili bölümü yeniden başlatabilmesini sağlayan bir Linux kernel özelliğidir- PostgreSQL’in spinlock yoluna
rsequygulanırsa, preempt edilen kilit sahibinin bekleyen tüm backend’leri geciktirmesi senaryosundan kaçınılabilir
- PostgreSQL topluluğunun tepkisi olumsuz oldu
- Linux 7.0 öncesinde ücretsiz elde edilen performansı geri kazanmak için ek bir kernel özelliğinin benimsenmesi kabul edilebilir bulunmuyor
- Bunun, kernel’in uzun süredir benimsediği “userspace’i bozmaz” ilkesine aykırı olduğu düşünülüyor; yani kernel yükseltmesinden önce düzgün çalışan yazılımın yükseltmeden sonra da düzgün çalışması gerekir
Henüz yorum yok.