AdTechSep 10, 202610 dk okuma

RTB Bidder'da Go GC Duraklamaları: Mark Assist, Deadline'lar ve Rust Kararı

Gerçek Zamanlı Teklif VermeRustGarbage CollectionTail Latency
Görsel yüklenemedi

Ortalaması sağlıklı görünürken zaman aşımı yüzünden açık artırma kaybeden bir Go bidder'ı, tek bir belirtinin altında iki sorun taşır: exchange'e ait olan ve gidiş-dönüşü kapsayan bir deadline ve mark assist'i teklifi puanlayan goroutine'e yazan bir collector. Deadline'ın nasıl bölündüğü, hangi Go ayarlarının işe yaradığı ve bir Rust hot path'inin neyi düzeltmediği aşağıda.

Ortalaması sağlıklı görünürken zaman aşımı yüzünden açık artırma kaybeden bir bidder, yanlış sayıyla tarif ediliyordur. Deadline exchange'e aittir ve ağı her iki yönde kapsar. Collector sürecin bir özelliğidir; bu yüzden duraklaması aynı anda her bağlantıya yazılır ve tek bir bağlantıdaki sıçrama başka bir açıklama gerektirir.

Rust'a yeniden yazmak mı, runtime'ı ayarlamak mı sorusu, kimsenin sormadığı bir sorunun iki yanıtı arasında bir seçimdir: deadline'ın hangi kısmı, neye harcanıyor. Aşağıdakiler bütçeyi collector'dan, collector'ı scheduler'dan ve yeniden yazımı bunu hak eden bileşenden ayırır.

Kısa yanıt yapısaldır. Deadline'ı exchange belirler ve ağı da kapsar; bu yüzden ilk onarım, tek bir bağlantının p99'unu handler'da geçen süreye, işlemci bekleme süresine ve hat üzerinde geçen süreye böler. amBrain'in kamuya kanıtlayabildikleri: sıfırdan teslim ettiğimiz bir demand-side platform olan RTBBidder'ı biz kurduk; orada her teklif kararı, gösterim başına onlarca hedefleme koşulunu değerlendirir. Ölçüme dayalı olarak yayımladığımız latency rakamları bir reklam bidder'ından değil trading yollarından gelir ve aşağıdaki hiçbir sayı bize ait bir bidder üzerinde ölçülmemiştir.

Deadline exchange'e aittir ve gidiş-dönüşün tamamını kapsar

OpenRTB spesifikasyonu tmax'ı, exchange'in tekliflerin alınması için tanıdığı, İnternet gecikmesi dahil milisaniye cinsinden azami süre olarak tanımlar ve bu değerin önceki tüm yönlendirmelerin yerine geçtiğini söyler. Bütçe, her isteğin içinde gelen bir gidiş-dönüştür.

Karşısında yargılandığınız eşik, kendi panonuzdaki eşik değildir. Google'ın Authorized Buyers dokümantasyonu, yanıtların %85'inin işlem noktasında ölçüldüğü şekliyle deadline içinde ulaşmasını şart koşar ve bunu tutturamayan bidder'ları kısıtlar. Onun saatiyle sizinkinin arasında, hesaplama olmayan her şey yer alır:

  • Exchange'e gidiş ve dönüş; bu, mühendislikten önce coğrafya ve peering meselesidir
  • Keepalive düştüğünde bağlantı kurulumu; çünkü açık artırma bütçesinin içine sıkışan yeni bir TLS handshake'i kaybedilmiş bir açık artırmadır
  • Handler'ınız isteği görmeden önce accept kuyruğunda geçen süre; bu süre tam da en yoğun olduğunuz anda büyür
  • Deserializasyon; maliyeti isteğin boyutuna göre değil, isteğin ne kadarını nesneye çevirdiğinize göre belirlenir

Dolayısıyla dahili deadline, o bağlantıda bir yanıtın maliyeti olarak kendi histogramınızın söylediği kadar tmax'ın altında durur ve bu, filo geneli için bir kez belirlenmek yerine exchange başına yeniden türetilir. Deadline bir kapasite planı değildir: deadline'da iptal edilen iş CPU'sunu zaten harcamıştır.

Aşırı yük altında bu, kimsenin saymadığı yanıtlar için tam bedel ödemek demektir; dolayısıyla eksik olan onarım kabul denetimidir: tmax'ı okuyun, ölçtüğünüz kuyruk gecikmesiyle karşılaştırın ve aritmetik tutmadığında no-bid yanıtı verin. Hızlı bir no-bid %85'e sayılır; geç bir teklif sayılmaz.

Tek bir collector her bağlantıya birden hizmet eder; dolayısıyla tek bir bağlantı farklı bir arızadır

Garbage collector sürecin bir özelliğidir; bu yüzden herhangi bir bağlantıda tetiklenen bir döngünün faturası her bağlantıya çıkar. İlk ıskalayan, tmax'ı en dar ve isteği en ağır olandır. Aynı tabloyu collector olmadan üreten nedenleri eleyin:

  • Tek bir exchange'den gelen bağlantıların az olması; çünkü HTTP/1.1 tek seferde bir istek taşır: bağlantı başına QPS ile handler süresinin çarpımının bire yaklaşması, kuyruğu bağlantının üzerine bindirir
  • Tek bir HTTP/2 bağlantısı: kaybolan tek bir paket, o bağlantıyı paylaşan her stream'i takılmaya sokar - RFC 9114'ün 2022'de HTTP/3'ün var olma nedeni olarak gösterdiği head-of-line blocking
  • Keepalive'ın düşmesi; bu çoğu zaman bir arıza değil bir varsayılandır: http.Server.IdleTimeout sıfır olduğunda ReadTimeout'a düşer, Google ise 2.5 dakikalık bir boşta kalma zaman aşımı ister ve nginx 75 saniyede kapatır
  • Yalnızca bazı exchange'lerin tetiklediği senkron bir feature sorgusu; burada kuyruk size değil uzak bir depoya aittir

Dördünün hiçbiri bir collector ayarıyla onarılmaz ve bu ayrımın ölçüm araçları vardır. Bir CPU profili collector faturalarını sembole göre ayırır: handler'a yazılanlar için runtime.gcAssistAlloc, arka plan marking'i için runtime.gcBgMarkWorker. Stop-the-world süresi /sched/pauses/total/gc:seconds, çalışmaya hazır bekleme /sched/latencies:seconds ve accept kuyruğu süreç dışından ListenOverflows üzerinden okunur.

Hangi Go onarımına ihtiyacınız olduğuna tek bir ayrım karar verir. Stop-the-world duraklaması aynı anda her goroutine'e yazılır, dolayısıyla tüm bağlantılarda düz bir sıçrama olarak görünür. Mark assist ise ayırmayı yapan goroutine'e yazılır, dolayısıyla en çok ayırma yapan isteklere iner. Assist'i iki şey düşürür: bid request başına daha az bayt ya da arka plan işçilerinin marking'in daha büyük kısmını üstlendiği daha uzun bir döngü. Trafik karışımı değiştiğinde bunlardan yalnızca ilki ayakta kalır.

Mark assist, bid request'inizin ödediği faturadır

Go collector'ı eşzamanlıdır ve resmi rehber, duraklama süresinin heap boyutuyla ölçeklenmediğini açıkça söyler; dolayısıyla stop-the-world geçişleri kısadır. Asıl önemli kaynak assist'lerdir: ayırma hızlı olduğunda goroutine'ler collector'a yardım eder, çünkü arka plan marking'i işlemcilerin sabit bir çeyreğini alır ve eksik kalan kısım ayırmayı yapana yazılır.

Hız, bunu bir eğim değil bir eşik yapar. Ayırma hızı, sabit bir arka plan payına karşı QPS ile bid request başına baytın çarpımıdır; bu yüzden trafiğinizin beşte birinde hiç assist etmeyen bir kod, 100K QPS'te neredeyse her istekte assist edebilir.

Marking maliyeti çöple değil, canlı işaretçi grafiğiyle orantılıdır ve bir bidder tam da marking için yanlış olan şekli tutar: kampanya indeksleri, kitle segmentleri, frekans önbellekleri. Discord 2020'de aynı bulguyu yayımladı: belleğin boş olup olmadığına karar vermek için bir LRU önbelleğinin tamamını tarayan bir collector. Tek bir ifadenin altında beş mekanizma saklanır:

  • Stop-the-world geçişleri: aynı anda her bağlantıda düz bir sıçrama; tek başına bir açık artırmayı kaybettirecek kadar uzun sürmesi nadirdir
  • Mark assist: trace'te hiç duraklama görünmez, yalnızca büyümüş bir handler süresi - bunu GC CPU'sunun assist payından okuyun
  • Marking maliyeti: ayırma artmadığı hâlde canlı heap büyüdüğünde yükselen GC CPU'su; bunu düz diziler kıpırdatır, daha az çöp kıpırdatmaz
  • Scheduler çekişmesi: çalışmaya hazır olduğu hâlde yürütülmeden bekleyen bir istek; bunu scheduler latency metriği gösterir, duraklama metriği göstermez
  • Zorunlu döngü: sakin bir örnekte kabaca iki dakikalık bir periyotla gelen sıçrama; bu, trafiğinizi değil toplamanın alt sınırını işaret eder

Bunu beş ayrı fatura olarak okuyun. Bunlardan tam olarak biri bir collector ayarıyla kapanır ve ayrım ölçülmeden dil değiştirilerek hiçbiri kapanmaz.

Kapalı döngü, ihtiyacınız olan kanıtı siler

Gil Tene bu arızaya coordinated omission adını verdi: ölçen sistem, test edilen sistemle aykırı değerleri ölçmekten kaçınacak şekilde uyum kurar; çünkü kapalı döngü yanıtı bekler ve takılma sırasında göndermeyi durdurur. ScyllaDB 2021'de bir karşılaştırma yayımladı: aynı iş yükü kapalı döngüde 249 mikrosaniyelik bir p99 bildirdi, düzeltmeli açık yük altında ise 665 ms - yaklaşık 2,700 katlık bir sapma.

  • Sabit hızda açık döngü yükü; latency, isteğin çıktığı andan değil hedeflenen gönderim anından itibaren sayılır
  • Üreteç gönderemediği istekleri kuyruğa almıyorsa HdrHistogram tarzı bir düzeltme
  • Ortalama yerine p99 ve p99.9, kendi fan-out'unuz da sayılarak: Dean ve Barroso 2013'te, p99'u bir saniye olan 100 sunucuya dokunmanın isteklerin %63'ünü yavaş bıraktığını gösterdi
  • Canınızı yakan exchange'den kopyalanmış bir trafik profili; zorunlu toplama aralığından daha uzun süre ve aynı cgroup kotası altında koşturulur

Yanıt bekleyen bir yük üreteci, tam da bulmak için kurulduğu takılma sırasında göndermeyi durdurur ve sonra bu sessizliği sonuca ortalar. Ardından yazdırdığı yüzdelik, sizin bidder'ınızı değil üretecin kendisini tarif eder.

Önce daha az bellek ayırın, sonra üç ayarı çevirin

Ayar yapmadan önce durma sayısını adlandırın; çünkü yorgunlukla verilen bir yeniden yazma kararı, karar değildir. Bir değil iki rakam: assist'i besleyen istek başına heap ayırmaları ve marking'i besleyen canlı heap.

Sıra, en büyük kazanç değil, önce kaynak sonra tavan şeklindedir. Assist'in üretildiği yerden başlayın: teklif yolunda escape analysis, ayrılmak yerine yeniden kullanılan buffer'lar ve yepyeni bir nesne grafiği oluşturmak yerine yalnızca ihtiyacınız olan alanları okuyan bir codec. Slice'ları kısa ömürlü tutun: istek buffer'ına açılan bir slice, teklif yaşadığı sürece o buffer'ın tamamını tutar.

  • sync.Pool baskıyı hafifletir ama hiçbir şey vaat etmez: bir öğe herhangi bir anda haber verilmeden kaldırılabilir ve havuza dönmüş bir nesne, dönüşü ile tahliyesi arasında hâlâ marking'e girer
  • Fatura canlı kümedir: kampanya ve segment indekslerini hot path dışında düz dizilere yeniden kurun; böylece işaretlenen grafik kampanya sayısıyla birlikte büyümeyi bırakır
  • GOGC, belleği collector CPU'suyla rehberin açıkça belirttiği bir oranda takas eder: değeri iki katına çıkarmak GC CPU maliyetini kabaca yarıya indirir ve assist payı da onunla birlikte düşer
  • GOMEMLIMIT bu takası bir tavana karşı yapar ve tasarımı gereği yumuşaktır; çünkü katı bir limit, heap sıçramasını süresiz bir takılmaya çevirir
  • Konteyner içinde GOMAXPROCS: Go 1.25 cgroup CPU limitini okur, CPU request'lerini ise açıkça okumaz; dolayısıyla request'i olan ama limiti olmayan bir pod eski davranışı sürdürür

Bu yaklaşımın bir tavanı var: Uber 2021'de, GOGC'yi konteyner bellek limitine karşı ayarlamanın kritik önemdeki servisleri genelinde yaklaşık 70,000 çekirdek kazandırdığını bildirdi. Bu bir maliyet sonucudur, bir yüzdelik sonucu değil.

Bir Rust hot path'inin verdikleri ve bunun bedeli

RTB House, Haziran 2025'te bir JVM teklif servisini anlattı: mikroservislere bölünme çok sayıda küçük istek üretti; eklenen gecikmenin, ortalama 2.5 ms'lik bir isteğe karşı 7 ms içinde kalması gerekiyordu ve 98. ile 99. yüzdelikler sık G1 duraklamaları altında bozuldu. Generational ZGC'ye geçtiler ve bedelini bellekle ödediler.

Bu onarımın neyi gerektirdiğine dikkat edin: geçilecek ikinci bir collector. Go tek bir collector ile gelir ve bu collector takılıp çıkarılabilir değildir; dolayısıyla Go'daki kollar ayırma hızı, canlı kümenin şekli ve GOMEMLIMIT'e karşı GOGC'dir. Bir Rust hot path'inin kaldırdığı şeyler ise nettir: assist yok, arka plan marking'i yok, zorunlu döngü yok. Ortadan kalkmayanların listesi ise çoğu ekibin beklediğinden uzundur:

  • Allocator, üstüne sayfa hataları ve NUMA yerleşimi: Rust standart kütüphanesi varsayılan global allocator'ın belirsiz olduğunu belirtir ve çok sayıda thread altında malloc bir kuyruk kaynağıdır
  • Zamanlama; çünkü worker havuzlu bir async runtime, bloklayan bir görev bir worker'a düştüğü anda Go scheduler'ının etkilerini yeniden üretir
  • Bellek muhasebesi; çünkü GOMEMLIMIT yalnızca Go runtime belleğini kapsar: aynı süreçteki bir Rust allocator'ı tavanınızın dışında kalır ve zorlama OOM killer'a geçer
  • İnsanlar; yani gece hot path için kimin nöbette olduğu ve tek bir repoda iki toolchain'in neye mal olduğu
  • Go dışarıda kalırsa sınırın kendisi: Cockroach Labs 2015'te bir cgo çağrısını 171 ns, bir Go çağrısını ise 1.83 ns olarak ölçtü ve ayakta kalan şey bu orandır

Kuyruk; isteğin ayrıştırılmasında, exchange'e giden bağlantıda ya da scheduler kuyruğunda yaşıyorsa Rust bu milisaniyelerin hiçbirini geri getirmez ve yanlış bileşeni yeniden yazmak, aynı zaman aşımı oranını korumak için bir çeyrek dönemi harcamaktır.

Bu yüzden servisi değil, ayırmaların sahibi olan en küçük parçayı taşıyın: gösterim değerlendirme döngüsü ve indeksleri, aday seçimi, hedefleme, frekans ve bütçe sorguları, puanlama. Sınırın bedelini geçiş başına hesaplayın: bid request başına bir kez, düz bir buffer ile; asla hedefleme kuralı başına bir kez değil. Doğrulamayı, aynalanmış bir akışla beslenen ayrı örneklerde yapın; asla test edilen sürecin içinde değil - orada bir gölge yol, ölçtüğünüz iki büyüklüğü de ikiye katlar.

Her iki yolda da işe yarayan bir kabul testi: kısa bir pencerede, kendi belirlediğiniz bir bellek tavanı altında collector kapalı koşun ve p99.9'u handler'ın içinde kaydedin. Bunu, trafiğin bir kısmını alan tek bir örnekte otomatik geri alma ile çalıştırın; çünkü GOGC kapalıyken tavana çarpan bir heap sıçraması runtime'ı arka arkaya döngülere sokar ve rehber bu takılmanın süresiz olabileceğini söyler. Sonucu yalnızca GC CPU sınırlayıcısı hiç devreye girmediyse okuyun: devreye girdiği anda yüzdelik, sınırlayıcıyı tarif eder.

Bunu düzelten firma önce zaman aşımı raporunu ister

Sorunun ikinci yarısının - bu işi kim yapar - bir satıcı listesine ihtiyaç duymayan bir testi var. GC kaynaklı latency'yi onaran bir firma, yeniden yazım satan bir firmadan farklı davranır:

  • Repoyu istemeden önce tmax dağılımını ve exchange bazında zaman aşımı raporunu ister
  • Ayrımı - handler, scheduler, hat - açıkça söyler ve bir dil önermeden önce milisaniyelerin üçünden hangisine ait olmasını beklediğini belirtir
  • Kendi açık döngü üretecini ve trafik profilini getirir, kapalı döngü yüzdeliğini kanıt olarak kabul etmez
  • Çıkış ölçütünü önceden söyler: hangi exchange, hangi yüzdelik, tmax'ına karşı ne kadar pay, bin istek başına ne kadar bellek
  • Sonrasında hot path'e ekip ayırabilir; çünkü gece kimsenin nöbet tutmadığı bir yeniden yazım, ikinci olayın kendisidir

Hiçbiri güven gerektirmez: her biri ilk görüşmede isteyebileceğiniz bir belgedir ve muğlak dönen bir yanıt, teşhisin atlandığını söyler.

Yani ilk soru hangi dil değildir. Soru, zaman aşımına uğrayan bağlantıda eksik milisaniyelerin üç toplamdan - handler, scheduler ya da hat - hangisine ait olduğu ve siz üzerine gittiğinizde bid request başına ayrılan baytın kıpırdayıp kıpırdamadığıdır.

amBrain'in kamuya kanıtlayabildikleri: amBrain; trading platformları, matching engine'ler, real-time bidding sistemleri ve casino platformu mühendisliği alanlarında uzmanlaşmış bir yazılım geliştirme şirketidir, 2019'dan beri yazılım geliştiriyoruz ve AdTech'te kurduğumuz şey DSP geliştirme, real-time bidding platformları ve ad exchange mühendisliğidir. Üç formatta çalışıyoruz: tam teslim, özel ekip ya da sizin ekibinize yerleşen mühendisler. Bir yeniden yazımı bir ayar turuna karşı tartıyorsanız, konuşmaya değer görüşme, bir dil seçmeden önce ayrımı yapan görüşmedir.

Masada buna benzer bir tasarım mı var?

Mevcut mimarinizi ve sizi endişelendiren hata senaryosunu getirin; yarım saatte birlikte üzerinden geçelim.

İlgili Yazılar

Görsel yüklenemedi
AdTech
Sep 10, 202610 dk okuma

Reklam Ölçümlemesi Tepe Yükte Olay Kaybediyor: Ek Yerleri, Mükerrer Anahtarlar ve Bidder Join'i

Yazıyı oku
Görsel yüklenemedi
AdTech
Mar 5, 20267 dk okuma

Yapay Zekâ 2026'da Programatik Reklamcılığı Nasıl Yeniden Şekillendiriyor

Yazıyı oku
Görsel yüklenemedi
AdTech
Feb 14, 20266 dk okuma

Gizlilik Öncelikli Hedefleme: Üçüncü Taraf Çerezler Olmadan Ad Tech Kurmak

Yazıyı oku