Sorunun geçmişi
Go, 1.3 sürümünde sync.Pool'u, geçici nesneleri önbelleğe almak ve çöp toplayıcı üzerindeki baskıyı azaltmak için bir mekanizma olarak tanıttı. Tasarım, bellek verimliliği pahasına hız için işlemci başına (P) yerel önbellekler tutarak kilitlerden bağımsız performansı önceliklendirdi. Bu mimari, yüksek eşzamanlılık altında beklenen geleneksel nesne havuzlama davranışını sürprizli hale getiren belirli başarısızlık modları yaratır.
Sorun
Gorutinler Get() çağrısı yaptıklarında, yalnızca mevcut P'nin yerel önbelleğine erişirler. Eğer o önbellek boşsa, diğer P'lerden nesne çalarlar; ancak gorutin göçü sonrası önceki P'lerden nesneleri geri alamazlar. GOMAXPROCS 32 olarak ayarlandığında, her P yüzlerce nesneyi biriktirebilir, bu da çarpan bellek büyümesine neden olur. Ayrıca, sync.Pool, GC döngüleri sırasında tüm nesneleri temizleyerek havuz boşaldığında yeni tahsislere zorlar, bu da tahsis oranları GC sıklıklarını aştığında durumu karmaşıklaştırır.
Çözüm
Geliştiriciler, sync.Pool'un sınırlı önbelleklemeden ziyade en iyi çaba ile yeniden kullanım sağladığını kabul etmelidirler. Bellek kısıtlı uygulamalar için, atomik sayıcılar veya kanallar kullanarak açık boyut sınırları olan özel parçalı havuzlar uygulayın. Alternatif olarak, başlangıçta sabit boyutta tampon havuzları ön tahsis edin ve ara sıra tahsis başarısızlıklarını veya engellemeleri kabul edin, böylece yığın büyümesi tahmin edilebilir kalır.
var bufferPool = sync.Pool{ New: func() interface{} { return new([4096]byte) }, } func handler() { // Her P bağımsız önbellek tutar buf := bufferPool.Get().(*[4096]byte) // Veriyi işleme... bufferPool.Put(buf) // Sadece mevcut P'nin önbelleğine geri döner }
Bir finansal ticaret platformu, []byte tamponları için sync.Pool kullanarak saniyede 50.000 piyasa veri mesajı işledi. GOMAXPROCS 32 olarak ayarlandığında yük testleri sırasında yığın kullanımı dakikalar içinde 8GB'a fırladı. Bu, teorik olarak gereken maksimum tampon alanı yalnızca 500MB olmasına rağmen OOM öldürmelerine neden oldu ve kritik bir üretim engeli oluşturdu.
Mühendislik ekibi, havuza dönen tampon boyutlarını sınırlamaya çalıştı ve tahsisleri 1KB ile sınırladı. Bu, nesne başına belleği azalttı ancak kök nedeni ele almadı—her P hala bağımsız olarak kendi tampon önbelleğini biriktiriyordu. 32 işlemci eş zamanlı çalışırken, çarpan etkisi, sınırsız büyümeye neden olmaya devam etti.
İkincisi, belirli boyutlu kanallar etrafında sync.RWMutex korumaları kullanarak özel parçalı bir havuz uyguladılar. Bu, bellek kullanımını başarıyla sınırladı ve OOM hatalarını önledi. Ancak, kilit yarışması %40 oranında verimliliği düşürdü, bu da gecikme hassasiyetine sahip ticaret gereksinimleri için kabul edilemez hale geldi.
Son olarak, sync.Pool'u, kilitsiz indeksleme için atomik işlemler kullanan manuel boyutlandırılmış bir halka tampon havuzuyla değiştirdiler. Bu, bellek kullanımını 2GB ile sınırladı ve verimliliği korudu, havuz boşaldığında ara sıra tahsislerin meydana geleceğini kabul etti.
Üçüncü çözümü, tahmin edilebilir bellek kullanımının mükemmel tahsis önlemesinden daha fazla öneme sahip olduğu için seçtiler. Sistem artık 1.5GB yığın kullanımı ile kararlı çalışıyor ve %99'luk yüzde gecikmeleri sürekli olarak 2ms altında kalıyor.
Neden sync.Pool Put() çağrıldıktan sonra bile Get() üzerinde nil döndürüyor?
sync.Pool, nesne saklamayı garanti etmediği için nil döndürebilir. Çöp toplama döngüleri sırasında, çalışma zamanı tüm havuzları tamamen temizler, her önbelleğe alınmış nesneyi son kullanıma bakılmaksızın kaldırır. Ayrıca, bir gorutin P'ler (işlemciler) arasında göç ettiğinde, önceki P'nin yerel önbelleğinde saklanan nesnelere erişemez ve yeni P'nin havuzu boş olduğunda Get() nil döner. Adaylar genellikle sync.Pool'un, nesne sürekliliği garanti eden geleneksel bir önbellek gibi davrandığını varsayıyorlar, ancak yalnızca en iyi çaba ile yeniden kullanım sağlar.
Nesne göstericileri içeren sync.Pool nesnelerini nasıl işler ve bu GC performansı için neden önemlidir?
sync.Pool, göstericiler içeren nesneleri depoladığında, bu nesneler GC taramaları sırasında hayatta kalır çünkü havuz onlara referanslar tutar. Bu, çöp toplayıcının bu nesnelerin işaret ettiği bellek alanını geri alamamasını sağlar ve tüm nesne grafiklerinin bir sonraki GC döngüsü havuzu temizlenene kadar hayatta kalmasına neden olur. Yüksek performanslı sistemler için adayların gösterici içermeyen nesneleri depolaması veya Put()'den önce göstericileri manuel olarak nil yapması, alınan belleği çöp toplayıcının geri almasına izin vererek yığın baskısını önemli ölçüde azaltır.
Eşzamanlı Put() ve Get() işlemleri ile ilgili sync.Pool'un belirli iş parçacığı güvenliği garantileri nelerdir?
sync.Pool, dış senkronizasyona gerek kalmadan birden fazla gorutin tarafından eşzamanlı kullanım için tamamen güvenlidir. Ancak, adaylar genellikle sync.Pool'un Son-Giren-İlk-Çıkan veya İlk-Giren-İlk-Çıkan sıralamasını garanti etmediğini atlarlar—geri alma sırası, P planlamasına bağlı olarak rastgeledir. Dahası, Get() ile döndürülen nesne sıfırlanmamıştır; önceki kullanıcının bıraktığı durumu içerir ve veri yarışlarını önlemek için manuel sıfırlama gerektirir.