GoProgramlamaGo Backend Geliştirici

Go'nun test paketi içindeki hangi senkronizasyon ilkelisi, alt test hiyerarileri için `-parallel` bayrağı sınırlarını yönetir?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı.

Tarihçe

Go test çerçevesi, büyük kod tabanlarında CI boru hatlarının uzun süresini ele almak için t.Parallel()'ı tanıttı. Çok çekirdekli işlemcilerin yaygın olarak benimsenmesinden önce, testler varsayılan olarak sırasıyla çalışıyordu. Projeler binlerce teste ölçeklendiğinde, tamamen sıralı yürütme bir darboğaz haline geldi, ancak sınırsız paralelizm, dosya tanıtıcıları veya veritabanı bağlantıları gibi işlem kaynaklarının tükenmesi riskini taşıyordu. Tasarım amacı, her test paketi için geliştiricilerin işçi havuzlarını manuel olarak düzenlemek veya karmaşık senkronizasyonlar gerektirmeden, küresel bir sınırı koruyarak yerleşik, isteğe bağlı bir eşzamanlılık modeli sağlamaktı.

Problem

Bir geliştirici t.Parallel() çağırdığında, test, diğer testlerle eşzamanlı çalışabileceğini çalıştırıcıya bildirmelidir. Ancak, çerçeve, kaynak açlığına neden olmamak için sıkı bir eşzamanlılık sınırı (varsayılan olarak GOMAXPROCS ama -parallel bayrağı ile yapılandırılabilir) uygulamak zorundadır. Zorluk, iç içe geçmiş alt testlerde artar: bir ana test, t.Run'ı birden fazla kez çağırabilir ve her alt test bağımsız olarak t.Parallel()'ı çağırabilir. Çözüm, ana testin tüm soyundan gelenler bitmeden yürütme yerini bırakmasını önlemeli ve derinlemesine iç içe geçmiş paralel alt testlerin aynı küresel havuzdan doğru şekilde yer almak için ana testi kilitlemeden slotları almasını sağlamalıdır.

Çözüm

testing paketi, -parallel bayrağı değeri ile boyutlandırılmış boş yapıların ( chan struct{} ) bir buffer kanalı olarak uygulanmış bir semafor kullanır. Bu kanal bir paketteki tüm testler arasında paylaşılır. Her T örneği, bu parallel kanalına ve ebeveyni ile koordinasyon sağlamak için bir iç signal kanalına bir referans tutar.

t.Parallel() çağrıldığında:

  1. Ebeveyn t.Run çağrısını bloklamak için signal kanalını kapatır, böylece ebeveyn, alt test çalışırken devam edebilir veya sona erebilir.
  2. Mevcut goroutine, parallel semafor kanalına gönderme yaparak bir yürütme slotu alır.
  3. Test işleyicisinde bir gecikmeli fonksiyon, test işlevi döndüğünde ve tüm t.Cleanup kancaları yürütüldüğünde parallel kanalından almak suretiyle slotu serbest bırakır.

Hiyerarşiler için, t.Run, alt test tamamen tamamlanana kadar ana goroutine'i bir sync.WaitGroup kullanarak engeller, alt test paralel çalışsa bile. Bu, ebeveynin kendi slotunu (veya beklemeyi) tutmasını sağlar, alt testlerin tamamı bitene kadar, küresel sınırın derin bir şekilde iç içe geçmiş paralel testler tarafından aşılmasını önler.

// Test paketinin iç yapısının kavramsal modeli type T struct { parallel chan struct{} // Paylaşılan semafor signal chan struct{} // Ebeveyne Parallel() çağrıldığını bildirir parent *T wg sync.WaitGroup // Alt testler için bekler } func (t *T) Parallel() { // Ebeveyni devam ettir close(t.signal) // Küresel havuzdan yer al t.parallel <- struct{}{} // Temizlik sona erdiğinde slotu serbest bırakır t.Cleanup(func() { <-t.parallel }) } func (t *T) Run(name string, f func(t *T)) bool { t.wg.Add(1) sub := &T{parallel: t.parallel, signal: make(chan struct{})} go func() { defer t.wg.Done() f(sub) }() <-sub.signal // Alt testin başlamasını bekle veya Parallel çağrısını yap t.wg.Wait() // Tamamlanmayı bekle return !sub.Failed() }

Hayattan bir durum

Bağlam

Bir platform ekibi, mikro hizmet mimarisi için 2.000 entegrasyon testini içeren bir monorepo'yu sürdürüyordu. Her test, Postgres ve Redis için ephemeral Docker konteynerlerini oluşturuyordu. Testlerin sıralı çalıştırılması 45 dakika alıyordu, bu da hızlı geri bildirim imkânını imkânsız hale getiriyordu. Ancak go test -parallel 100 çalıştırmak, CI koşucularının çekirdek max_user_namespaces limitini tüketmesine neden oldu, bu durumda ana makine çöktü ve derleme önbelleği bozuldu.

Problem

Ekip, konteyner yoğun testleri beş eşzamanlı örnekle sınırlamak zorundaydı ki bu da çekirdek sınırlarına saygı gösteriyor, aynı zamanda saf birim testlerin maksimum verimlilik için -parallel 32 ile çalışmasına izin veriyordu. Go'nun standart test paketi, aynı çalıştırmada farklı test kategorileri için farklı sınırları uygulamak için yerleşik bir yol sunmadan, yalnızca her çağrıda tek bir küresel -parallel değeri kabul eder.

Düşünülen Çözümler

Dış düzenleme ile Bazel. Bazel'a geçiş önerildi çünkü test bölme ve kaynak etiketleme destekliyordu (örneğin, tags = ["resources:postgres:1"]). Bu, programlayıcının eşzamanlı veritabanı testlerini tam olarak sınırlamasına olanak tanıyordu. Ancak, bu tüm yapı sisteminin yeniden yazılmasını ve go test'in basitliğini kaybetmeyi gerektiriyordu. Öğrenme eğrisi dikti ve yerel geliştirme iş akışları köklü biçimde değişti, Bazel'ın sorgu diline aşina olmayan geliştiricileri yavaşlattı.

Test paketlerinde manuel semafor. Geliştiriciler, bir paket düzeyinde var dbSem = make(chan struct{}, 5) eklemeyi ve her entegrasyon testinin başında bunu manuel olarak edinmesini düşündüler. Bu, ince ayar kontrol sağladı ama önemli bir boilerplate ile birlikte semaforu tutarken bir test paniği halinde kilitlenme riski taşıyordu. Ayrıca, eşzamanlılık modelini parçalamıştı—bazı testler -parallel bayrağına saygı gösterirken, diğerleri özel semaforu dikkate alıyordu—bu durum hata ayıklamayı zorlaştırdı ve kaynak hesabında tutarsızlıklara yol açtı.

Yapı etiketi ayrımı ile CI aşamaları. Ekip, testleri yapı etiketleri kullanarak ayırmayı tercih etti. Tüm konteynerleştirilmiş testlere //go:build integration eklediler ve birim testlerini etiketsiz bıraktılar. CI boru hattı önce birim testleri için go test -short -parallel 32 ./... çalıştırdı, ardından ayrı olarak entegrasyon testleri için go test -tags=integration -parallel 5 ./... çalıştırdı. Bu, mevcut Go araç zinciri özelliklerinden yararlandı, test mantığını değiştirmeden. Dezavantajı, birim ve entegrasyon testleri arasında paketler arası paralelliği kaybetmeleriydi; aşamalar sırasıyla çalıştırıldı. Ancak, birim testleri üç dakikada tamamlandığı için toplam süre (3m + 20m) kabul edilebilir ve kararlıydı.

Seçilen Çözüm ve Sonuç

Yapı etiketi ayrımını tercih ettiler. Bu, yalnızca dosya başlıklarına etiket eklemeyi gerektiriyordu ve özel senkronizasyon olmadan standart testing paketinin semaforunu doğal olarak kullanıyordu. CI, kararlı hale geldi, çekirdek sınırlarına saygı gösterildi ve geliştiriciler yine de hata ayıklama için yerel olarak go test -tags=integration -parallel 4 çalıştırabiliyordu. Toplam CI süresi 45 dakikadan 23 dakikaya düştü ve ana makine çökmesi tamamen durdu.

Adayların Sıklıkla Kaçırdığı Noktalar

Neden t.Parallel()'ı bir goroutine başlattıktan sonra çağırmak, bazen o goroutin'in yanlış test çıktısına kayıt yapmasına veya paniğe neden olmasına yol açar?

t.Parallel() çağrıldığında, mevcut test goroutine semaforda bloke olur ve ana test koşarı bir sonraki test ile devam eder. Ancak, başlatılan goroutine, T örneğini miras alır. Ana test işlevi geri dönerken goroutine hâlâ çalışıyorsa, test paketi T'yi tamamlanmış olarak işaretler ve çıktı tamponlarını kapatır. Yetişkin goroutine'den sonraki t.Log veya t.Error çağrıları "TestX tamamlandıktan sonra goroutine'de kayıt" ile paniğe neden olabilir. Doğru yaklaşım, goroutine'in tamamlanmasını sync.WaitGroup kullanarak senkronize etmek veya t.Cleanup'ın onun için beklemesini sağlamaktır, çünkü t.Parallel() asılı goroutin'ler için otomatik olarak beklemiyor; yalnızca test işlevinin yaşam döngüsünü yürütücü ile koordine ediyor.

Test paketi, bir ana testin, alt testlerin—bazıları da t.Parallel() çağırabilir—tüm yürütmesi tamamlanmadan önce paralelizm slotunu serbest bırakmasını nasıl engelliyor?

T yapısı, bir sync.WaitGroup'ı içerir. Alt test oluşturmak için t.Run çağrıldığında, ana önce t.wg.Add(1) çağırır ve alt test goroutine'ini başlatmadan önce, alt test tamamlandığında t.wg.Done() çağırır. Önemle, alt test kendisi t.Parallel()'ı çağırdığında, hemen ebeveynin WaitGroup'unu azaltır (bu, ebeveynin kendi işlev gövdesini bitirebilmesine izin verir), ancak ebeveyn testinin genel tamamlanması—ve dolayısıyla semafor tokeninin serbest bırakılması, gerçekleştirme zincirinde son bir t.wg.Wait() ile engellenir. Bu, kök paralel testin, tüm serbest ve paralel alt testlerin sonuçlanana kadar slotu tuttuğu ağaç biçiminde bir bekletme yaratır, bu da -parallel sınırının yalnızca aktif goroutin'ler değil, aktif test ağaç sayısını doğru bir şekilde yansıttığını güvence altına alır.

Neden t.Setenv'i t.Parallel()'dan sonra çağırmak paniğe neden olabilir ve bu, Go'da paralel testlerin izolasyon modelini neyi ortaya koyar?

t.Setenv, t.Parallel()'dan sonra çağrıldığında paniğe neden olur çünkü ortam değişkenleri işlem çapında küresel bir durumdur. Paralel testler aynı süreçte eşzamanlı çalışırken; eğer bir test PATH'i değiştirirse ve diğeri onu okursa, sonuç bir veri yarışı ve belirsiz davranış olur. Bunu önlemek için Go'nun test paketi, bir test paralel hale geldiğinde ortamı "donmuş" olarak işaretler ve t.Setenv veya os.Setenv aracılığıyla mutasyona yönelik herhangi bir girişim bir paniği tetikler. Bu, paralel testlerin, tek bir adres alanı içindeki eşzamanlılık için tasarlandığını, ancak değişmez ortak durumu veya açık senkronizasyonu varsaydığını ortaya koyar. Adaylar genellikle t.Parallel()'ın küresel işlem durumunun değiştirilmemesi gerektiği anlamına geldiğini atlar; bu nedenle, yalnızca test paralel değilse durumu geri yüklemek için t.Cleanup kullanılmalıdır, veya testleri tamamen küresel durumdan kaçınacak şekilde tasarlamak gerekir.