Sliding Window’u Ölçmek: .NET + Azure Deney Defteri
Bazı şeyleri iddia ederek serinin son yazısına kadar geldik. Ufak çaplı örneklerle de bir önce ki makalede göstermeye çalıştım. Fakat bir şeyi iddia etmek ile ölçerek gerçek veriler üzerinden…
Şu serinin parçası: Rate Limiting
- Token Bucket: .NET ile Rate Limiting’in Temel Algoritması
- Sliding Window: Token Bucket’ın Yeterli Olmadığı Yerler
- Production’da Sliding Window: Tek Instance Yetmediğinde
- Sliding Window’u Ölçmek: .NET + Azure Deney Defteri
Bu sayfada
Henüz hedeflediğim yerde olmayabilirim . Ancak dün olduğum yerden daha yakınım…

Sliding Window’u Ölçmek
Bazı şeyleri iddia ederek serinin son yazısına kadar geldik. Ufak çaplı örneklerle de bir önce ki makalede göstermeye çalıştım. Fakat bir şeyi iddia etmek ile ölçerek gerçek veriler üzerinden konuşmak başka bir şeydir.
Serinin sonunu Azure üzerinde bazı kurulum, özel metrikler ve testlere ayırdım. Toplamda beş deney yaptım. Hangi algoritmanın ne zaman ne yaptığına ve global limit gerçekten global mi bunu ölçtüm.
Test Mimarisi
Ölçümün güvenilir olabilmesi için ortamın net olması gerekir. Kurulum şu şekilde:
- Azure Container Apps: API’nin çok replikalı çalıştığı yer. Replika sayısının sabitlemek için min ve max replikayı eşitledim. Yani autoscaling kapalı.
- Azure Managed Redis: Replikaların paylaştığı tek ortak state.
- Azure Load Testing: Yük testini uygulayan servis.
- Application Insights + OpenTelemetry: Latency, hata oranı ve custom rate-limiter metriklerini toplayan katman.
Örnek uygulamamızda toplamda dört endpoint vardır. Bunlar: fixed, in-memory (sliding), token-bucket (hepsi replika) ve /api/transfers (redis global state). Böylece her test için doğru algoritmayı doğru endpoint’e yönledirmiş oluyoruz.

Test Ortamının Mimarisi
Adil Karşılaştırma Kuralları
Yapacağımız karşılaştırmanın anlamlı olması için tüm testlerde bazı değerleri sabit tuttum. Bunlar:
- Aynı Azure region
- Aynı container image
- Aynı CPU/Memory
- Autoscaling kapalı
- Warm-up yapılmış
- Her test öncesi Redis state temizliği
- Replika sayısı test tipine göre ayarlandı. Algoritma davranışını izole eden testler için 1 replika dağıtık testleri ölçen testler için 3 replika
Test Senaryomuz
Bu testleri yaparken örnek aldığım senaryo: bir müşteri 60 saniyede en fazla 10 transfer başlatabilir.
- Endpoint: POST /api/transfers (algoritma varyantları)
- Partition key: CustomerId
- Test ortamında X-Customer-Id header’ı dikkate alındı. Production ortamda tavsiye etmem.
Uyguladığım testlerin komutlarını yazının sonunda kaynak olarak paylaşacağım. Görseller ve sonuçlar yazıyı yeterince uzattığı için komutları buraya eklemiyorum. Azure tarafında kurduğum mimarinin kurulum rehberini ve testlerde kullandığım warm-up dosyalarını, açıklamalarıyla birlikte ayrıca vereceğim.
Test 1 — Fixed vs Sliding: Sızıntı Testi
Kurulum algoritmayı izole etmek için 1 replika olarak yapıldı. Aynı müşteri pencerenin son saniyesinde (zaman diliminin son parçası) 10 istek ve yeni zaman diliminin ilk penceresinde 10 istek atıyor. Yani ~2–3 saniye de 20 istek.
Burada ki amaç sızıntıyı gerçek bir cloud ortamda göstermek. Fixed Window bu istekleri kabul ederken Sliding Window 429 döner.
| Algoritma | Gönderilen | Batch arası | Kabul | Red | Sonuç |
|-----------------------------|------------|-------------|-------|-----|--------|
| Fixed Window (/fixed) | 10 + 10 | 60 sn | 20 | 0 | Pencere resetlendi ikinci grup da kabul edildi (sızıntı davranışı) |
| Sliding Window (/in-memory) | 10 + 10 | 30 sn | 10 | 10 | Batch 1 hala son 60 sn içinde ikinci grup reddedildi |


Test 1 Azure Çıktıları
Not: .NET içinde yer alan Fixed limiter’ı penceresini ilk isteğe göre başlattığından aynı bekleme süresinde sızıntıyı yakalayamayız. Bu yüzden Fixed tarafında 60 saniye’lik bir bekleme varken Sliding tarafında bu bekleme 30 saniye olarak ayarlanmıştır.
Test 2 — Sürekli Trafik
Bu testin amacı normal yük altında algoritmaların kabul davranışlarını ve latency değerlerini karşılaştırmaktır.
| Algoritma | Gönderilen | Kabul | Red | p50 (ms) | p95 (ms) | p99 (ms) |
|----------------|------------|-------|-----|----------|----------|----------|
| Fixed Window | 120 | 20 | 100 | 0.111 | 0.181 | 0.308 |
| Sliding Window | 120 | 20 | 100 | 0.115 | 0.202 | 0.276 |
| Token Bucket | 120 | 24 | 96 | 0.117 | 0.206 | 0.371 |
Burada kabul değerinin 20 çıkmasının sebebi testi 60 saniye olarak yapmadık. 120 saniye süren bu teste ikinci pencereye geçiş olduğu için sonuç normal. Bu testten çıkarılması gerekenler:
- Sürekli ve düzgün trafikte Fixed ve Sliding denktir. Aralarında ki farkı sadece boundary ve burst’da görebiliriz. Token Bucket ise 24 kabul ile farkını gösteriyor. 6 saniyede 1 token dolduğundan sürekli olan bir trafikte bu algoritma fazladan geçirir.
- Bence en önemli çıktı bu test için burası. Latency neredeyse yok. p99’da 0.3–0.4 ms, p50’de ~0.11ms. Yani rate limiting kararının kendisi bedava sayılır. İşte bu sonuç bize algoritma kararının bir performans sebebi değil mimari bir karar olduğunu net bir şekilde gösteriyor.


Test 2 Sonuçları

Test 2 p50 Sonuçları

Test 2 p95 Sonuçları

Test 2 p99 Sonuçları
Test 3 — Burst Trafiği
Replikanın 1 olduğu son testimiz. Aynı müşteri t=0’da tek seferde 100 istek gönderiyor. Sonra ki 59 saniyede istek gönderilmiyor. Burada ki amacımız ani bir yığılmada üç algoritmanında tepkisini ölçmek.
| Algoritma | Burst | Kabul | Red |
|----------------|-------|-------|-----|
| Fixed Window | 100 | 10 | 90 |
| Sliding Window | 100 | 10 | 90 |
| Token Bucket | 100 | 10 | 90 |
Burada üç algoritmanında aynı sonucu verdiğini görüyoruz. Karşılaştırmanın adil olması için nominal hızı sabit tuttum. Token Bucket’ın asıl farkı kapasitesini nominal hızın üstüne çıkardığında burst’e izin vermesidir.

Nominal Limitlerin eşit olduğu anda üç algoritmanın davranışı
Test 4 — Multi Replika
Artık replika sayısını 3’e çıkardım. Daha önce bahsettiğim gibi eğer global bir limit uygulanacak ise sayacın ortak olması gerekmektedir. Şuanda her bir replika kendi sayacına sahip.
| Algoritma | Replika | Gönderilen | Kabul | Red |
|----------------------------|---------|------------|--------|-----|
| Sliding Window (in-memory) | 3 | 40 | 30 | 10 |
| Fixed Window (in-memory) | 3 | 40 | 30 | 10 |
Her bir algoritmaya toplamda 40 istek atıp limiti 10 olarak ayarlamamıza rağmen kabul değerinin 30 olduğunu görüyoruz. Bunun sebebi her bir replika’nın kendi sayacını bulunduruyor olmasıdır.
| Replika (instance) | Kendi saydığı kabul |
|--------------------|---------------------|
| `...-75jr4` | 10 |
| `...-7nzvp` | 10 |
| `...-mnqs4` | 10 |
Limit 60 saniye de 10 iken 3 replikada 30 istek geçmiş oldu.


Her bir instance için veriyi gösterir
Not: KQL kodunda gördüğünüz partition startswith “d4-” ibaresi yapılan 5 testi ayrı ayrı almak için konulmuş bir ayraçtır. Her bir isteğin header’ına eklenen X-Customer-Id’nin değeri bu şekilde ayrıştırılmıştır. Kaynak kod içerisinde gerekli açıklama eklenmiştir.
Test 5 — Redis ile Global Limit
Replika’nın 3 olduğu yeni ortamda bu sefer sayaç olarak Redis’i kullanıyorum. Aynı müşteriyle 40 istek atıp sonuçları topluyorum. Redis state deney öncesi temizleniyor.
| Replika | Gönderilen | Kabul | Red |
|---------|------------|-------|-----|
| 3 | 40 | 10 | 30 |
Not: Redis latency bu test aşamasında ölçülmedi. Amacımız sayacın ortak olduğunda sonucun ne olduğunu görmek. Redis kaynaklı oluşacak olan network latency farklı bir konudur.

Ortak sayaç sonrası Azure çıktısı
Son olarak 4. ve 5. testlerin azure tarafında ki verilerine tek ekranda bakalım.

Test 4 ve 5’in aynı ekranda görüntüsü
Sonuç olarak aynı kod, aynı 3 replika ve aynı istek yükünde farklı 2 sonuç. Aralarında ki tek fark sayacın bulunduğu yer. Global olarak tanımladığımız bir yapıda bizi şaşırtacak bir sonuç.
Buraya kadar sabırla okuyarak zaman ayırdığınız için teşekkür ederim. Geri dönüşleriniz benim için değerlidir.