← blog · 31 Ekim 2026

Aşırı Yükte Yük Atma: Eşzamanlılık Sınırı, Kuyruk Süresi, Son Tarih ve Kendini Batıran Sağlık Denetimleri

Kapasitesinin üstünde istek alan servis çökmez, istemcinin vazgeçtiği işleri yapmaya devam eder. Sınırlı kuyruk, eşzamanlılık sınırı, son tarih kontrolü ve önceliklendirmeyle fazla yükü erken ve ucuz biçimde reddetmenin yolu ve en sık düşülen tuzaklar.

Bir servis kapasitesinin üstünde istek aldığında genellikle çökmez; daha kötüsünü yapar. İstekleri kabul etmeye devam eder, iç kuyruklar uzar, her isteğin bekleme süresi istemcinin zaman aşımını geçer ve sonunda sunucu, istemcinin çoktan vazgeçtiği işleri yapmakla meşgul olur. Ölçüldüğünde işlemci yüzde yüzde, verim normalin üstünde görünür ama başarılı yanıt oranı sıfıra yaklaşır. Buna iyi verim çöküşü (goodput collapse) denir. Çözüm daha fazla kaynak değil, kapasitenin üstündeki isteği erken ve ucuz biçimde reddetmektir: yük atma.

Bu yazı yeniden deneme bütçesi ya da kullanıcı başına hız sınırlamasıyla aynı şey değil. Hız sınırlama "bu müşteri payını aştı" der; yük atma "ben şu an daha fazlasını kaldıramam" der. İlki adalet, ikincisi hayatta kalma meselesidir ve ikisine de ihtiyacınız olur.

Sınırsız kuyruk aşırı yükü gecikmeye çevirir

Çoğu sunucu yazılımının varsayılanı, gelen bağlantıyı kabul edip bir iş parçacığı ya da olay döngüsü boşalana kadar bekletmektir. Bu bekleme yeri fiilen sınırsız bir kuyruktur. Little yasası burada işe yarar: sistemdeki ortalama iş sayısı, geliş hızı ile ortalama süre çarpımına eşittir (L = λW). Saniyede 500 istek kaldırabilen bir servise saniyede 600 istek gelirse, fazlalık her saniye kuyruğa 100 iş ekler. Bir dakika sonra kuyrukta 6000 iş vardır ve yeni gelen bir istek 12 saniye bekler. İstemcinin zaman aşımı 5 saniyeyse bu noktadan sonra yapılan işin tamamı çöpe gider.

Dolayısıyla ilk kural: her kuyruğun bir üst sınırı olsun ve sınır, bekleme süresi istemcinin sabrını aşmayacak kadar küçük seçilsin.

Üç yaklaşım

Sabit eşzamanlılık sınırı. Aynı anda işlenen istek sayısına bir tavan koyarsınız; tavan doluysa yeni istek hemen 503 alır. Basittir, anlaşılırdır ve en çok bunu kullanmanız gerekir. Zorluğu doğru sayıyı bulmaktır: sayı, servisin darboğazına göre seçilir. Darboğaz bir veritabanı bağlantı havuzuysa, havuz boyutunun biraz üstü iyi bir başlangıçtır; işlemciyse çekirdek sayısının birkaç katı. Sayıyı yük testiyle bulun: eşzamanlılığı artırırken p99 gecikmenin kırıldığı noktanın biraz altını seçin.

Kuyrukta geçen süreye göre atma. Sınırı iş sayısıyla değil, isteğin kuyrukta beklediği süreyle koyarsınız. İstek işlenmeye alınırken ne kadar beklediğine bakılır; eşik aşılmışsa iş yapılmadan reddedilir. Bu, ağ ekipmanlarındaki CoDel algoritmasının uygulama düzeyindeki karşılığıdır ve sabit sayıdan daha dayanıklıdır, çünkü isteklerin maliyeti değiştiğinde de doğru davranır.

Uyarlanabilir sınır. Sınırı gözlenen gecikmeye göre sürekli ayarlayan algoritmalar (TCP tıkanıklık denetiminden esinlenen AIMD ya da gradyan tabanlı yöntemler). Gecikme yükselince sınır daralır, düşünce genişler. Bazı vekil sunucular ve istemci kütüphaneleri bunu hazır sunar. Güçlüdür ama davranışını açıklamak zordur; gece yarısı neden isteklerin reddedildiğini anlamaya çalışırken bu bir maliyettir.

Benim önerim: sabit eşzamanlılık sınırıyla başlayın, üstüne kuyruk süresi kontrolü ekleyin. Uyarlanabilir sınırı yalnızca istek maliyetleri çok değişken olduğu ve sabit sayıyı tutturamadığınız kanıtlandığında devreye alın.

Uygulama: bekletmeyen bir sınır

Go ile yazılmış, standart kütüphaneden başka bir şey gerektirmeyen bir ara katman:

package main

import (
	"net/http"
	"time"
)

type Limiter struct {
	slots   chan struct{}
	maxWait time.Duration
}

func NewLimiter(n int, maxWait time.Duration) *Limiter {
	return &Limiter{slots: make(chan struct{}, n), maxWait: maxWait}
}

func (l *Limiter) Wrap(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t := time.NewTimer(l.maxWait)
		defer t.Stop()
		select {
		case l.slots <- struct{}{}:
			defer func() { <-l.slots }()
			next.ServeHTTP(w, r)
		case <-t.C:
			w.Header().Set("Retry-After", "2")
			http.Error(w, "overloaded", http.StatusServiceUnavailable)
		case <-r.Context().Done():
		}
	})
}

func main() {
	api := http.NewServeMux()
	api.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(100 * time.Millisecond)
		w.Write([]byte("ok\n"))
	})

	root := http.NewServeMux()
	root.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok\n"))
	})
	root.Handle("/", NewLimiter(50, 20*time.Millisecond).Wrap(api))

	http.ListenAndServe(":8080", root)
}

Burada üç karar var. Kanal dolu olduğunda istek en fazla 20 milisaniye bekler; bu küçük pencere anlık dalgalanmaları emer ama uzun bir kuyruk oluşturmaz. İstemci bağlantıyı kapatırsa (r.Context().Done()) iş hiç başlamaz. Sağlık denetimi ucu sınırın dışında kalır; neden önemli olduğu aşağıda.

Son tarihe saygı

Reddetmenin en ucuz anı, iş başlamadan öncesidir. İstemci isteğe bir son tarih iliştiriyorsa, sunucu işe başlamadan kalan süreye bakmalı ve işi bitiremeyecekse hiç başlamamalıdır. gRPC bunu protokol düzeyinde yapar: istemcinin koyduğu son tarih sunucuya taşınır ve sunucunun yaptığı alt çağrılara da bağlam üzerinden aktarılabilir. HTTP'de standart bir alan yoktur; kendi başlığınızı tanımlayıp bağlama son tarih olarak işlemeniz gerekir. Go'da işleyicinin başında ctx.Deadline() ile kalan süreyi okuyup tipik işlem süresinden azsa reddetmek birkaç satırdır.

Bu kontrolü yalnızca giriş katmanında değil, servisler arası her çağrıda yapın. Zincirin ortasındaki bir servis, üstündeki çağıranın vazgeçtiğini bilmezse kendi altındaki veritabanına boşuna yük bindirir.

Önceliklendirme

Her istek eşit değildir. Ödeme onayı ile arka planda çalışan bir rapor dışa aktarımı aynı havuzu paylaşıyorsa, aşırı yükte ikisi birlikte düşer. En az iki sınıf tanımlayın ve ayrı sınırlar verin: kullanıcının beklediği etkileşimli istekler ve beklemeye dayanabilen toplu işler. Toplu işin sınırı dolunca etkileşimli trafik etkilenmez. Daha incesi tek havuzda öncelik sırasıdır, ama ayrı havuzlar hem daha basit hem de gözlemlemesi daha kolaydır.

Tuzaklar

Hazırlık denetimini yük atmaya bağlamak. Aşırı yükte hazırlık (readiness) denetimi başarısız olursa yük dengeleyici o örneği havuzdan çıkarır. Kalan örneklere daha çok yük düşer, onlar da düşer ve birkaç dakika içinde hiç örnek kalmaz. Canlılık (liveness) denetimi düşerse daha da kötüsü olur: örnek yeniden başlatılır, ısınma süresi boyunca hiç iş yapamaz. Sağlık uçları sınırın dışında olmalı ve "süreç yaşıyor ve bağımlılıklarına ulaşabiliyor" sorusuna cevap vermeli, "şu an boş mu" sorusuna değil.

Reddetmenin pahalı olması. Reddetme yolu istek gövdesini okuyor, kimlik doğrulama yapıyor ya da veritabanına bakıyorsa, yük atma işe yaramaz. Ret kararı, isteğin işlenmesinde mümkün olan en erken noktada ve neredeyse sıfır maliyetle verilmeli.

İstemcinin hemen yeniden denemesi. 503 alan istemci beklemeden tekrar denerse, attığınız yük geri gelir ve iki katına çıkar. Retry-After başlığını gönderin, istemci tarafında üstel geri çekilme ve yeniden deneme bütçesi uygulayın. Kendi istemci kütüphanelerinizin 503'e nasıl davrandığını test edin; çoğu varsayılan olarak hemen tekrar dener.

Durum kodunu karıştırmak. Kullanıcı kotasını aşan isteğe 429, sunucunun kapasitesi dolduğu için reddedilen isteğe 503 dönün. Alarmlarınız da buna göre ayrışsın: 429 artışı genellikle bir müşterinin davranışıdır, 503 artışı sizin kapasite sorununuzdur.

Otomatik ölçeklemeye güvenmek. Yeni örneklerin ayağa kalkması dakikalar sürer, ani bir dalga saniyeler içinde gelir. Ölçekleme, yük atmanın yerine geçmez; yük atma, ölçekleme yetişene kadar servisi ayakta tutan şeydir. Ölçekleme metriği olarak ret oranını da kullanabilirsiniz.

Sınırı her örnekte ayrı ayrı düşünmemek. Eşzamanlılık sınırı örnek başınadır. Örnek sayısı değişince toplam kapasite değişir, ama alttaki veritabanının kapasitesi değişmez. On örnek, her biri 50 eşzamanlı istekle, tek bir veritabanına 500 eşzamanlı sorgu gönderebilir. Sınırı darboğazın gerçekten bulunduğu yere göre hesaplayın.

Ölçün

Yük atma görünmezse yanlış ayarlandığını fark etmezsiniz. En azından şunları metrik olarak yayınlayın: anlık işlenen istek sayısı, reddedilen istek sayısı ve isteklerin kuyrukta geçirdiği süre. Ret oranı normal günlerde sıfıra yakın olmalı; normal trafikte de ret görüyorsanız sınır çok sıkıdır.

Ne zaman gerekmez

Trafiği düşük ve tahmin edilebilir bir iç araç için bu karmaşıklık değmez. Kuyruk tüketen asenkron işçilerde de gerekmez: orada geri basıncı kuyruğun kendisi sağlar, işçi kaldırabildiği kadar mesaj çeker ve fazlası broker'da bekler. Bu doğru bir tasarımdır, çünkü o işi bekleyen bir kullanıcı yoktur. Yük atma, bir kullanıcının ya da çağıranın yanıtı belirli bir sürede beklediği eşzamanlı yollar içindir. O yollarda reddetmek, geç yanıt vermekten her zaman daha iyidir: hızlı bir hata, istemciye başka bir yol deneme şansı verir; yavaş bir başarısızlık ise herkesi birlikte batırır.