1 puan yazan GN⁺ 2 시간 전 | Henüz yorum yok. | WhatsApp'ta paylaş
  • 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ü
  • perf profilleme sonucunda CPU süresinin %55,60’ının s_lock fonksiyonu içinde harcandığı görüldü
    • Çağrı yolu: StartReadBufferGetVictimBufferStrategyGetBuffers_lock

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ı
  • 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

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_buffers 120GB 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
  • StrategyGetBuffer iç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 t bekleme süresi oluşur
    • Bu ek süre yalnızca t değ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_lock içinde harcanır

Huge Pages ile çözüm

  • shared_buffers 120GB 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_pages parametresiyle kontrol edilir
    • off, on, try (varsayılan) olmak üzere üç değer desteklenir
    • try, 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ğurur
    • on olarak 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_buffers kullanan ü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 rseq uygulanı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.

Henüz yorum yok.