Tarihçe
Erken Go uygulamaları sabit boyutlu yığınlar tahsis ediyordu (her goroutine için 1KB), bu da yüksek eşzamanlılıkla bellek tükenmesine veya derin özyineleme sırasında taşmaya neden oluyordu. Dil, erken sürümlerde segmentli yığınlardan (bağlantılı parçalar) Go 1.3+ ile devamlı yığın kopyalamaya evrildi, bu da önbellek yerelitesini iyileştirir ve işaretçi yönetimini basitleştirir.
Problem
Bir goroutine mevcut yığın segmentini tükenirdiğinde, runtime daha büyük bir bellek alanı tahsis etmeli ve tüm mevcut yığın verilerini taşımalıdır. Bu taşımada yığın değişkenlerini referans alan işaretçilerin geçersiz hale gelme riski vardır, çünkü bellek adresleri move sırasında değişir, bu da bellek bozulmasına veya çökmesine neden olabilir.
Çözüm
Derleyici, her işlev girişinde bir yığın kontrolü ön yazısı ekler, yığın işaretçisini koruma sayfası ile karşılaştırır. Eğer alan yetersizse, runtime.morestack'i çağırır; bu, yeni bir yığın (genellikle boyutunu iki katına çıkararak) tahsis eder, eski içeriği kopyalar ve diğer yığın konumlarına işaret eden yığın içindeki tüm işaretçileri bulup ayarlamak için derleyici tarafından üretilen işaretçi bitmap'lerini kullanır.
Kod Örneği
Aşağıdaki işlev, yığın değişkenlerine işaretçilerin, yığın derinliği büyüdüğünde bile geçerli kalmasını gösterir:
func Calculate(depth int, prev *int) int { if depth == 0 { return *prev } // current yığında tahsis edilir current := depth * 100 // ¤t eski yığın konumuna işaret edebilir // Eğer yığın burada büyüyorsa, runtime işaretçiyi günceller return Calculate(depth-1, ¤t) + *prev }
Yürütme yeni yığın üzerinde güncellenmiş registerlar ile devam eder, böylece tüm işaretçiler doğru yeni adreslere erişir.
Senaryo
Finansal eşleştirme motoru, yığın derinliği başlangıç 2KB yığın tahsisini aştığında, yüksek volatilite piyasa olayları sırasında ara sıra çökme yaşadı. Sistem, eşzamanlı bağlantıları yöneten milyonlarca hafif goroutine'i tehlikeye atmadan, özyinelemeli algoritmaların netliğini koruyan bir çözüm gerektiriyordu.
Problem
Eşleştirme algoritması, ağaç biçimindeki sipariş derinliğini geçmek için derin özyineleme kullandı, bu da ticaret hacmi zirve yaptığında yığın taşmasına neden oldu. Çözüm, genellikle boşta kalan goroutine'ler için büyük yığınlar tahsis etmeden, sınırsız özyinelemeyi güvenli bir şekilde ele alabilmeliydi.
Çözüm 1: Sabit Büyük Yığınlar
Tüm goroutine'ler için büyük yığınları önceden tahsis etmek için debug.SetMaxStack'i kullanmak veya runtime varsayımlarını değiştirmek. Artıları: Büyüme üzerine yük ve taşma riskini tamamen ortadan kaldırır. Eksileri: Boşta kalan bağlantı işleyicileri için aşırı bellek tasarrufu yapar, hafif goroutine vaadini ihlal eder ve maksimum uygun eşzamanlılığı azaltır.
Çözüm 2: İteratif Dönüşüm
Özyinelemeli ağaç geçişini, geçiş durumunu takip etmek için açıkça yığın tahsis edilmiş bir kesirle iteratif bir algoritmaya yeniden yazmak. Artıları: Tahmin edilebilir bellek kullanımı ve taşma riski yok. Eksileri: Artan kod karmaşıklığı, algoritmik netlik kaybı ve yüksek hacimli ticaret sırasında sık kesir tahsisatı nedeniyle ek bellek yönetim baskısı.
Çözüm 3: Dinamik Yığın Büyümesi
Özyinelemeli tasarımı korumak ancak Go'nun devamlı yığın büyümesine güvenmek; derleyicinin doğru işaretçi haritaları ile işlev çerçevelerini optimize ettiğinden emin olmak. Artıları: Temiz özyinelemeli mantığı korur, bellek ihtiyacıyla orantılı olarak kullanır ve kod değişiklikleri olmadan trafik zirvelerini otomatik olarak ele alır. Eksileri: Yığın kopyalama sırasında mikro saniyelik duraklamalar, ancak bunlar küçük varsayılan yığınlar ve verimli kopyalama ile hafifletilir.
Seçilen Yaklaşım
Çözüm 3, yığın kopyalamaya ilişkin 100 nanosaniyelik aşırı yükün ağ gecikmesine oranla önemsiz olduğunu ve özyinelemeli eşleştirme algoritmasının matematiksel netliğini koruduğunu belirttikten sonra seçilmiştir. Sonsuz döngülerin 1GB yığın tüketmesini önlemek için özyinelemeli derinlik sınırlamaları ekledik.
Sonuç
Sistem, piyasa stres testleri sırasında 50.000 eşzamanlı özyinelemeli hesaplamayı çökmeden sürdürebildi. Bellek kullanımı 100.000 goroutine için 300MB'ın altında kaldı ve yığın büyüme olayları sırasında p99 gecikmesi 2 mikro saniyeden daha az arttı, sıkı yüksek frekanslı işlem gereksinimlerini karşıladı.
Yığın kopyalamanın yığın değişkenlerine işaret eden işaretçileri neden bozmadığını, yığın yeni bir bellek adresine taşındığında?
runtime, her işlev için derleyici tarafından üretilen yığın haritalarına (bitmap'lere) dayanır. Bu haritalar, yığın çerçevesindeki hangi slotların işaretçi içerdiğini belirler. runtime.copystack sırasında, runtime bu haritalar aracılığıyla iterasyon yapar, eski yığın aralığına işaret eden her işaretçiyi bulur ve bunu yeni yığında karşılık gelen ofsete günceller. Bu, fiziksel bellek adresi değişse bile, tüm referansların geçerli kalmasını ve doğru yeni konumlara işaret etmesini sağlar.
Go, CGO çağrıları sırasında yığın büyümesini ve bunun Go yığın verilerine işaret eden işaretçileri nasıl yönetir?
CGO yürütmesi, C koduna girmeden önce her zaman sistem yığınına (g0) geçer. runtime, C işlevlerine herhangi bir goroutine yığın işaretçisinin maruz kalmadığından emin olur. C kodu çalışırken (ayrıca bir goroutine aracılığıyla) yığın büyümesi yaşanırsa, C yığını etkilenmez. C'den Go'ya dönerken, runtime, runtime.entersyscall geçişi sırasında kaydedilen güncellenmiş yığın işaretçisini kullanarak (potansiyel olarak taşınmış) goroutine yığınına geri döner.
"runtime: goroutine yığını 1000000000 bayt limitini aşıyor" kritik hatasının nedeni nedir ve bu normal büyümeden nasıl farklıdır?
Normal yığın genişlemesi daha büyük bir devamlı alana kopyalanırken, bu hata runtime.morestack'in istenen büyümenin katı sınırı (64-bit sistemlerde 1GB) aşacağını algıladığında ortaya çıkar. Bu, sınırsız özyineleme veya azgın tahsisi gösterir. Normal büyüme şeffaf ve kopyalama temellidir, bu sınıra ulaşmak ise hemen bir panik yaratır çünkü runtime bellek talebini tehlikeye atmadan yerine getiremez ve yürütmeye devam etmek tehlikeli olur.