Go Programming

Go'da Yüksek Geçişli Hız Sınırlamayı Ustalaşmak: Bir Token Havuzu Uygulaması

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.

Share: