İçeriğe geç

Rate Limiting — Kapıdan Saniyede Kaç Kişi Geçer?

Orta 10 dk Çok sık karşılaşılır

Önce şunu oku: REST Tasarımı ve Idempotency

30 saniyede özet

İstekleri anahtar başına sınırlamak servisi hem kötü niyetten hem de istemcilerin kazara yağdırdığı isteklerden korur. Sınırı aşan 429 ve ne zaman döneceğini alır. Asıl karar, sayacın nasıl saydığı ve nerede durduğu.

Popüler bir kafenin kapısında bir görevli düşün. Herkesi aynı anda içeri alırsa mutfak durur; kimseyi almazsa kafe boş kalır.

Kova kısa bir kalabalığa izin verir, sonra hızı sabitler.
Adım adım oku
  1. Kapıda bir jeton kavanozu var; her istek bir jeton alıp içeri girer.
  2. Ani bir kalabalık gelir ve kavanozdaki jetonları bitirir.
  3. Jetonu kalmayan istek kibarca geri çevrilir: 429 ve biraz sonra gel.
  4. Kavanoza her saniye bir jeton geri damlar; bekleyen istek sırası gelince geçer.
  1. Bayt: Servis çöktü! Loglarda tek bir müşteri saniyede yüzlerce istek atmış.

  2. Sen: Saldırı mı?

  3. Bayt: Hayır, bizim mobil uygulama. Hata alınca beklemeden tekrar deniyormuş, sonsuza kadar.

  4. Bayt: Kapıya bir görevli koysak? Kaç kişi, hangi hızla geçecek, ona karar verelim.

429: kibar bir “biraz sonra gel”

rate limitingBir istemcinin belirli sürede yapabileceği istek sayısını sınırlamak. Aşan istek 429 Too Many Requests alır; birden çok gateway varsa sayaç paylaşılmalıdır.Sözlükte gör →, bir istemcinin belirli sürede yapabileceği istek sayısını sınırlar. Sınırı aşan istek işlenmez, 429 Too Many Requests alır.

İyi bir 429 ne zaman dönüleceğini de söyler. Retry-AfterSunucunun istemciye ne zaman tekrar deneyebileceğini söylediği HTTP başlığı. Saniye sayısı ya da bir tarih olur; çoğunlukla 429 ve 503 ile gelir.Sözlükte gör → başlığı istemciye kaç saniye beklemesi gerektiğini bildirir.

Sınır aşıldı
HTTP/1.1 429 Too Many Requests
Retry-After: 3
Content-Type: application/problem+json
{ "title": "Çok fazla istek", "detail": "Bu anahtar için sınır 10 saniyede 10 istek." }

Sınır bir anahtar başına sayılır: kullanıcı, API anahtarı ya da en son çare IP. Aynı şirket ağının arkasındaki yüz kişi tek bir IP’den görünebilir; IP başına sınır onları birbirine karıştırır.

Kafam karıştı, daha basit anlat

Kapıdaki görevli sayar. Sınırı aşana “şimdi olmaz, üç saniye sonra gel” der. Kimi saydığı da önemli: kişiyi mi, binayı mı?

Hızlı kontrolBaşlangıç

Sınırı aşan bir isteğe sunucu en uygun hangi cevabı döner?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kimliği doğrulanmış kullanıcıların kullandığı bir API'de sınırı neye göre saymak en doğru?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Pencereyle saymak

En basit sayaç bir sabit penceredir: her on saniyede bir sıfırlanan bir sayı. Birinci pencere ilk on saniyeyi, ikinci pencere sonraki on saniyeyi sayar.

Sınır: 10 saniyede 10 istek, sabit pencere. İstemci 10. saniyede 10, 11. saniyede 10 istek atıyor. Kaç tanesi geçer? Cevabı göster

Yirmisi de geçer. İlk on istek birinci pencerenin son saniyesine düşüyor, sonraki on istek sayacın sıfırlandığı ikinci pencereye. İki saniyede sınırın iki katı geçti.

Kayan pencere bunu düzeltir: her istekte son on saniyeye bakılır. Bedeli hafızadır; kabul edilen her isteğin zamanı tutulur ya da iki pencerenin ağırlıklı ortalaması hesaplanır.

İki pencerenin de ortak bir huyu var: ani bir kalabalık hakkın hepsini bir anda harcar. Pencere boşalana kadar saniyede tek istek atan sakin kullanıcı da reddedilir.

Kafam karıştı, daha basit anlat

Sabit pencere, saat başı sıfırlanan bir sayaçtır. Sıfırlanmadan hemen önce ve hemen sonra gelen iki kalabalık ikisi de geçer.

Hızlı kontrolOrta

Sınır 60 saniyede 100 istek, sabit pencereyle. İstemci 59. saniyede 100, 61. saniyede 100 istek atıyor. Kaç istek geçer?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Satır satır: token bucket

token bucketKapasitesi kadar jeton tutan ve belli bir hızla dolan bir kova. Her istek bir jeton harcar; kova kısa patlamalara izin verir, uzun vadede hızı dolum hızında tutar.Sözlükte gör → iki ayarla çalışır. Kapasite bir anda ne kadar patlamaya izin verileceğini, dolum hızı uzun vadeli ortalamayı belirler.

Kovada kaç jeton var?

TokenBucket.java
1final class TokenBucket {
2 private final double capacity;
3 private final double tokensPerMs;
4 private double tokens;
5 private long lastMs;
6
7 TokenBucket(double capacity, double tokensPerSecond, long nowMs) {
8 this.capacity = capacity;
9 this.tokensPerMs = tokensPerSecond / 1000;
şu an çalışan satır this.tokens = capacity; // starts full
11 this.lastMs = nowMs;
12 }
13
14 synchronized boolean tryAcquire(long nowMs) {
15 tokens = Math.min(capacity, tokens + (nowMs - lastMs) * tokensPerMs);
16 lastMs = nowMs;
17 if (tokens < 1) return false;
18 tokens -= 1;
19 return true;
20 }
21}

Debug

Adım 1/6

kurulum new TokenBucket(3, 1, 0): kapasite 3, saniyede 1 jeton. Kova dolu başlar.

tokens
= 3
Java 21UTF-8LF10:1

Sol/sağ ok tuşlarıyla da gezebilirsin.

Jetonlar ayrı ayrı sayılmaz; her istekte geçen süre kadar jeton eklenir. Bu yüzden kova bir zamanlayıcıya ihtiyaç duymaz.

synchronized tek bir JVM içindeki yarışı çözer. Uygulama üç kopya çalışıyorsa her birinin kendi kovası olur; o sorunu tuzaklarda göreceğiz.

Hızlı kontrolOrta

Kova dolu başlıyor. Ne yazdırılır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
Burst.java
1TokenBucket bucket = new TokenBucket(3, 1); // capacity 3, +1 token per second, starts full
2StringBuilder out = new StringBuilder();
3for (int second = 1; second <= 3; second++) {
4 bucket.refill(); // one second passed; never above capacity
5 for (int i = 0; i < 2; i++) {
6 out.append(bucket.tryAcquire() ? "✓" : "✕");
7 }
8 out.append(' ');
9}
10System.out.println(out.toString().trim());
Java 21UTF-8LF

Bir token bucket'ta kapasite 20, dolum hızı saniyede 5. Hangisi doğru?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Her sütun bir saniye: yeşil geçenler, taralı ✕ ile gösterilenler 429 alanlar. Sınır hep aynı: 10 saniyede 10 istek.

Kapıda kaç istek geçer?

Tohum 1

geçti 429kesikli çizgi: sabit pencerenin sınırı

15101520
  • pencere 1–10. sn: 0/10
Geçen
✓ 0
429 alan
✕ 0
10 sn'de en çok geçen / sınır 10
✓ 0
Hız
Adım 0

Şu an ne oldu?

Sınır: 10 saniyede 10 istek

Oynat'a bas. Her sütun bir saniye: yeşil geçen, kırmızı 429 alan istekler.

Görevler0/3

  • Tek instance'la iki saniyede 20 isteği içeri alaçık

    İpucu

    Sabit pencerede patlamaları pencere sınırının iki yanına koy.

  • Ani yükte sakin isteklerin hiçbiri 429 almasınaçık

    İpucu

    Patlamaya kısa süre izin verip sonra hızı sabitleyen algoritmayı seç. Sayaçları çoğaltmak hile sayılır.

  • Selde sınırın üç katı isteği geçiraçık

    İpucu

    Sayaçları instance'lara böl.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanı oynat. Sabit pencere, sınırda iki patlama: yirmisi de geçiyor. Algoritmayı kayan pencere yap; ikinci patlama reddediliyor.
  2. Trafiği “Sakin + ani 15” yap. Sabit ve kayan pencerede sakin kullanıcı da 429 alıyor. Token bucket’a geç: patlamanın onu geçiyor, sakin istekler hiç reddedilmiyor.
  3. Trafiği “Sel” yap, kurulumu “3 instance, her biri kendi sayacıyla”. On saniyede otuz istek geçiyor. Ortak sayaca geç: yine on.
Hızlı kontrolOrta

Uygulama yük dengeleyicinin arkasında 4 kopya çalışıyor. Her kopya kullanıcı başına bellekte 100/dk sınır tutuyor. Bir kullanıcı pratikte dakikada kaç istek geçirebilir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Her instance kendi sayacını tutuyor. Yük dengeleyici istekleri dağıtır; her kopya kendi on hakkını verir. Sayaç ortak bir yerde, örneğin Redis’te durur ve oku-karar ver-yaz tek bir atomik adımda yapılır.

Yanlış anahtar. IP başına sınır, aynı ağdaki herkesi tek kişi sayar. Kimliği doğrulanmış istekte kullanıcıyı ya da API anahtarını say.

İstemcinin tekrar denemesi. 429 alan istemci hemen tekrar denerse yükü büyütür. Retry-After’a uyar, yoksa exponential backoffBaşarısız her denemeden sonra bekleme süresini katlayarak artırmak. Süreye rastgele bir pay (jitter) eklenir ki istemciler aynı anda geri dönmesin.Sözlükte gör → ile bekler ve bekleme süresine rastgele bir pay ekler.

Kafam karıştı, daha basit anlat

Üç kapı görevlisi ayrı ayrı sayarsa kafeye üç kat kişi girer. Görevliler aynı defteri kullanmalı.

Hızlı kontrolOrta

Her özellik hangi algoritmaya ait?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Sabit pencere

Her pencerede sıfırlanan tek bir sayı

    Kayan pencere

    Hep son N saniyeye bakar

      Token bucket

      Kapasite ve dolum hızı

        Derinleş · API anahtarı başına sınır: Redis'te ortak kova 3 dosya · ~76 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/resources/scripts/ token_bucket.lua Lua betiği: oku, doldur, jeton al ve yaz tek adımda çalışır. Redis betiği çalıştırırken başka komut araya girmez; iki instance aynı jetonu harcayamaz. Saat Redis'ten okunur, instance'ların saat farkı işe karışmaz.

        src/main/resources/scripts/token_bucket.lua
        -- KEYS[1] = bucket key, ARGV[1] = capacity, ARGV[2] = tokens per second
        local capacity = tonumber(ARGV[1])
        local rate = tonumber(ARGV[2])
        local time = redis.call('TIME') -- Redis clock, not the caller's
        local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
        local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
        local tokens = tonumber(bucket[1]) or capacity -- a new key starts full
        local ts = tonumber(bucket[2]) or now
        tokens = math.min(capacity, tokens + (now - ts) / 1000 * rate)
        local allowed = 0
        if tokens >= 1 then
        tokens = tokens - 1
        allowed = 1
        end
        redis.call('HSET', KEYS[1], 'tokens', tokens, 'ts', now)
        redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / rate * 1000)) -- a full bucket needs no key
        local retry_ms = 0
        if allowed == 0 then
        retry_ms = math.ceil((1 - tokens) / rate * 1000)
        end
        return { allowed, retry_ms }

        src/main/java/com/shop/api/ RateLimitFilter.java Filtre: anahtarı doğrulanmış kullanıcıdan alır, IP'ye yalnızca giriş yapılmamışsa düşer. Kova her istekte Redis'te bir kez sorulur.

        src/main/java/com/shop/api/RateLimitFilter.java
        @Component
        class RateLimitFilter extends OncePerRequestFilter {
        private static final String CAPACITY = "20";
        private static final String TOKENS_PER_SECOND = "10";
        @SuppressWarnings("rawtypes") // Redis returns a list of Longs; the script's result type is erased
        private static final RedisScript<List> TOKEN_BUCKET =
        RedisScript.of(new ClassPathResource("scripts/token_bucket.lua"), List.class);
        private final StringRedisTemplate redis;
        RateLimitFilter(StringRedisTemplate redis) {
        this.redis = redis;
        }
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
        FilterChain chain) throws ServletException, IOException {
        String key = "rate:" + clientKey(request);
        @SuppressWarnings("unchecked")
        List<Long> result = redis.execute(TOKEN_BUCKET, List.of(key), CAPACITY, TOKENS_PER_SECOND);
        if (result.get(0) == 1L) {
        chain.doFilter(request, response);
        return;
        }
        TooManyRequests.write(response, result.get(1));
        }
        private static String clientKey(HttpServletRequest request) {
        Principal user = request.getUserPrincipal();
        return user != null ? "user:" + user.getName() : "ip:" + request.getRemoteAddr();
        }
        }

        src/main/java/com/shop/api/ TooManyRequests.java Cevap: 429 ile birlikte Retry-After saniye olarak döner. İstemci ne zaman döneceğini tahmin etmek zorunda kalmaz.

        src/main/java/com/shop/api/TooManyRequests.java
        final class TooManyRequests {
        private TooManyRequests() {
        }
        static void write(HttpServletResponse response, long retryMs) throws IOException {
        // Retry-After is whole seconds; round up so the client never comes back too early.
        long seconds = Math.max(1, (retryMs + 999) / 1000);
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.setHeader(HttpHeaders.RETRY_AFTER, String.valueOf(seconds));
        response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
        response.getWriter().write("{\"title\":\"Too Many Requests\",\"status\":429}");
        }
        }

        Kendini sına

        Önce hızlı bir ısınma: puan yok, kayıt yok. Sonra asıl sorular.

        Şimşek turu1/5

        429 alan istemci en doğru ihtimalle hemen tekrar denemeli.

        Soru 1/3İleri

        Bu senaryoda hangi yolu seçersin?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        SenaryoAçık bir ödeme API'si. Partnerler API anahtarıyla bağlanıyor ve sözleşmede anahtar başına dakikada 600 istek yazıyor. Servis otomatik ölçeklemeyle 3 ile 12 kopya arasında çalışıyor; önünde zaten bir API gateway ve Redis var. Partnerler gün sonunda toplu işlerle ani patlamalar yapıyor.

        Aklında kalacak üç şey

        1. 1 Sınırı aşan istek 429 alır; Retry-After başlığı istemciye ne zaman döneceğini söyler. İyi istemci bekler ve tekrar denerken araya rastgele bir pay koyar.
        2. 2 Sabit pencere ucuzdur ama pencere sınırında iki katı geçirir. Token bucket kısa bir patlamaya kapasitesi kadar izin verir, sonra dolum hızında sabitler.
        3. 3 Birden fazla instance varken sayaç ortak bir yerde, örneğin Redis'te, tek atomik adımda güncellenir. Herkes kendi sayacını tutarsa sınır instance sayısıyla çarpılır.
        Sonraki kapı Fiyatı değiştirdin, cache'i de güncelledin. Ekran neden hâlâ eski fiyatı gösteriyor? Cache Stratejileri — Defter Ne Zaman Yalan Söyler? · 10 dk

        4 kart sonraki derste seni bekliyor

        0/4 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.