GoProgramlamaKıdemli Go Backend Geliştirici

**sync.Map** hangi özel koşul altında atomik olarak kirli depolama alanındaki girişleri kilобстаны koruması altındaki okuma haritasına terfi ettirir?

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

Sorunun Cevabı

Sync.Map, kilitlenmemiş okuma ve kilitli yazma işlemleri arasında içeriğin rekabetini azaltmak için dikkatlice ayrılan kilitli ve kilitsiz işlemler aracılığıyla içerik çelişkisini kontrolü sağlamak amacıyla çift harita mimarisi kullanır. Yapı, kilitlenmemiş okuma haritasında (read) yer alan girişleri tutmak için atomik işaretçilere sahip bir okuma haritası için atomik bir işaretçi korur; bu, bu katmanda anahtarlar mevcut olduğunda kilitlenmemiş aramaları mümkün kılar. Okuma haritasında yazma veya önbellek hataları için, son yapılandırmaları içeren bir süper küme anahtarlarının bulunduğu mutex korumalı bir dirty haritasına geri döner. Bu katmanlar arasındaki geçişi yöneten kritik bir terfi göstergecisi: atomik misses sayacı (okuma haritasında başarısız aramaları izleyerek) dirty haritasının uzunluğunu aştığında, çalışma zamanı atomik olarak kirli haritayı yeni okuma haritası olarak terfi ettirir.

İç yapısı bu atomik işlemleri sağlamak için özel yapılar kullanır:

type readOnly struct { m map[any]*entry amended bool // dirty'de read'de olmayan anahtarlar varsa true } type entry struct { p atomic.Pointer[any] // gerçek değer veya silinmişse nil }

Bu yapılar, çalışma zamanının haritaları atomik olarak değiştirmesine olanak tanırken, eş zamanlı goroutine'ler için güvenli erişimi de sağlar ve terfi eşiği, çift arama maliyetinin birçok erişim üzerine amortize edilmesini garanti eder.

Hayattan Bir Durum

Dağıtık sistemler ekibimiz, 100k+ QPS işleyen yüksek hacimli bir meta veri hizmetinde ciddi gecikme zirveleriyle karşılaştı. Hizmet, UUID ile anahtarlanmış yapılandırma nesnelerini önbelleğe almış olup, trafiğin %95'i sıcak anahtarların %5'ine düşmüştü, arka plandaki goroutine'ler sürekli olarak yeni dağıtılan hizmetler için yeni yapılandırmaları ekliyordu.

Çözüm 1: sync.RWMutex ile harita

İlk uygulama, sync.RWMutex ile korunan standart bir harita kullandı. Kavramsal olarak basit olmasına rağmen, yüksek eşzamanlılık altında bu yaklaşım, tüm okuma goroutine'lerinin, kilit durum kelimesinin önbellek satırları için rekabet ettiği için ciddi bir çelişkiye maruz kaldı. Arka plandaki yazarlar yeni yapılandırmalar eklemek için yazma kilidini aldıklarında, tüm okuyucular engellendi ve önbellek yenileme döngüleri sırasında p99 gecikme zirveleri 500 ms'nin üzerinde çıktı.

Çözüm 2: Çatallı mutex yaklaşımı

Sonrasında, 256 sync.RWMutex örneği ile anahtar tabanlı dağılım kullanan çatallı bir harita prototipini oluşturduk. Bu tasarım, yükü farklı önbellek satırları ve ayrı mutex’ler arasında yayarak çelişkiyi azalttı. Ancak, yeniden boyutlandırma sırasında tutarlı hash’leri korumada önemli karmaşıklık getirdi ve kaçınılmaz sıcak anahtarlar, hala kuyruk gecikme zirveleriyle karşılaşan dengesiz parçalar oluşturdu.

Çözüm 3: sync.Map

Profiling’in belirgin erişim desenlerini doğruladıktan sonra nihayet sync.Map'i benimsedik: okumalar, kararlı, uzun ömürlü anahtarları hedef alırken, yazmalar geçici yeni anahtarlar tanıttı. Okuma yolundaki kilitsiz atomik yükler tamamen önbellek satırı sarsıntısını ortadan kaldırdı ve otomatik terfi göstergecisi, özel iş yükü özelliklerimize uygun hale geldi. Tek iş parçacıklı verim, basit bir haritadan yaklaşık %20 daha düşük olmasına rağmen, mutex çelişkisi ortadan kaldırıldığı için yüksek yazma patlamaları sırasında p99 gecikmesini 5 ms'nin altına indirdi.

Dağıtım, kuyruk gecikmesi stabilitesinde 100 kat iyileşme sağladı ve yapılandırma yenilemeleri sırasında goroutine yığılmalarını tamamen ortadan kaldırdı. Hizmetin kullanılabilirliği, yoğun trafik dönemlerinde %99,9'dan %99,99'a yükseldi ve aylarca operasyonel süre boyunca sıfır bellek sızıntısı gözlemledik.

Adayların Sıklıkla Gözden Kaçırdığı Şeyler

*sync.Map neden değerleri entry işaretçileri olarak saklar, doğrudan interface{} değerleri yerine ve bu, kilitsiz silmeyi nasıl mümkün kılar?

read haritası, kilitsiz silmeyi, harita yapısını değiştirmeden mümkün kılmak için *entry yapılarını saklar. Bir anahtarı silerken, sync.Map, girişin iç işaretçisini atomik karşılaştır ve değiştir işlemleri kullanarak nil ile atomik olarak değiştirir; boş bir yer işaretleyerek harita girişini sağlam tutar. Silmeler sırasında okunacak harita yapısının bu değişmezliği, eşzamanlı okuyucuların kilit olmadan çalışabilmesini sağlar; ancak, silinen anahtarlar, bir sonraki terfi döngüsü bunları kaldırana kadar bellek tüketmeye devam eder.

sync.Map, kirli haritayı okuma için ne zaman terfi ettireceğini nasıl belirler? Bu özel eşik performans için neden önemlidir?

Terfi, atomik misses sayacının, okuma haritasında başarısız aramalar sırasında artırdığı, dirty haritasının uzunluğunu aştığında gerçekleşir. Bu eşik, çift arama cezalarının maliyetinin, tüm dirty haritasının read atomik işaretçisine kopyalanma masrafını aşmasını garantiler. Tetiklendiğinde, dirty haritası atomik olarak okuma haritasına terfi ettirilir, dirty haritası nil olarak ayarlanır ve kaçırmalar sıfıra sıfırlanır, böylece terfi maliyeti, birçok başarısız arama üzerinde amortize edilmiş olur.

Kirli haritanın okuma haritasına atomik terfisi sırasında eşzamanlı okuyucuların, kısmen güncellenmiş harita durumlarını gözlemlemeden devam etmesini sağlayan mekanizma nedir?

Terfi sırasında, kod, read alanının işaretçisini önceki dirty haritasına yönlendirecek şekilde atomik bir işaretçi değişimi gerçekleştirir ve Go'nun bellek modeli, bunun tüm goroutine’ler için atomik olarak görünür olduğunu garanti eder. Eşzamanlı okuyucular, ya eski read haritasını ya da yeni terfi ettirilmiş haritayı gözlemler, ama asla geçersiz ya da kısmen oluşturulmuş bir durumu gözlemlemezler; çünkü harita atamaları işaretçi değişiminden önce tamamlanır. Eski read haritası, in-flight okuyucular için erişilebilir kalır; çünkü Go'nun çöp toplayıcısı, tüm referanslar bırakılana kadar onu geri almaz ve bu, sync.Map'in kilitsiz yapısal geçişler için çöp toplama nasıl kullandığını gösterir.