Modern dağıtık sistemler dünyasında, API'nizi trafik patlamalarına ve kötü niyetli istismarlara karşı korumak vazgeçilmezdir. Basit sabit pencere sayaçları kolay uygulanabilir olsa da, genellikle pencere sıfırlandığında isteklerin ikiye katlandığı sınır sorunlarından muzdarip olurlar. Token Havuzu Algoritması daha üstün bir alternatif sunar; patlayıcı trafiğe izin verirken ortalama bir hızı korur. Bu yazıda, Go'da iplik güvenli ve yüksek performanslı bir hız sınırlayıcı uygulama sürecine derinlemesine ineceğiz.
Neden Token Havuzu Algoritması?
Token havuzu algoritması basit bir ilkeye dayanır: Tokenler sabit bir hızla bir havuza eklenir. Her istek bir token tüketir. Eğer havuz boşsa, istek reddedilir veya ertelenir. Bu model, havuzun maksimum kapasitesine kadar kısa süreli trafik patlamalarına izin verdiği için esnektir; bu da uzun vadeli istikrardan ödün vermeden ara sıra artan talepleri yönetmesi gereken API'ler için idealdir.
Go'da Uygulama Zorlukları
Go'da eşzamanlılık (concurrency) uygularken iplik güvenliği (thread safety) hayati önem taşır. Token sayısını belirlemek için standart bir tamsayı kullanamayız; yarış durumlarını (race conditions) önlemek için eşitleme ilkelere ihtiyacımız vardır. Durumumuzu korumak için sync.Mutex kullanacağız. Ancak, yüksek geçişli senaryolarda her istek için kilitlenmek darboğaza neden olabilir. Bu nedenle, uygulamamız verimli zaman hesaplamaları kullanarak kilit rekabetini (lock contention) minimize etmeyi hedefler.
Temel Uygulama
Aşağıda Token Havuzu'nun sağlam bir uygulaması yer almaktadır. Hızı (saniyedeki token sayısı), kapasiteyi (izin verilen maksimum token sayısı) ve mevcut token sayısını tutan bir yapı tanımlıyoruz. Ayrıca son yenileme zamanını izlemek için bir zamanlayıcı başlatıyoruz.
package ratelimiter
import (
"sync"
"time"
)
// TokenBucket iplik güvenli bir hız sınırlayıcı uygular.
type TokenBucket struct {
mu sync.Mutex
rate float64 // saniyedeki token sayısı
capacity float64 // izin verilen maksimum token sayısı
tokens float64 // mevcut token sayısı
lastTime time.Time // son yenileme zamanı
}
// NewTokenBucket yeni bir hız sınırlayıcı örneği oluşturur.
func NewTokenBucket(rate, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity, // Tam dolu başlat
lastTime: time.Now(),
}
}
// Allow bir istekin izin verilip verilmediğini kontrol eder. Geçen süreye göre tokenları yeniler.
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastTime).Seconds()
// Geçen süreye göre tokenları yenile
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
tb.lastTime = now
// Yeterli tokenımız olup olmadığını kontrol et
if tb.tokens >= 1.0 {
tb.tokens--
return true
}
return false
}
Yüksek Geçiş İçin Optimize Etme
Yukarıdaki uygulama, mutex sayesinde iplik güvenlidir. Ancak aşırı yüksek geçişli ortamlarda, her istek süresince kilidi tutmak gecikmeyi etkileyebilir. Daha fazla optimizasyon için kritik olmayan yol güncellemeleri için atomic.Float64 kullanmayı düşünebilirsiniz; ancak mutex yaklaşımı genellikle çoğu API geçidi için yeterlidir. Başka bir optimizasyon tekniği, token havuzu ile birlikte "kayan pencere" (sliding window) yaklaşımı kullanmak veya daha karmaşık analitiklere ihtiyacınız varsa son istek zaman damgaları için bir halka arabelleği (ring buffer) uygulamaktır.
Pratik Kullanım Örneği
Bunu bir Gin veya Echo web sunucusuna entegre etmek basittir. İşte bunu bir ara katman (middleware) bağlamında nasıl kullanabileceğinize dair bir örnek:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"yourproject/ratelimiter"
)
func main() {
// Saniyede 10 istek, 20'lik bir patlama kapasitesi ile izin ver
limiter := ratelimiter.NewTokenBucket(10, 20)
router := gin.Default()
router.GET("/api/resource", func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Hız sınırı aşıldı",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Başarılı",
})
})
router.Run(":8080")
}
Sonuç
Bir hız sınırlayıcı uygulamak, her backend geliştiricisi için kritik bir beceridir. Token Havuzu algoritmasını seçerek, trafik patlamalarını esnek bir şekilde yönetme yeteneği kazanırken sisteminizin yük altında dayanıklı kalmasını sağlarsınız. Burada sağlanan Go uygulaması, kullanım kolaylığını iplik güvenliğiyle dengeleyerek üretim seviyesindeki API'ler için sağlam bir temel oluşturur. Ölçeklendirmeyi daha da ileri götürdükçe, Redis tabanlı algoritmalar gibi dağıtık hız sınırlama çözümlerini keşfetmeyi düşünün; ancak tek örneklilik (single-instance) hizmetleri için bu Go-native yaklaşım verimli ve güvenilirdir.